v3 : socle Rust (moteur pur, accès trustinWallet, serveur) ; maquette Nuxt rangée dans maquette/

- crates/ld-engine : formule inertielle (parité avec la maquette, garde C),
  coquille / changement de fond, assistant d'édition à règles, composition
  d'un document sous vote permanent (adoptions, aperçus, rivales).
- crates/trustin-access : qui voit, qui ouvre, qui gère les accès.
- crates/ld-server : API axum + SQLite, connexion Ğ1 (client OAuth2 de
  connect.monnaie-libre.fr), invitations, entrée en vigueur automatique,
  signalements de contradiction, traductions éditoriales, publication git,
  taille de la toile lue sur la chaîne.
- seeds/acte-engagement-certification.json : 33 articles, fr + en + es ;
  en vigueur : préambule, E1, E2 (seed a minima).
- maquette/ : frontend, backend et docker v2 déplacés tels quels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-21 02:04:42 +02:00
co-authored by Claude Opus 5.5
parent 4b1732916d
commit 67ddee28b2
275 changed files with 8979 additions and 0 deletions
@@ -0,0 +1,187 @@
<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">&nbsp;{{ 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>