- Aujourd'hui (Fil 13 sections + capture sticky) + Le chemin (tunnel 2 gestes, Q0 inline, 3 chips, dérogation asymétrique, alternatives réglage/consignation) - Registre + fiche décision (timeline, périmètre premier/second lieu auditable, affluence non-ignorable, S'instruire condensé, éléments + cartographie de clôture, épreuve du réel, Remettre en question, PV A4, gravure) - Salle de vote 5 modalités (consentement, nuancé+histogramme, binaire hérité avec jauge inertielle, Réglage collectif complet — faisceau, médiane basse, Pour moi, Explorer, cristallisation-geste —, élection à départage humain) - Textes (Pacte en clair, document vivant, diff, vue projetée, Atelier des formules porté du v1 sur le moteur unique) + Mandats (faits comptés, feux de la rampe, wizard 3 étapes) + Observatoire (consigner→observer→protocoliser) - Onboarding 7 gabarits + Données locales (export/import, attributs, atelier) - voting→framing gardé (Reformuler d'un réglage figé non cristallisé) - Pages v1 supprimées (login, documents, mandates, protocols, sanctuary, tools, decisions/new) — 342 tests verts, build zéro erreur Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
188 lines
7.0 KiB
Vue
188 lines
7.0 KiB
Vue
<script setup lang="ts">
|
||
// <!-- ld-v2 --> Un curseur du Réglage collectif : libellé métier, valeur en 700,
|
||
// statu quo losange creux, alternative clavier + champ numérique, poignée ≥2.25rem.
|
||
// Faisceau strip-plot : points translucides anonymes, médiane trait épais,
|
||
// baseline marqueur creux. Part « calculé » : rail estompé, jamais saisissable.
|
||
import { BIMODAL_BANNER, PARAM_DERIVED_LABEL } from '~/lexicon'
|
||
import type { ParamDef } from '~/types/domain'
|
||
|
||
const props = defineProps<{
|
||
param: ParamDef
|
||
value: number
|
||
dots?: number[]
|
||
median?: number
|
||
locked?: boolean
|
||
neutral?: boolean
|
||
bimodal?: boolean
|
||
}>()
|
||
const emit = defineEmits<{ (e: 'update:value', v: number): void }>()
|
||
|
||
function frac(v: number): number {
|
||
const span = props.param.max - props.param.min
|
||
if (span <= 0) return 0
|
||
return Math.min(1, Math.max(0, (v - props.param.min) / span))
|
||
}
|
||
function fmt(v: number): string {
|
||
return v.toLocaleString('fr-FR', { maximumFractionDigits: 2 })
|
||
}
|
||
function onInput(event: Event) {
|
||
const raw = Number((event.target as HTMLInputElement).value)
|
||
if (Number.isFinite(raw)) emit('update:value', raw)
|
||
}
|
||
const inputId = `vps-${props.param.key}-${Math.random().toString(36).slice(2, 7)}`
|
||
</script>
|
||
|
||
<template>
|
||
<!-- ld-v2 -->
|
||
<div class="vps" :class="{ 'vps--derived': param.derived, 'vps--neutral': neutral, 'vps--locked': locked }">
|
||
<div class="vps__head">
|
||
<label class="vps__label" :for="inputId">{{ param.label }}</label>
|
||
<span v-if="param.derived" class="vps__tag">{{ PARAM_DERIVED_LABEL }}</span>
|
||
<span class="vps__value">
|
||
{{ fmt(value) }}<span v-if="param.unit" class="vps__unit"> {{ param.unit }}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="vps__row">
|
||
<div class="vps__rail-zone">
|
||
<!-- Faisceau + repères, calés sur la course utile de la poignée -->
|
||
<div class="vps__overlay" aria-hidden="true">
|
||
<span
|
||
v-for="(dot, i) in dots ?? []"
|
||
:key="i"
|
||
class="vps__dot"
|
||
:style="{ '--p': frac(dot) }"
|
||
/>
|
||
<span
|
||
v-if="param.baseline !== undefined"
|
||
class="vps__baseline"
|
||
:style="{ '--p': frac(param.baseline) }"
|
||
:title="`Aujourd'hui : ${fmt(param.baseline)}`"
|
||
/>
|
||
<span
|
||
v-if="median !== undefined"
|
||
class="vps__median"
|
||
:style="{ '--p': frac(median) }"
|
||
:title="`Médiane : ${fmt(median)}`"
|
||
/>
|
||
</div>
|
||
<input
|
||
v-if="!param.derived"
|
||
:id="inputId"
|
||
class="vps__range"
|
||
type="range"
|
||
:min="param.min"
|
||
:max="param.max"
|
||
:step="param.step"
|
||
:value="value"
|
||
:disabled="locked"
|
||
:aria-label="param.label"
|
||
@input="onInput"
|
||
>
|
||
<div v-else class="vps__ghost">
|
||
<div class="vps__ghost-fill" :style="{ width: `${frac(value) * 100}%` }" />
|
||
</div>
|
||
</div>
|
||
<input
|
||
v-if="!param.derived"
|
||
class="vps__num"
|
||
type="number"
|
||
:min="param.min"
|
||
:max="param.max"
|
||
:step="param.step"
|
||
:value="value"
|
||
:disabled="locked"
|
||
:aria-label="`${param.label} — saisie directe`"
|
||
@change="onInput"
|
||
>
|
||
</div>
|
||
|
||
<p v-if="bimodal" class="vps__bimodal">
|
||
<UIcon name="i-lucide-split" />
|
||
<span>{{ BIMODAL_BANNER(param.label) }}</span>
|
||
</p>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.vps { display: flex; flex-direction: column; gap: 0.2rem; --vps-accent: var(--mood-accent); }
|
||
.vps--neutral { --vps-accent: var(--mood-text-muted); }
|
||
.vps__head { display: flex; align-items: baseline; gap: 0.5rem; }
|
||
.vps__label { font-size: 0.9375rem; font-weight: 600; }
|
||
.vps__tag {
|
||
font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
|
||
color: var(--mood-text-muted); background: var(--mood-bg);
|
||
padding: 1px 8px; border-radius: var(--r-pill);
|
||
}
|
||
.vps__value { margin-left: auto; font-weight: 700; font-size: 1.0625rem; color: var(--vps-accent); }
|
||
.vps__unit { font-size: 0.8125rem; font-weight: 600; color: var(--mood-text-muted); }
|
||
.vps__row { display: flex; align-items: center; gap: 0.75rem; }
|
||
.vps__rail-zone { position: relative; flex: 1; min-width: 0; }
|
||
|
||
/* Faisceau et repères : course utile = largeur − poignée (2.25rem) */
|
||
.vps__overlay { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
|
||
.vps__overlay > * { position: absolute; top: 50%; left: calc(1.125rem + (100% - 2.25rem) * var(--p)); }
|
||
.vps__dot {
|
||
width: 8px; height: 8px; margin: -4px 0 0 -4px; border-radius: 50%;
|
||
background: var(--vps-accent); opacity: 0.25;
|
||
}
|
||
.vps__median {
|
||
width: 4px; height: 20px; margin: -10px 0 0 -2px; border-radius: 2px;
|
||
background: var(--vps-accent);
|
||
}
|
||
.vps__baseline {
|
||
width: 10px; height: 10px; margin: -5px 0 0 -5px;
|
||
transform: rotate(45deg);
|
||
background: var(--mood-surface);
|
||
box-shadow: inset 0 0 0 2px var(--mood-text-muted);
|
||
}
|
||
|
||
/* Poignée tactile ≥ 2.25rem, clavier natif */
|
||
.vps__range {
|
||
appearance: none; -webkit-appearance: none;
|
||
display: block; width: 100%; height: 2.5rem;
|
||
background: transparent; cursor: pointer; position: relative; z-index: 2;
|
||
}
|
||
.vps__range:disabled { cursor: not-allowed; opacity: 0.6; }
|
||
.vps__range::-webkit-slider-runnable-track {
|
||
height: 8px; border-radius: 4px;
|
||
background: color-mix(in srgb, var(--vps-accent) 18%, var(--mood-bg));
|
||
}
|
||
.vps__range::-webkit-slider-thumb {
|
||
-webkit-appearance: none; width: 2.25rem; height: 2.25rem; margin-top: -14px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle at center, var(--vps-accent) 0 7px, color-mix(in srgb, var(--vps-accent) 22%, var(--mood-surface)) 8px);
|
||
box-shadow: 0 1px 4px var(--mood-shadow);
|
||
}
|
||
.vps__range::-moz-range-track {
|
||
height: 8px; border-radius: 4px;
|
||
background: color-mix(in srgb, var(--vps-accent) 18%, var(--mood-bg));
|
||
}
|
||
.vps__range::-moz-range-thumb {
|
||
width: 2.25rem; height: 2.25rem; border: none; border-radius: 50%;
|
||
background: radial-gradient(circle at center, var(--vps-accent) 0 7px, color-mix(in srgb, var(--vps-accent) 22%, var(--mood-surface)) 8px);
|
||
box-shadow: 0 1px 4px var(--mood-shadow);
|
||
}
|
||
|
||
/* Part calculée : rail estompé, non saisissable */
|
||
.vps__ghost {
|
||
height: 8px; margin: 1.05rem 1.125rem;
|
||
border-radius: 4px; background: var(--mood-bg); opacity: 0.75; overflow: hidden;
|
||
}
|
||
.vps__ghost-fill { height: 100%; background: color-mix(in srgb, var(--mood-text-muted) 35%, transparent); transition: width 0.15s ease; }
|
||
.vps--derived .vps__value { color: var(--mood-text-muted); }
|
||
|
||
.vps__num {
|
||
width: 5.25rem; min-height: 2.25rem; padding: 0.25rem 0.5rem;
|
||
font-size: 0.9375rem; font-weight: 600; text-align: right;
|
||
box-shadow: inset 0 0 0 1.5px var(--mood-input-border);
|
||
}
|
||
.vps__num:focus-visible { box-shadow: inset 0 0 0 1.5px var(--mood-input-focus); }
|
||
|
||
.vps__bimodal {
|
||
display: flex; align-items: baseline; gap: 0.4rem; margin: 0.15rem 0 0;
|
||
font-size: 0.8125rem; font-weight: 600; color: var(--mood-warning);
|
||
background: var(--mood-status-fenetre-bg); border-radius: var(--r-input); padding: 0.4rem 0.65rem;
|
||
}
|
||
</style>
|