From 81b604e5f221a2c80ad778e7a63c97ee44d4e916 Mon Sep 17 00:00:00 2001 From: Yvv Date: Fri, 30 Oct 2026 11:21:57 +0100 Subject: [PATCH] =?UTF-8?q?v3=20:=20l'entr=C3=A9e=20=E2=80=94=20accueil=20?= =?UTF-8?q?avec=20le=20choix=20du=20groupe,=20logo=20=C2=AB=20Cl=C3=A9=20d?= =?UTF-8?q?e=20vo=C3=BBte=20=C2=BB,=20quatre=20ambiances,=20menu=20et=20pa?= =?UTF-8?q?ges=20d'outil=20par=20groupe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Accueil : le titre, le leitmotiv en trois lignes (contexte et cadrage · processus et modalités · boîte à outils), puis directement le choix du groupe ; sans session, la connexion à sa place. /groupes redirige vers /, « Mes groupes » et le retour après connexion mènent à /. - Logo « Clé de voûte » du DA (signe en currentColor, clé en --cle, libre Literata italique, Decision romain), droit, dans le menu et la barre mobile ; Stamp retiré, le sceau 井 ne bouge pas. Favicon, icônes 16/32/48, apple-touch, 192/512, manifeste. - Quatre ambiances : pierre et calcaire (claires), nuit et ardoise (sombres), --cle par ambiance ; quatre pastilles à la place du soleil / lune ; première visite selon l'appareil (pierre / nuit). Calcaire et ardoise : textes, accent, mousse et avertissement à 4,5:1 au moins. - Page d'un groupe : les quatre piliers, puis « En cours ». - Menu : nombre de docs en cours à côté de « Docs de référence », surligné seulement dedans, seul Docs réagit au survol ; les trois autres éteints mais ouvrent leur page ; hors d'un groupe, le dernier groupe ouvert. - /g/[group]/docs « En cours » + « À venir » ; /mandats et /protocoles du groupe : « À venir » éteints ; /g/[group]/decisions : une ligne par doc, « N modules à voter », « dont N sans ton vote ». Anciennes adresses /decisions, /mandats, /protocoles : vers le groupe courant, sinon /. - e2e : C1, C2, C11, C12, M1 mis à jour ; C13–C17 pour L1–L6 ; CONTROLES.md. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 19 +- .../chantiers/acte-certification/CONTROLES.md | 14 +- web/e2e/entree.spec.ts | 161 +++++++++++++++ web/e2e/mobile.spec.ts | 9 +- web/e2e/reader.spec.ts | 26 ++- web/src/app.css | 57 +++++- web/src/app.html | 24 ++- web/src/lib/assets/cle-de-voute.svg | 1 + web/src/lib/components/DocCards.svelte | 59 ++++++ web/src/lib/components/GroupChoice.svelte | 144 +++++++++++++ web/src/lib/components/Logo.svelte | 54 +++++ web/src/lib/components/Pillars.svelte | 89 ++++++++ web/src/lib/components/Sidebar.svelte | 191 ++++++++++-------- web/src/lib/components/SoonList.svelte | 49 +++++ web/src/lib/components/Stamp.svelte | 35 ---- web/src/lib/components/ToolSoon.svelte | 62 ++++++ web/src/lib/group.ts | 17 ++ web/src/lib/groups.svelte.ts | 93 +++++++++ web/src/lib/i18n/en.ts | 42 ++-- web/src/lib/i18n/es.ts | 42 ++-- web/src/lib/i18n/fr.ts | 42 ++-- web/src/lib/mood.svelte.ts | 22 +- web/src/routes/+layout.svelte | 13 +- web/src/routes/+page.svelte | 108 ++++------ web/src/routes/connexion/+page.svelte | 2 +- web/src/routes/decisions/+page.svelte | 24 +-- web/src/routes/decisions/+page.ts | 7 + web/src/routes/equipe/+page.svelte | 2 +- web/src/routes/g/[group]/+page.svelte | 89 ++------ .../routes/g/[group]/decisions/+page.svelte | 133 ++++++++++++ web/src/routes/g/[group]/docs/+page.svelte | 60 ++---- web/src/routes/g/[group]/mandats/+page.svelte | 5 + .../routes/g/[group]/protocoles/+page.svelte | 5 + web/src/routes/groupes/+page.svelte | 140 +------------ web/src/routes/groupes/+page.ts | 6 + web/src/routes/invitation/+page.svelte | 2 +- web/src/routes/mandats/+page.svelte | 24 +-- web/src/routes/mandats/+page.ts | 7 + web/src/routes/protocoles/+page.svelte | 24 +-- web/src/routes/protocoles/+page.ts | 7 + web/static/apple-touch-icon.png | Bin 0 -> 3977 bytes web/static/favicon-16.png | Bin 0 -> 410 bytes web/static/favicon-32.png | Bin 0 -> 937 bytes web/static/favicon-48.png | Bin 0 -> 1298 bytes web/static/favicon.svg | 10 +- web/static/icon-192.png | Bin 0 -> 4314 bytes web/static/icon-512.png | Bin 0 -> 12002 bytes web/static/manifest.webmanifest | 16 ++ 48 files changed, 1323 insertions(+), 613 deletions(-) create mode 100644 web/e2e/entree.spec.ts create mode 100644 web/src/lib/assets/cle-de-voute.svg create mode 100644 web/src/lib/components/DocCards.svelte create mode 100644 web/src/lib/components/GroupChoice.svelte create mode 100644 web/src/lib/components/Logo.svelte create mode 100644 web/src/lib/components/Pillars.svelte create mode 100644 web/src/lib/components/SoonList.svelte delete mode 100644 web/src/lib/components/Stamp.svelte create mode 100644 web/src/lib/components/ToolSoon.svelte create mode 100644 web/src/lib/groups.svelte.ts create mode 100644 web/src/routes/decisions/+page.ts create mode 100644 web/src/routes/g/[group]/decisions/+page.svelte create mode 100644 web/src/routes/g/[group]/mandats/+page.svelte create mode 100644 web/src/routes/g/[group]/protocoles/+page.svelte create mode 100644 web/src/routes/groupes/+page.ts create mode 100644 web/src/routes/mandats/+page.ts create mode 100644 web/src/routes/protocoles/+page.ts create mode 100644 web/static/apple-touch-icon.png create mode 100644 web/static/favicon-16.png create mode 100644 web/static/favicon-32.png create mode 100644 web/static/favicon-48.png create mode 100644 web/static/icon-192.png create mode 100644 web/static/icon-512.png create mode 100644 web/static/manifest.webmanifest diff --git a/CLAUDE.md b/CLAUDE.md index 8de392f..8b17ff5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,13 +44,15 @@ crates/ # sur la chaîne) · seed.rs · web.rs (sert web/build) tests/api.rs # scénarios bout en bout seeds/ # acte-engagement-certification.json (trilingue, 33 articles, I1/E1/E2 en vigueur) + # upcoming/.json : les « À venir » des outils d'un groupe (fr, en, es), lus au démarrage web/src/ lib/i18n/ # fr.ts SOURCE (types Key) · en.ts · es.ts — parité testée - lib/components/ # Sidebar, DocPanel, VoteView, ProposeView, UnitBlock, DeepList, Seal, Stamp… - routes/ # / · connexion · invitation · equipe · groupes · g/[group] · g/[group]/docs/[doc] (lecteur, - # panneau dans l'URL ?onglet=&article=) · acces (trustinWallet) · decisions/mandats/ - # protocoles (à venir) -web/e2e/ # Playwright C1–C12 + M1 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 + lib/components/ # Sidebar, DocPanel, VoteView, ProposeView, UnitBlock, DeepList, Seal, Logo, Pillars… + routes/ # / (accueil + choix du groupe) · connexion · invitation · equipe · g/[group] (piliers, en cours) · + # g/[group]/docs · g/[group]/docs/[doc] (lecteur, + # panneau dans l'URL ?onglet=&article=) · g/[group]/decisions · g/[group]/mandats · + # g/[group]/protocoles (à venir) · acces (trustinWallet) ; /groupes, /decisions… redirigent +web/e2e/ # Playwright C1–C17 + M1 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 docker/ # app.Dockerfile · app-compose.yml (sonic) .woodpecker/ # app.yml · maquette.yml docs/ # chantiers/ · adr/ · dev/BLUEPRINT-V2.md (doctrine de la maquette) · recherche/ @@ -71,7 +73,7 @@ maquette/ # v2 intacte (frontend Nuxt, backend FastAPI, docker, docs-v ```bash export PATH=$HOME/.cargo/bin:$PATH cargo fmt --all --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace -cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 13 contrôles +cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 18 contrôles LD_DEV_LOGIN=1 cargo run -p ld-server # :8002, sert web/build cd web && npm run dev # :3002 à chaud @@ -94,8 +96,9 @@ docker build -f docker/app.Dockerfile --target production . # rejoue toute la un `from_secret` absent fait refuser tout le pipeline Woodpecker ; la route hôte `/*` de fabio masque le webroot ACME → arrêter le service avant `acme.sh --issue` ; Playwright en locale `fr-FR` (sinon l'interface part en anglais). -- **Design** : sans bordures, arrondis 16/12/20/14, ambiances `pierre` / `nuit` - (`data-mood`, posé avant le premier rendu), sceau hexagramme 井 #48 en SVG (`Seal`, `Stamp` — +- **Design** : sans bordures, arrondis 16/12/20/14, quatre ambiances `pierre`, `calcaire` + (claires), `nuit`, `ardoise` (sombres) (`data-mood`, posé avant le premier rendu, clé `ld:mood`), logo + « Clé de voûte » (`Logo`, jamais incliné, `--cle` par ambiance), sceau hexagramme 井 #48 en SVG (`Seal` — jamais le glyphe, qui s'affiche mal). Mouvement réduit respecté. - **Domaine** : decision.librodrome.org ; dépôt des textes `git.open.us.org/yvv/decision-depot-textes-reference` (provisoire : puis un dépôt pour l'ouverture, puis le dépôt officiel pour les textes en vigueur). diff --git a/docs/chantiers/acte-certification/CONTROLES.md b/docs/chantiers/acte-certification/CONTROLES.md index 6000f93..77b5c91 100644 --- a/docs/chantiers/acte-certification/CONTROLES.md +++ b/docs/chantiers/acte-certification/CONTROLES.md @@ -9,8 +9,8 @@ Où tester : les contrôles qui écrivent (C5, C6, C7) se font dans le **groupe | # | Contrôle | Geste humain | Attendu | |---|---|---|---| -| C1 | L'accueil | ouvrir `/` sans être connectée | « Rendre possible la décision collective », la phrase d'outils, les quatre sections ; le menu à gauche avec les quatre outils, trois grisés ; « Se connecter avec sa clé Ğ1 » | -| C2 | Les groupes | se connecter, ouvrir « Groupes » | Monnaie libre, Axiom, Librodrome, Groupe local Vallée Drôme Crestois ; **pas** l'Atelier du canal | +| C1 | L'accueil, sans session | ouvrir `/` sans être connectée | « Rendre possible la décision collective », puis le leitmotiv en trois lignes : « contexte et cadrage », « processus et modalités », « boîte à outils » ; « Se connecter avec sa clé Ğ1 » et sa note ; pas de piliers ni de groupes ; le menu à gauche avec les quatre outils, trois éteints | +| C2 | L'accueil, avec session | se connecter : on arrive sur `/` | le titre, le leitmotiv, puis directement le choix du groupe : Monnaie libre en grand, Axiom, Librodrome, Groupe local Vallée Drôme Crestois ; **pas** l'Atelier du canal ; `/groupes` et « Mes groupes » (menu) ramènent ici | | C3 | Le texte en vigueur | Monnaie libre → Docs de référence → Acte d'engagement Certification | trois articles seulement (Préambule, Unicité du compte, Réciprocité), chacun « seed a minima · 0 vote » ; en dessous, « Propositions en cours » (32 lignes) | | C4 | Gestes contextuels | survoler le Préambule → « Le vote » | un panneau : seuil 50 voix pour, il en manque 50 ; « La formule du seuil » se déplie ; l'URL garde le panneau ; Échap le ferme | | C5 | Entrée en vigueur | groupe d'essai : à deux, voter « Pour » sur « Les deux garanties réciproques » | au second vote, « … est entré en vigueur » ; le texte en vigueur compte quatre articles | @@ -21,7 +21,12 @@ Où tester : les contrôles qui écrivent (C5, C6, C7) se font dans le **groupe | C10 | trustinWallet | ouvrir `/acces` sans être administratrice | « La gestion des accès est réservée aux administrateurs de trustinWallet. » (côté admin : créer un lien d'invitation) | | C11 | Une personne, une voix | avec un lien d'invitation, ouvrir le vote d'un article de Monnaie libre | « Ici, seuls les membres de la toile de confiance Ğ1 votent… » à la place des boutons ; dans le groupe d'essai, les boutons sont là | | C12 | Lien d'équipe | administrateur : `/acces` → « Liens d'équipe », cocher le groupe d'essai, créer ; l'ouvrir dans deux navigateurs, sous deux noms | chacun arrive sur ses groupes sous son nom ; dans `/acces`, « 2 / 30 personnes » et deux identités « venue par » le lien ; un nom déjà pris est refusé, même autrement écrit (« luna » pour « Luna ») ; sur un navigateur où quelqu'un est connecté, « Ce n'est pas moi » le déconnecte ; « Révoquer » ferme le lien | -| M1 | Mobile | téléphone (ou fenêtre étroite) | menu en tiroir, les trois gestes visibles sans survol, le panneau s'ouvre en feuille | +| M1 | Mobile | téléphone (ou fenêtre étroite) | le logo dans la barre du haut ; l'accueil montre les groupes ; menu en tiroir (le nombre à côté de « Docs de référence », les quatre pastilles d'ambiance), les trois gestes visibles sans survol, le panneau s'ouvre en feuille | +| C13 | Logo et ambiances | ouvrir `/` (appareil en clair, puis en sombre) ; choisir chaque pastille d'ambiance ; recharger | le logo « Clé de voûte » (signe, *libre*, **Decision**) droit, sans tampon ; le sceau 井 en bas à droite ; première visite : pierre (clair) ou nuit (sombre) ; les quatre pastilles (Pierre, Calcaire, Nuit, Ardoise) changent l'ambiance, qui reste après rechargement ; favicon, icône Apple et manifeste servis | +| C14 | La page d'un groupe | Monnaie libre | le nom ; le pavé des quatre piliers (Docs de référence « ouvert », les trois autres « à venir ») ; « En cours » : l'Acte d'engagement Certification, qui ouvre son lecteur | +| C15 | Le menu | survoler les outils ; ouvrir Docs, puis Mandats ; revenir à l'accueil | « 1 » à côté de « Docs de référence » ; aucun outil surligné sur la page du groupe, seul celui où l'on est ensuite ; au survol, seul Docs réagit ; Mandats s'ouvre quand même (ses « À venir ») ; hors d'un groupe, les outils mènent au dernier groupe ouvert (ou à l'accueil s'il n'y en a pas) | +| C16 | En cours, à venir | Monnaie libre → Docs, Mandats, Protocoles ; les anciennes adresses `/decisions`, `/mandats`, `/protocoles` | Docs : « En cours » (Acte d'engagement Certification) et « À venir » (Acte d'engagement forgerons) ; Mandats : Administration forum Duniter, Administration forum Monnaie libre ; Protocoles : Runtime upgrade — décision on-chain ; les « À venir » sont éteints, sans lien ; ils suivent la langue ; les anciennes adresses mènent aux outils du groupe courant | +| C17 | Décisions | Monnaie libre → Décisions ; voter sur une version d'un article, recharger | une ligne par doc : « Acte d'engagement Certification · 33 modules à voter · dont 33 sans ton vote », puis « dont 32 » ; la ligne ouvre le lecteur | ## Au-delà de la liste @@ -30,7 +35,7 @@ Ce que l'automate ne voit pas et que Chiara regarde : - **la lecture** : le texte est-il beau, aéré, sans bruit ? lit-on un article sans effort ? - **la fatigue** : un geste de trop, une étape inutile, un mot qui arrête ; - **le vocabulaire** : un libellé qui parle du système au lieu de parler de toi ou du groupe ; -- **les deux ambiances** (pierre / nuit) et le clavier seul (Tab, Entrée, Échap) ; +- **les quatre ambiances** (pierre, calcaire, nuit, ardoise) et le clavier seul (Tab, Entrée, Échap) ; - **les langues** : une traduction maladroite, une chaîne restée en français. Chaque remarque passe par « Faire un retour » (menu de gauche) : elle arrive datée, avec la @@ -47,3 +52,4 @@ page, la langue et la taille de l'écran. Les retours se traitent dans trustinWa | 2026-10-21 | automate | local, liens d'équipe et huitième relecture | C1–C12, M1 | 13 / 13 verts | | 2026-10-21 | automate | local, neuvième relecture (noms, places, quantités) | C1–C12, M1 | 13 / 13 verts | | 2026-10-21 | automate | local, mention « sur invitation » (dixième relecture) | C1–C12, M1 | 13 / 13 verts | +| 2026-10-30 | automate | local, lot A (entrée, logo, ambiances, menu, listes, Décisions) | C1–C17, M1 | 18 / 18 verts | diff --git a/web/e2e/entree.spec.ts b/web/e2e/entree.spec.ts new file mode 100644 index 0000000..7e983d0 --- /dev/null +++ b/web/e2e/entree.spec.ts @@ -0,0 +1,161 @@ +import { expect, test, type Page } from '@playwright/test'; +import { DOC, person, signIn } from './helpers'; + +const ML = '/g/monnaie-libre'; +const path = (p: string) => (u: URL) => u.pathname === p; + +async function background(page: Page, selector: string) { + return page.locator(selector).evaluate((el) => getComputedStyle(el).backgroundColor); +} + +test('C13 — logo « Clé de voûte » sans tampon, quatre ambiances, icônes', async ({ browser }) => { + // First visit: the device's light or dark setting picks pierre or nuit. + for (const [scheme, expected] of [['light', 'pierre'], ['dark', 'nuit']] as const) { + const ctx = await browser.newContext({ locale: 'fr-FR', colorScheme: scheme }); + const page = await ctx.newPage(); + await page.goto('/'); + await expect(page.locator('html')).toHaveAttribute('data-mood', expected); + await ctx.close(); + } + const ctx = await browser.newContext({ locale: 'fr-FR', colorScheme: 'light', viewport: { width: 1360, height: 900 } }); + const page = await ctx.newPage(); + await page.goto('/'); + const nav = page.getByRole('navigation', { name: 'Boîte à outils' }); + const brand = nav.locator('a.brand'); + await expect(brand).toHaveAccessibleName('libreDecision'); + await expect(brand.locator('svg')).toBeVisible(); + await expect(page.locator('.stamp')).toHaveCount(0); + expect(await brand.locator('svg').evaluate((el) => getComputedStyle(el).transform)).toBe('none'); + // The seal stays where it is. + await expect(page.locator('svg.seal')).toBeVisible(); + const moods = nav.getByRole('radiogroup', { name: 'Ambiance' }); + for (const [name, mood, cle] of [ + ['Calcaire', 'calcaire', '#a8661e'], + ['Ardoise', 'ardoise', '#e0a65c'], + ['Nuit', 'nuit', '#6ec1c6'], + ['Pierre', 'pierre', '#16808c'], + ]) { + await moods.getByRole('radio', { name }).click(); + await expect(page.locator('html')).toHaveAttribute('data-mood', mood); + await expect(moods.getByRole('radio', { name })).toHaveAttribute('aria-checked', 'true'); + expect(await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--cle').trim())).toBe(cle); + } + await moods.getByRole('radio', { name: 'Ardoise' }).click(); + await page.reload(); + await expect(page.locator('html')).toHaveAttribute('data-mood', 'ardoise'); + // Favicon and icons. + for (const href of ['/favicon.svg', '/apple-touch-icon.png', '/icon-192.png', '/icon-512.png', '/manifest.webmanifest']) { + expect((await page.request.get(href)).status(), href).toBe(200); + } + await expect(page.locator('link[rel="icon"][type="image/svg+xml"]')).toHaveAttribute('href', /favicon\.svg$/); + await expect(page.locator('link[rel="apple-touch-icon"]')).toHaveAttribute('href', /apple-touch-icon\.png$/); + await ctx.close(); +}); + +test('C14 — la page d’un groupe : les quatre piliers, puis « En cours »', async ({ page }) => { + await signIn(page, 'Chiara'); + await page.goto(ML); + await expect(page.getByRole('heading', { level: 1 })).toHaveText('Monnaie libre'); + const pillars = page.locator('.pillars .pillar'); + await expect(pillars).toHaveCount(4); + for (const name of ['Décisions', 'Docs de référence', 'Mandats', 'Protocoles']) { + await expect(pillars.filter({ hasText: name })).toHaveCount(1); + } + await expect(pillars.filter({ hasText: 'Docs de référence' })).toContainText('ouvert'); + await expect(pillars.filter({ hasText: 'à venir' })).toHaveCount(3); + const current = page.locator('section', { has: page.getByRole('heading', { name: 'En cours' }) }); + await current.getByRole('link', { name: /Acte d'engagement Certification/ }).click(); + await expect(page).toHaveURL(path(DOC)); + await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification"); +}); + +test('C15 — le menu : le nombre de docs, surligné seulement dedans, seul Docs réagit au survol', async ({ browser }) => { + const page = await person(browser, 'Lina'); + // Nothing remembered yet: the tools lead home. + await page.goto('/'); + const nav = page.getByRole('navigation', { name: 'Boîte à outils' }); + const tool = (name: string) => nav.locator('.tool', { hasText: name }); + await expect(tool('Docs de référence')).toHaveAttribute('href', '/'); + await page.goto(ML); + await expect(tool('Docs de référence').locator('.count')).toHaveText('1'); + await expect(nav.locator('.tool[aria-current="page"]')).toHaveCount(0); + await expect(nav.locator('.tool.off')).toHaveCount(3); + // Hover: Docs reacts, a tool to come does not. + const docsBefore = await background(page, 'nav .tool.docs'); + await tool('Docs de référence').hover(); + await expect.poll(() => background(page, 'nav .tool.docs')).not.toBe(docsBefore); + const mandatesBefore = await background(page, 'nav .tool.mandats'); + await tool('Mandats').hover(); + await page.waitForTimeout(400); + expect(await background(page, 'nav .tool.mandats')).toBe(mandatesBefore); + // Inside a tool, it alone is highlighted. + await tool('Docs de référence').click(); + await expect(page).toHaveURL(path(`${ML}/docs`)); + await expect(nav.locator('.tool[aria-current="page"]')).toHaveText(/Docs de référence/); + await page.goto(DOC); + await expect(nav.locator('.tool[aria-current="page"]')).toHaveText(/Docs de référence/); + // A tool to come opens its page, which shows what is coming, dimmed. + await tool('Mandats').click(); + await expect(page).toHaveURL(path(`${ML}/mandats`)); + await expect(nav.locator('.tool[aria-current="page"]')).toHaveText(/Mandats/); + // Outside a group, the tools aim at the last group opened. + await page.goto('/'); + await expect(tool('Docs de référence')).toHaveAttribute('href', `${ML}/docs`); + await expect(tool('Protocoles')).toHaveAttribute('href', `${ML}/protocoles`); + await expect(nav.locator('.tool[aria-current="page"]')).toHaveCount(0); +}); + +test('C16 — Docs, Mandats, Protocoles : « En cours » et « À venir », éteints', async ({ browser }) => { + const page = await person(browser, 'Noé'); + // Old addresses, before any group: home. + await page.goto('/mandats'); + await expect(page).toHaveURL(path('/')); + await page.goto(`${ML}/docs`); + const section = (name: string) => page.locator('section', { has: page.getByRole('heading', { name, exact: true }) }); + await expect(section('En cours').getByRole('link', { name: /Acte d'engagement Certification/ })).toBeVisible(); + const soon = section('À venir'); + await expect(soon.locator('li')).toHaveText(["Acte d'engagement forgerons"]); + await expect(soon.getByRole('link')).toHaveCount(0); + await page.goto(`${ML}/mandats`); + await expect(page.getByRole('heading', { level: 1 })).toHaveText('Mandats'); + await expect(section('À venir').locator('li')).toHaveText(['Administration forum Duniter', 'Administration forum Monnaie libre']); + await expect(section('À venir').getByRole('link')).toHaveCount(0); + await page.goto(`${ML}/protocoles`); + await expect(section('À venir').locator('li')).toHaveText(['Runtime upgrade — décision on-chain']); + // Old addresses lead to the tools of the current group. + for (const tool of ['decisions', 'mandats', 'protocoles']) { + await page.goto(`/${tool}`); + await expect(page).toHaveURL(path(`${ML}/${tool}`)); + } + // In English and Spanish, what is coming follows. + await page.getByRole('navigation', { name: 'Boîte à outils' }).getByRole('button', { name: 'ES' }).click(); + await page.goto(`${ML}/protocoles`); + await expect(page.locator('section.soon li')).toHaveText(['Runtime upgrade — decisión on-chain']); + await page.getByRole('navigation', { name: 'Caja de herramientas' }).getByRole('button', { name: 'FR' }).click(); +}); + +test('C17 — Décisions : une ligne par doc, les modules à voter, dont sans ton vote', async ({ browser }) => { + const page = await person(browser, 'Maëlle'); + await page.goto(`${ML}/decisions`); + await expect(page.getByRole('heading', { level: 1 })).toHaveText('Décisions'); + const rows = page.locator('ul.lines > li'); + await expect(rows).toHaveCount(1); + await expect(rows.first()).toContainText("Acte d'engagement Certification"); + await expect(rows.first()).toContainText('33 modules à voter'); + await expect(rows.first()).toContainText('dont 33 sans ton vote'); + // A vote on one version of E3 (it has a variant): one module fewer without her vote. « Contre » + // counts as much, and leaves the previews of C9 unchanged (the server is shared). + const group = await (await page.request.get('/api/groups/monnaie-libre')).json(); + const doc = await (await page.request.get(`/api/docs/${group.docs[0].id}`)).json(); + const e3 = doc.units.find((u: { code: string }) => u.code === 'E3'); + const r = await page.request.post(`/api/proposals/${e3.proposals[1].id}/vote`, { + data: { choice: 'against' }, + headers: { 'x-requested-with': 'libredecision' }, + }); + expect(r.ok()).toBe(true); + await page.reload(); + await expect(rows.first()).toContainText('33 modules à voter'); + await expect(rows.first()).toContainText('dont 32 sans ton vote'); + await rows.first().getByRole('link').click(); + await expect(page).toHaveURL(path(DOC)); +}); diff --git a/web/e2e/mobile.spec.ts b/web/e2e/mobile.spec.ts index 4400d0e..eaea48f 100644 --- a/web/e2e/mobile.spec.ts +++ b/web/e2e/mobile.spec.ts @@ -1,12 +1,19 @@ import { expect, test } from '@playwright/test'; import { DOC, signIn } from './helpers'; -test('M1 — mobile : menu en tiroir, actions visibles, panneau en feuille', async ({ page }) => { +test('M1 — mobile : logo dans la barre, menu en tiroir, actions visibles, panneau en feuille', async ({ page }) => { await signIn(page, 'Chiara'); + await page.goto('/'); + const brand = page.locator('header.topbar a.brand'); + await expect(brand).toHaveAccessibleName('libreDecision'); + await expect(brand.locator('svg')).toBeVisible(); + await expect(page.locator('a.root')).toContainText('Monnaie libre'); await page.goto(DOC); await page.getByRole('button', { name: 'Menu' }).click(); const drawer = page.getByRole('dialog', { name: 'Boîte à outils' }); await expect(drawer.locator('.tool', { hasText: 'Protocoles' })).toBeVisible(); + await expect(drawer.locator('.tool', { hasText: 'Docs de référence' }).locator('.count')).toHaveText('1'); + await expect(drawer.getByRole('radiogroup', { name: 'Ambiance' }).getByRole('radio')).toHaveCount(4); await drawer.getByRole('button', { name: 'Fermer' }).first().click(); const first = page.locator('section.text article.unit').first(); await first.getByRole('button', { name: 'Le vote' }).click(); diff --git a/web/e2e/reader.spec.ts b/web/e2e/reader.spec.ts index 1acd089..377e50c 100644 --- a/web/e2e/reader.spec.ts +++ b/web/e2e/reader.spec.ts @@ -1,25 +1,37 @@ import { expect, test } from '@playwright/test'; import { DOC, person, signIn, TEST_DOC } from './helpers'; -test('C1 — l’accueil : l’intro, les quatre sections, le menu complet', async ({ page }) => { +test('C1 — l’accueil sans session : le titre, le leitmotiv en trois lignes, la connexion, le menu complet', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('heading', { level: 1 })).toHaveText('Rendre possible la décision collective'); - await expect(page.getByText('en fonction du contexte, processus et modalités, boîte à outils')).toBeVisible(); + await expect(page.locator('.leitmotiv li')).toHaveText(['contexte et cadrage', 'processus et modalités', 'boîte à outils']); const nav = page.getByRole('navigation', { name: 'Boîte à outils' }); for (const item of ['Décisions', 'Docs de référence', 'Mandats', 'Protocoles']) { await expect(nav.locator('.tool', { hasText: item })).toBeVisible(); } - await expect(nav.locator('.tool.disabled')).toHaveCount(3); + await expect(nav.locator('.tool.off')).toHaveCount(3); await expect(page.getByRole('link', { name: 'Se connecter avec sa clé Ğ1' })).toBeVisible(); + // The four pillars live on the page of a group, no longer here. + await expect(page.locator('.pillars')).toHaveCount(0); + await expect(page.getByText('Monnaie libre', { exact: true })).toHaveCount(0); }); -test('C2 — les groupes de la même clé ; pas l’Atelier du canal', async ({ page }) => { +test('C2 — l’accueil avec session : le choix du groupe, ceux de la même clé ; pas l’Atelier du canal', async ({ page }) => { await signIn(page, 'Chiara'); - await page.goto('/groupes'); + await page.goto('/'); + await expect(page.getByRole('heading', { level: 1 })).toHaveText('Rendre possible la décision collective'); + await expect(page.locator('a.root')).toContainText('Monnaie libre'); for (const g of ['Monnaie libre', 'Axiom', 'Librodrome', 'Groupe local Vallée Drôme Crestois']) { await expect(page.getByText(g, { exact: true })).toBeVisible(); } await expect(page.getByText('Atelier du canal')).toHaveCount(0); + await expect(page.getByRole('link', { name: 'Se connecter avec sa clé Ğ1' })).toHaveCount(0); + // The old address and « Mes groupes » lead here. + await page.goto('/groupes'); + await expect(page).toHaveURL((u) => u.pathname === '/'); + await page.goto('/g/monnaie-libre'); + await page.getByRole('navigation', { name: 'Boîte à outils' }).getByRole('link', { name: 'Mes groupes' }).click(); + await expect(page).toHaveURL((u) => u.pathname === '/'); }); test('C3 — le texte en vigueur : préambule et deux engagements, seed a minima', async ({ page }) => { @@ -144,7 +156,7 @@ test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de const ctx = await browser.newContext({ locale: 'fr-FR' }); const invited = await ctx.newPage(); await invited.goto(`/invitation#${link.split('#')[1]}`); - await expect(invited).toHaveURL(/\/groupes$/); + await expect(invited).toHaveURL((u) => u.pathname === '/'); await invited.goto(DOC); await invited.locator('ul.deep button.row', { hasText: 'Connaissance suffisante' }).first().click(); const panel = invited.locator('aside.panel'); @@ -172,7 +184,7 @@ test('C12 — lien d’équipe : chaque personne qui l’ouvre a sa propre ident await expect(page.getByText('« Équipe de test » t’ouvre', { exact: false })).toBeVisible(); await page.getByLabel('Ton nom').fill(name); await page.getByRole('button', { name: 'Rejoindre' }).click(); - await expect(page).toHaveURL(/\/groupes$/); + await expect(page).toHaveURL((u) => u.pathname === '/'); await ctx.close(); } // A shared browser: the person signed in steps aside, the next one gives a name. diff --git a/web/src/app.css b/web/src/app.css index 429b187..2f1ae94 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,5 +1,8 @@ /* libreDecision v3 — tokens and base. - Two moods: « pierre » (cool stone, light) and « nuit » (well at night, dark). + Four moods, two light and two dark: « pierre » (cool stone) and « nuit » (well at night) + carry the water; « calcaire » (warm limestone) and « ardoise » (dark slate) carry the ochre. + `--water` is the accent of the mood (links, buttons, current place), whatever its hue; + `--cle` is the keystone of the logo (Clé de voûte, DA 2026-10-28). Borderless: depth comes from background and shadow, never from strokes. Radii: cards 16px · inputs 12px · pills and buttons 20px · icon holders 14px. */ @@ -23,12 +26,38 @@ --ochre: #a8661e; --ochre-wash: rgb(168 102 30 / 0.12); --on-water: #f6f8f7; + --cle: #16808c; --shadow: 0 1px 2px rgb(20 40 40 / 0.06), 0 8px 28px rgb(20 40 40 / 0.07); --shadow-strong: 0 2px 6px rgb(20 40 40 / 0.1), 0 18px 48px rgb(20 40 40 / 0.16); --veil: rgb(16 30 31 / 0.28); color-scheme: light; } +/* Calcaire: warm limestone, the ochre. The accent is darker than the keystone + (#a8661e, 3.5:1) so that a link or a label reads at 4.5:1 or more; warnings + turn to brick, away from the accent. */ +[data-mood='calcaire'] { + --bg: #f2ede4; + --surface: #fbf8f2; + --surface-2: #e8e0d2; + --ink: #2a241e; + --ink-soft: #5c5247; + --ink-faint: #857868; + --water: #8a5214; + --water-light: #e6c79c; + --water-wash: rgb(138 82 20 / 0.08); + --moss: #446b38; + --moss-wash: rgb(68 107 56 / 0.12); + --ochre: #a3432b; + --ochre-wash: rgb(163 67 43 / 0.1); + --on-water: #fbf8f2; + --cle: #a8661e; + --shadow: 0 1px 2px rgb(60 42 20 / 0.06), 0 8px 28px rgb(60 42 20 / 0.07); + --shadow-strong: 0 2px 6px rgb(60 42 20 / 0.1), 0 18px 48px rgb(60 42 20 / 0.16); + --veil: rgb(42 32 20 / 0.28); + color-scheme: light; +} + [data-mood='nuit'] { --bg: #0e1819; --surface: #142224; @@ -44,12 +73,36 @@ --ochre: #e0a458; --ochre-wash: rgb(224 164 88 / 0.12); --on-water: #0e1819; + --cle: #6ec1c6; --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 8px 28px rgb(0 0 0 / 0.28); --shadow-strong: 0 2px 6px rgb(0 0 0 / 0.4), 0 18px 48px rgb(0 0 0 / 0.5); --veil: rgb(0 0 0 / 0.5); color-scheme: dark; } +/* Ardoise: dark slate, the ochre; warnings in a light brick. */ +[data-mood='ardoise'] { + --bg: #171b20; + --surface: #1d2228; + --surface-2: #262c34; + --ink: #e4e6e2; + --ink-soft: #a9afb5; + --ink-faint: #7a828b; + --water: #e0a65c; + --water-light: #6b4c26; + --water-wash: rgb(224 166 92 / 0.09); + --moss: #93c08a; + --moss-wash: rgb(147 192 138 / 0.12); + --ochre: #e98d72; + --ochre-wash: rgb(233 141 114 / 0.12); + --on-water: #171b20; + --cle: #e0a65c; + --shadow: 0 1px 2px rgb(0 0 0 / 0.32), 0 8px 28px rgb(0 0 0 / 0.3); + --shadow-strong: 0 2px 6px rgb(0 0 0 / 0.42), 0 18px 48px rgb(0 0 0 / 0.52); + --veil: rgb(0 0 0 / 0.5); + color-scheme: dark; +} + :root { --font-ui: 'Nunito Variable', system-ui, -apple-system, 'Segoe UI', sans-serif; --font-text: 'Literata Variable', 'Iowan Old Style', Georgia, serif; @@ -166,7 +219,7 @@ button { .btn.primary { background: var(--water); color: var(--on-water); - box-shadow: 0 6px 18px rgb(31 111 120 / 0.22); + box-shadow: 0 6px 18px color-mix(in srgb, var(--water) 22%, transparent); } .btn.quiet { background: transparent; diff --git a/web/src/app.html b/web/src/app.html index 011dfbb..2d0dc2d 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -4,15 +4,27 @@ + + + + + + libreDecision %sveltekit.head% diff --git a/web/src/lib/assets/cle-de-voute.svg b/web/src/lib/assets/cle-de-voute.svg new file mode 100644 index 0000000..bd98e22 --- /dev/null +++ b/web/src/lib/assets/cle-de-voute.svg @@ -0,0 +1 @@ + diff --git a/web/src/lib/components/DocCards.svelte b/web/src/lib/components/DocCards.svelte new file mode 100644 index 0000000..45c16cf --- /dev/null +++ b/web/src/lib/components/DocCards.svelte @@ -0,0 +1,59 @@ + + +{#if docs.length} + +{:else} +

