v3 : l'entrée — accueil avec le choix du groupe, logo « Clé de voûte », quatre ambiances, menu et pages d'outil par groupe

- 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 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-30 11:21:57 +01:00
co-authored by Claude Opus 5.5
parent c640413a70
commit 81b604e5f2
48 changed files with 1323 additions and 613 deletions
+93
View File
@@ -0,0 +1,93 @@
// The groups as the menu and the pages share them: the last group opened on this
// device (outside a group, the tools of the menu aim at it), and the views
// already read (the menu counts the documents of the current group).
import { api, ApiError } from './api';
import type { GroupView, Tool } from './group';
const KEY = 'ld:group';
/** The last group opened, read straight from storage (usable before any mount). */
export function lastGroup(): string | null {
try {
return localStorage.getItem(KEY);
} catch {
return null;
}
}
/** Where a tool leads: its page in the group, or home when no group is known. */
export function toolHref(tool: Tool, group: string | null): string {
return group ? `/g/${group}/${tool}` : '/';
}
class Groups {
last = $state<string | null>(null);
views = $state<Record<string, GroupView>>({});
private asked = new Set<string>();
private running = new Map<string, Promise<GroupView>>();
init() {
this.last = lastGroup();
}
/** Read a group (a read already under way is shared); one that opens becomes the last group. */
load(slug: string): Promise<GroupView> {
const running = this.running.get(slug);
if (running) return running;
this.asked.add(slug);
const p: Promise<GroupView> = this.fetch(slug).finally(() => {
if (this.running.get(slug) === p) this.running.delete(slug);
});
this.running.set(slug, p);
return p;
}
private async fetch(slug: string): Promise<GroupView> {
try {
const v = await api.get<GroupView>(`/groups/${slug}`);
this.views[slug] = v;
if (v.group.open) this.remember(slug);
else if (this.last === slug) this.forget();
return v;
} catch (e) {
// A group that no longer shows or opens is no longer the last group; a network hiccup keeps it.
if (this.last === slug && e instanceof ApiError && (e.code === 'not_found' || e.code === 'forbidden')) this.forget();
throw e;
}
}
/** Read a group once, for the menu; errors stay silent there. */
ensure(slug: string) {
if (this.views[slug] || this.asked.has(slug)) return;
this.load(slug).catch(() => {});
}
remember(slug: string) {
this.last = slug;
try {
localStorage.setItem(KEY, slug);
} catch {
/* no storage: the menu simply leads home */
}
}
/** On sign-out, or when the last group no longer opens. */
forget() {
this.last = null;
try {
localStorage.removeItem(KEY);
} catch {
/* no storage */
}
}
/** On sign-out: nothing read under the previous identity is kept. */
reset() {
this.forget();
this.views = {};
this.asked.clear();
this.running.clear();
}
}
export const groups = new Groups();