- overlays Nuxt UI (UModal/USlideover) : géométrie posée en CSS (assets/css/overlays.css) — les utilitaires tailwind attendus par @nuxt/ui ne sont pas générés (UnoCSS ne scanne pas node_modules) : drawer mobile et modales de confirmation se rendaient hors écran - a11y : title/description sur les 3 overlays (DialogTitle/Description rendus, warnings reka-ui éteints), fermeture du drawer ancrée et testée - suppression du collectif actif : bascule sur un collectif restant — jamais d'accueil orphelin (test store ajouté, 343 vitest verts) - observatoire : la route « transmis » entre dans les barres (base 100 %) - overflows mobiles à zéro : formules KaTeX (défilement), table vigueur (défilement < 768px), pill cible (retour à la ligne), inputs decider et mandat (border-box) - vérifié navigateur : drawer, modale, 14 routes sans débordement, zéro erreur console Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
253 lines
10 KiB
TypeScript
253 lines
10 KiB
TypeScript
// ─────────────────────────────────────────────────────────────
|
|
// libreDecision v2 — lexicon: SINGLE source of French UI labels.
|
|
// Tone rules (BLUEPRINT-V2.md):
|
|
// 1. tutoyer la personne dans ses gestes ;
|
|
// 2. actes collectifs à l'infinitif (« Adopter notre Pacte ») ;
|
|
// 3. sujet grammatical des phrases générées = les personnes ou le collectif,
|
|
// JAMAIS la formule ou le système.
|
|
// The anti-lexicon vitest test (tests/lexicon.spec.ts) enforces FORBIDDEN_UI_TERMS
|
|
// over every UI-visible string (lexicon + .vue templates).
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
import type {
|
|
DecisionRoute,
|
|
DecisionStatus,
|
|
InertiaPreset,
|
|
NominationMethod,
|
|
NuancedValue,
|
|
VoteMethod,
|
|
Weight,
|
|
Reversibility,
|
|
} from '~/types/domain'
|
|
|
|
/** Words that must NEVER appear in UI-visible text (case-insensitive, accent-aware).
|
|
* They may exist as code identifiers — the test only scans UI strings. */
|
|
export const FORBIDDEN_UI_TERMS = [
|
|
'déléguer',
|
|
'délégation',
|
|
'délégué',
|
|
'dette',
|
|
'retard',
|
|
'en souffrance',
|
|
'corpus',
|
|
'snapshot',
|
|
'verdict',
|
|
'triage',
|
|
'oligarchie',
|
|
'peuple adulte',
|
|
] as const
|
|
|
|
// ── Routes — first person: the path is a gesture, not a category ──
|
|
export const ROUTE_LABELS: Record<DecisionRoute, string> = {
|
|
solo: 'Je décide',
|
|
mandate: 'Je décide, sous mandat',
|
|
transmit: 'Je transmets',
|
|
advice: "J'écoute, puis je décide",
|
|
collective: 'Nous décidons',
|
|
record: 'Je le consigne',
|
|
}
|
|
|
|
/** Short pill forms (registry filters). */
|
|
export const ROUTE_SHORT: Record<DecisionRoute, string> = {
|
|
solo: 'seul',
|
|
mandate: 'sous mandat',
|
|
transmit: 'transmis',
|
|
advice: 'sur avis',
|
|
collective: 'ensemble',
|
|
record: 'consigné',
|
|
}
|
|
|
|
export const ROUTE_ICONS: Record<DecisionRoute, string> = {
|
|
solo: 'i-lucide-zap',
|
|
mandate: 'i-lucide-key-round',
|
|
transmit: 'i-lucide-send',
|
|
advice: 'i-lucide-message-circle',
|
|
collective: 'i-lucide-users',
|
|
record: 'i-lucide-notebook-pen',
|
|
}
|
|
|
|
export const ICONS = {
|
|
urgent: 'i-lucide-siren',
|
|
alreadyDecided: 'i-lucide-book-open-check',
|
|
recurrence: 'i-lucide-repeat',
|
|
parametric: 'i-lucide-sliders-horizontal',
|
|
crystallize: 'i-lucide-stamp',
|
|
} as const
|
|
|
|
// ── Decision states — plain French pills ──
|
|
export const STATUS_LABELS: Record<DecisionStatus, string> = {
|
|
draft: 'préparation',
|
|
advice: 'avis',
|
|
objection: 'objection',
|
|
framing: 'formulation',
|
|
voting: 'vote',
|
|
adopted: 'en vigueur',
|
|
rejected: 'rejeté',
|
|
revoked: 'révoqué',
|
|
transmitted: 'transmis',
|
|
closed: 'clos',
|
|
}
|
|
|
|
/** Session display state (frozen lives on VoteSession, shown as a decision badge). */
|
|
export const FROZEN_LABEL = 'figé'
|
|
export const FROZEN_BANNER = 'Votes figés — en attente de cristallisation'
|
|
|
|
// ── Weight & reversibility — the tunnel chips ──
|
|
export const WEIGHT_LABELS: Record<Weight, string> = {
|
|
light: 'rien de lourd',
|
|
binding: 'du temps ou des ressources',
|
|
structural: 'la structure, les règles, du long terme',
|
|
}
|
|
export const REVERSIBILITY_LABELS: Record<Reversibility, string> = {
|
|
easy: 'facilement réversible',
|
|
costly: 'coûteux à défaire',
|
|
irreversible: 'irréversible',
|
|
}
|
|
|
|
// ── Nuanced scale — never « CONTRE » ──
|
|
export const NUANCED_LABELS: Record<NuancedValue, string> = {
|
|
0: 'Pas du tout',
|
|
1: 'Plutôt non',
|
|
2: 'Réservé',
|
|
3: 'Plutôt oui',
|
|
4: 'Oui',
|
|
5: 'Tout à fait',
|
|
}
|
|
|
|
// ── Vote methods ──
|
|
export const METHOD_LABELS: Record<VoteMethod, string> = {
|
|
consent: 'Consentement',
|
|
binary: 'Pour / contre',
|
|
nuanced: 'Vote nuancé',
|
|
parametric: 'Réglage collectif',
|
|
election: 'Élection',
|
|
}
|
|
export const BINARY_COST = 'deux camps, un perdant'
|
|
export const BINARY_DESCRIPTION = "l'outil de dernier recours des très grands corps"
|
|
|
|
// ── The review — l'épreuve du réel ──
|
|
export const REVIEW_TITLE = "L'épreuve du réel"
|
|
export const REVIEW_QUESTION = 'Le réel a-t-il suivi ?'
|
|
export const REVIEW_VERDICTS = {
|
|
confirmed: 'Ça tient',
|
|
revise: 'À revoir',
|
|
revoke: 'À révoquer',
|
|
} as const
|
|
|
|
// ── Windows, assent, perimeter ──
|
|
export const WINDOW_OK = 'Ça me va'
|
|
export const WINDOW_OBJECT = "J'objecte"
|
|
export const ASSENT_MISSING = 'Il manque un accord explicite'
|
|
export const BOUNDARY_SUSPENDED = 'compte à rebours suspendu — frontière contestée'
|
|
export const CONCERN_FIRST = 'Concerné·e en premier lieu'
|
|
export const CONCERN_SECOND = "Concerné·e en second lieu — s'est déclaré·e"
|
|
export const CONCERN_ME = 'Ça me concerne'
|
|
export const PERIMETER_OVERFLOW = "Le périmètre déborde — élargis d'un cran"
|
|
export const SCOPE_WIDEN = "Élargir d'un cran"
|
|
export const SCOPE_KEEP = 'Maintenir — en motivant publiquement'
|
|
|
|
// ── Parametric — le réglage collectif ──
|
|
export const PARAMETRIC_ALT = 'C\'est un réglage (montant, taux, répartition) — décidez au curseur'
|
|
export const PARAM_CONSTRAINT_LABEL = 'contrainte'
|
|
export const PARAM_DERIVED_LABEL = 'calculé'
|
|
export const IMPACT_CARD_TITLE = 'Pour moi'
|
|
export const IMPACT_DISCLAIMER = 'estimation sur données déclarées'
|
|
export const EXPLORE_LABEL = 'Explorer'
|
|
export const EXPLORE_DISCLAIMER = 'exploration — ne compte pas'
|
|
export const CRYSTALLIZE_ACTION = 'Cristalliser'
|
|
export const CRYSTALLIZE_CARD = 'Les votes sont figés — cristallise la médiane'
|
|
export const MEDIAN_EXPLANATION =
|
|
'Le collectif retient la médiane de chaque curseur — robuste aux extrêmes, '
|
|
+ "et c'est une position que chacun aurait pu proposer."
|
|
export const BIMODAL_BANNER = (paramLabel: string) =>
|
|
`Deux positions distinctes se dessinent sur ${paramLabel} — scinder la question ou changer de référentiel ?`
|
|
export const CRYSTALLIZE_CHOICES = ['Cristalliser quand même', 'Scinder', 'Reformuler'] as const
|
|
export const CRYSTALLIZE_MODAL_DESC = "Tu figes d'un geste la position que le collectif dessine."
|
|
|
|
// ── Record & observatory ──
|
|
export const RECORD_ALT = 'Déjà tranché — je le consigne'
|
|
export const RECORD_HOW = "Comment ça s'est décidé ?"
|
|
export const OBSERVATORY_TITLE = "L'Observatoire"
|
|
export const OBSERVATORY_SUBTITLE = 'comment nous décidons, dans les faits'
|
|
export const REVOKED_SECTION = "Révoquées — ce qu'on en a appris"
|
|
export const MATURATION_CARD = 'Cette pratique est mûre — protocolise-la en quelques phrases'
|
|
|
|
// ── Élection ──
|
|
export const BLANK_VOTE = 'Vote blanc'
|
|
export const ELECTION_RULE =
|
|
"La personne la plus désignée est nommée ; en cas d'égalité, vous départagez — jamais l'outil."
|
|
|
|
// ── Urgence ──
|
|
export const URGENT_TOGGLE = "C'est urgent"
|
|
export const URGENT_BADGE = 'Décidé en urgence — le collectif ratifie'
|
|
export const URGENT_REFUSED = "Irréversible : l'urgence ne peut pas contourner le collectif."
|
|
|
|
// ── Fil / Aujourd'hui ──
|
|
export const FEED_HEADER = 'À toi de décider'
|
|
export const CAPTURE_PLACEHOLDER = "Qu'est-ce que tu décides ?"
|
|
export const DOSSIER_COMPLETE_CARD = 'Le dossier est complet — clore et publier la cartographie de clôture'
|
|
|
|
// ── Fiche / tunnel ──
|
|
export const PATH_CARD_TITLE = 'Le chemin'
|
|
export const CHOOSE_OTHERWISE = 'Je choisis autrement'
|
|
export const WHY_DISCLOSURE = 'Pourquoi ?'
|
|
export const ENGAGES_LABEL = 'Ce que ça engage'
|
|
export const BASELINE_PREFIX = "Aujourd'hui :"
|
|
export const BASELINE_ARROW = "Aujourd'hui → Proposé"
|
|
export const INSTRUCT_BLOCK = "S'instruire"
|
|
export const REOPEN_HANDLE = 'Remettre en question'
|
|
export const ADOPTED_STAMP = 'Décidé'
|
|
export const ADOPTED_TOAST = "C'est décidé — et révisable, comme tout ici."
|
|
export const VOTE_PRIVACY = "Ton vote est remplaçable — ton historique n'est visible que de toi."
|
|
export const WHO_VOTES = (n: number, date: string) =>
|
|
`Qui vote : ${n} personne${n > 1 ? 's' : ''} — liste arrêtée le ${date}`
|
|
|
|
// ── Mandats ──
|
|
export const MANDATES_TITLE = 'Mandats — les pouvoirs confiés'
|
|
export const MANDATE_EXERCISE = 'Exercice du mandat'
|
|
export const MANDATE_SPOTLIGHT = 'Feux de la rampe'
|
|
export const MANDATE_SPOTLIGHT_SUB = 'toutes les décisions prises sous ce mandat — contestables'
|
|
export const MANDATE_CLAIM = 'Réclamer un mandat'
|
|
export const MANDATE_REVOKE = 'Demander la révocation'
|
|
export const NOMINATION_LABELS: Record<NominationMethod, string> = {
|
|
'ratified-self': 'Auto-désignation ratifiée',
|
|
'election-no-candidate': 'Élection sans candidat',
|
|
'nuanced-vote': 'Vote nuancé',
|
|
consent: 'Consentement',
|
|
draw: 'Tirage au sort',
|
|
rotation: 'Rotation',
|
|
}
|
|
|
|
// ── Textes / Pacte ──
|
|
export const PACT_BADGE = 'les règles du jeu'
|
|
export const PACT_SUBTITLE = 'Notre contrat social — sacralisé, jamais immuable'
|
|
export const INERTIA_LABELS: Record<InertiaPreset, string> = {
|
|
low: 'inertie faible',
|
|
standard: 'inertie standard',
|
|
high: 'inertie haute',
|
|
max: 'inertie maximale',
|
|
}
|
|
export const FIRST_DECISION = 'Adopter notre Pacte'
|
|
export const CREATE_SIGNATURE = 'Osons, nous verrons'
|
|
export const OPENING_DREAM = "Une démocratie qui n'a jamais existé — à votre échelle."
|
|
export const CLOSING_LINE = 'La ville change, le puits demeure.'
|
|
|
|
// ── Preuve / gravure / données ──
|
|
export const PROOF_LOCAL = 'empreinte locale — démo'
|
|
export const SECRET_DISPLAY = "secret d'affichage — démo"
|
|
export const LINEAGE_PREFIX = 'essaimé de'
|
|
export const WORKSHOP_MODE = 'mode atelier'
|
|
export const WORKSHOP_RECORDED = (by: string, forWhom: string, date: string) =>
|
|
`saisi par ${by} pour ${forWhom} — atelier du ${date}`
|
|
export const ATTRIBUTES_HINT =
|
|
'servent uniquement ta carte Pour moi, jamais montrés aux autres'
|
|
export const SYNC_NOTE =
|
|
'Un serveur pourra se brancher plus tard, tes données ne changent pas de forme.'
|
|
export const NO_PROTOCOL_BANNER = 'Aucun protocole — crée-le ou décide sur avis'
|
|
|
|
// ── Génération des phrases de seuil (sujet = le collectif, jamais la formule) ──
|
|
export const thresholdSentence = (W: number, T: number, threshold: number) =>
|
|
`${T.toLocaleString('fr-FR')} votant${T > 1 ? 's' : ''} sur ${W.toLocaleString('fr-FR')} : `
|
|
+ `à si faible participation, il faut presque l'unanimité — ${threshold.toLocaleString('fr-FR')} pour. `
|
|
+ 'Plus de monde vote, plus le seuil descend.'
|