forked from yvv/decision
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:
@@ -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();
|
||||
Reference in New Issue
Block a user