{t('group.current.none')}

+{/if} + + diff --git a/web/src/lib/components/GroupChoice.svelte b/web/src/lib/components/GroupChoice.svelte new file mode 100644 index 0000000..d456555 --- /dev/null +++ b/web/src/lib/components/GroupChoice.svelte @@ -0,0 +1,144 @@ + + +
+

{t('groups.title')}

+ + {#if data.error} + data.load()} /> + {:else if data.value} + {#if root} + + Ğ1 + {root.name} + {#if root.open}{/if} + {#if !root.open}{t('groups.locked')}{/if} + + {/if} + + {#if sameKey.length} +

{t('groups.same.key')}

+
    + {#each sameKey as g (g.slug)} +
  • + {#if g.open} + {g.name} + {:else} +
    + {g.name} + {t('groups.locked')} +
    + {/if} +
  • + {/each} +
+ {#if sameKey.some((g) => !g.open)}

{t('groups.locked.long')}

{/if} + {/if} + + {#if others.length} +

{t('groups.test')}

+ + {/if} + + {#if !all.length}

{t('groups.empty')}

{/if} + {/if} +
+ + diff --git a/web/src/lib/components/Logo.svelte b/web/src/lib/components/Logo.svelte new file mode 100644 index 0000000..f277a40 --- /dev/null +++ b/web/src/lib/components/Logo.svelte @@ -0,0 +1,54 @@ + + + + + diff --git a/web/src/lib/components/Pillars.svelte b/web/src/lib/components/Pillars.svelte new file mode 100644 index 0000000..807ff48 --- /dev/null +++ b/web/src/lib/components/Pillars.svelte @@ -0,0 +1,89 @@ + + + + + diff --git a/web/src/lib/components/Sidebar.svelte b/web/src/lib/components/Sidebar.svelte index 7099f5b..3d1bc91 100644 --- a/web/src/lib/components/Sidebar.svelte +++ b/web/src/lib/components/Sidebar.svelte @@ -1,61 +1,70 @@