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
+289
View File
@@ -0,0 +1,289 @@
<script setup lang="ts">
/**
* /creer — onboarding en 3 étapes (layout bare) : le premier usage EST le
* tutoriel. Étape 1 identité, étape 2 gabarit, étape 3 personnes.
* Atterrit sur / où la première décision « Adopter notre Pacte » attend.
*/
import { useCollectiveStore } from '~/stores/collective'
import type { SeedName } from '~/stores/collective'
import type { TemplateId } from '~/data/templates'
import { CREATE_SIGNATURE, FIRST_DECISION, OPENING_DREAM } from '~/lexicon'
import { COLOR_CHOICES } from '~/components/onboarding/OnboardingIdentity.vue'
import { TEMPLATE_ICONS } from '~/components/onboarding/OnboardingTemplates.vue'
definePageMeta({ layout: 'bare' })
const store = useCollectiveStore()
const step = ref<1 | 2 | 3>(1)
const name = ref('')
const color = ref(COLOR_CHOICES[0]!.hex)
const template = ref<TemplateId | null>(null)
const meName = ref('')
const members = ref<string[]>([])
const busy = ref(false)
const issues = ref<string[]>([])
const STEP_TITLES: Record<1 | 2 | 3, string> = {
1: 'Donne-lui un nom, une couleur, une ambiance.',
2: 'Choisis un point de départ — tout reste amendable par décision.',
3: 'Toi, puis les premières personnes autour de la table.',
}
const canContinue = computed(() => {
if (busy.value) return false
if (step.value === 1) return name.value.trim().length > 0
if (step.value === 2) return template.value !== null
return meName.value.trim().length > 0
})
function next() {
if (!canContinue.value) return
issues.value = []
if (step.value < 3) step.value++
else void create()
}
function back() {
if (busy.value || step.value === 1) return
issues.value = []
step.value--
}
/** « Les Jardins du Canal » → « les-jardins-du-canal ». */
function slugify(text: string): string {
return (
text
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') || 'collectif'
)
}
function keepBlockingIssues(all: { level: string; message: string }[]): boolean {
const blocking = all.filter(i => i.level === 'error').map(i => i.message)
issues.value = blocking
return blocking.length > 0
}
async function create() {
if (!template.value || busy.value) return
busy.value = true
const result = await store.createFromTemplate(template.value, {
name: name.value.trim(),
slug: slugify(name.value),
color: color.value,
icon: TEMPLATE_ICONS[template.value],
meName: meName.value.trim(),
memberNames: members.value,
})
if (keepBlockingIssues(result.issues) || !result.state) {
busy.value = false
return
}
await navigateTo('/')
}
async function explore(seed: SeedName) {
if (busy.value) return
busy.value = true
issues.value = []
const result = await store.loadSeed(seed)
if (keepBlockingIssues(result.issues) || !result.state) {
busy.value = false
return
}
await navigateTo('/')
}
function onKeydown(ev: KeyboardEvent) {
if (ev.key !== 'Enter') return
const target = ev.target as HTMLElement | null
if (target && ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(target.tagName)) return
next()
}
onMounted(() => window.addEventListener('keydown', onKeydown))
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
</script>
<template>
<!-- ld-v2 -->
<div class="creer">
<header class="creer__head">
<h1 class="creer__dream">{{ OPENING_DREAM }}</h1>
<p class="creer__step-title">{{ STEP_TITLES[step] }}</p>
</header>
<OnboardingIdentity
v-if="step === 1"
v-model:name="name"
v-model:color="color"
@next="next"
/>
<OnboardingTemplates
v-else-if="step === 2"
v-model="template"
@next="next"
@seed="explore"
/>
<OnboardingMembers
v-else
v-model:me-name="meName"
v-model:members="members"
/>
<ul v-if="issues.length" class="creer__issues">
<li v-for="msg in issues" :key="msg">
<UIcon name="i-lucide-circle-alert" /> {{ msg }}
</li>
</ul>
<footer class="creer__nav">
<button v-if="step > 1" type="button" class="ld-btn ld-btn--quiet" :disabled="busy" @click="back">
<UIcon name="i-lucide-arrow-left" /> Retour
</button>
<span v-else class="creer__spacer" />
<span class="creer__dots" role="status" :aria-label="`Étape ${step} sur 3`">
<span
v-for="n in 3"
:key="n"
class="creer__dot"
:class="{ 'creer__dot--active': step === n, 'creer__dot--done': step > n }"
/>
</span>
<button type="button" class="ld-btn" :disabled="!canContinue" @click="next">
<template v-if="step < 3">
Continuer <UIcon name="i-lucide-arrow-right" />
</template>
<template v-else>
<UIcon name="i-lucide-sparkles" /> Fonder le collectif
</template>
</button>
</footer>
<p v-if="step === 3" class="creer__hint">
La première décision t'attendra : « {{ FIRST_DECISION }} ».
</p>
<p v-if="step === 3" class="creer__signature">{{ CREATE_SIGNATURE }}</p>
</div>
</template>
<style scoped>
.creer {
display: flex;
flex-direction: column;
gap: clamp(1.5rem, 4vw, 2.25rem);
width: min(100%, 56rem);
margin-inline: auto;
padding-bottom: 3.5rem;
}
.creer__head {
display: flex;
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
.creer__dream {
margin: 0;
font-size: clamp(1.6rem, 5vw, 2.6rem);
font-weight: 800;
line-height: 1.15;
letter-spacing: -0.02em;
color: var(--mood-text);
}
.creer__step-title {
margin: 0;
font-size: clamp(0.9375rem, 2.5vw, 1.0625rem);
color: var(--mood-text-muted);
}
.creer__issues {
margin: 0;
padding: 0.875rem 1.1rem;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.4rem;
border-radius: var(--r-input);
background: color-mix(in srgb, var(--mood-error) 10%, transparent);
color: var(--mood-error);
font-size: 0.9rem;
font-weight: 600;
}
.creer__issues li {
display: flex;
align-items: center;
gap: 0.45rem;
}
.creer__nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.creer__spacer {
min-width: 5.5rem;
}
.creer__dots {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.creer__dot {
width: 0.55rem;
height: 0.55rem;
border-radius: 50%;
background: var(--mood-text-muted);
opacity: 0.3;
transition: transform 0.12s ease, opacity 0.12s ease, background 0.12s ease;
}
.creer__dot--active {
background: var(--mood-accent);
opacity: 1;
transform: scale(1.25);
}
.creer__dot--done {
background: var(--mood-accent);
opacity: 0.55;
}
.creer__hint {
margin: -0.75rem 0 0;
text-align: center;
font-size: 0.875rem;
color: var(--mood-text-muted);
}
.creer__signature {
margin: 0;
text-align: center;
font-size: 0.875rem;
font-style: italic;
opacity: 0.65;
color: var(--mood-accent);
}
@media (max-width: 480px) {
.creer__nav {
flex-wrap: wrap;
justify-content: center;
}
.creer__dots {
order: -1;
width: 100%;
justify-content: center;
}
.creer__spacer { display: none; }
}
</style>