forked from yvv/decision
- 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>
94 lines
2.7 KiB
TypeScript
94 lines
2.7 KiB
TypeScript
// 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();
|