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
+112 -79
View File
@@ -1,61 +1,70 @@
<script lang="ts">
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { i18n, LANGS, t } from '$lib/i18n/index.svelte';
import { mood } from '$lib/mood.svelte';
import { TOOLS, type Tool } from '$lib/group';
import { groups, toolHref } from '$lib/groups.svelte';
import { i18n, LANGS, t, type Key } from '$lib/i18n/index.svelte';
import { MOODS, mood } from '$lib/mood.svelte';
import { session } from '$lib/session.svelte';
import Icon from './Icon.svelte';
import Stamp from './Stamp.svelte';
import Logo from './Logo.svelte';
let { onfeedback, onnavigate }: { onfeedback: () => void; onnavigate?: () => void } = $props();
const group = $derived(page.params.group ?? null);
const docsHref = $derived(
group ? `/g/${group}/docs` : session.me ? '/groupes' : '/connexion?return=/groupes',
);
const LABELS: Record<Tool, Key> = {
decisions: 'nav.decisions',
docs: 'nav.docs',
mandats: 'nav.mandates',
protocoles: 'nav.protocols',
};
const path = $derived(page.url.pathname);
const tools = $derived([
{ key: 'decisions', label: t('nav.decisions'), href: null },
{ key: 'docs', label: t('nav.docs'), href: docsHref },
{ key: 'mandats', label: t('nav.mandates'), href: null },
{ key: 'protocoles', label: t('nav.protocols'), href: null },
]);
/** The group of the page, if any. */
const here = $derived(page.params.group ?? null);
/** Whose tools the menu shows: the group of the page, or else the last one opened. */
const target = $derived(here ?? (session.me ? groups.last : null));
$effect(() => {
if (session.me && target) groups.ensure(target);
});
const view = $derived(target ? groups.views[target] : undefined);
/** Documents in progress in that group, next to « Docs de référence ». */
const count = $derived(view?.group.open ? view.docs.length : null);
/** A tool is highlighted only when one is in it. */
const inTool = (tool: Tool) => here !== null && (path === `/g/${here}/${tool}` || path.startsWith(`/g/${here}/${tool}/`));
async function signOut() {
await session.logout();
groups.reset();
onnavigate?.();
await goto('/');
}
</script>
<nav class="sidebar" aria-label={t('nav.label')}>
<a class="brand" href="/" onclick={() => onnavigate?.()}>
<Stamp />
<span class="word">libre<b>Decision</b></span>
</a>
<a class="brand" href="/" onclick={() => onnavigate?.()}><Logo /></a>
{#if session.me}
<a class="groups" href="/groupes" class:current={path === '/groupes'} onclick={() => onnavigate?.()}>
<a class="groups" href="/" class:current={path === '/'} aria-current={path === '/' ? 'page' : undefined} onclick={() => onnavigate?.()}>
<span class="eyebrow">{t('nav.groups')}</span>
</a>
{/if}
<ul class="tools">
{#each tools as tool (tool.key)}
{#each TOOLS as tool (tool)}
{@const on = inTool(tool)}
<li>
{#if tool.href}
<a
href={tool.href}
class="tool"
class:current={path.includes('/docs')}
aria-current={path.includes('/docs') ? 'page' : undefined}
onclick={() => onnavigate?.()}>{tool.label}</a
>
{:else}
<span class="tool disabled" aria-disabled="true" title={t('group.tool.soon')}>
{tool.label}<span class="soon">{t('nav.soon')}</span>
</span>
{/if}
<a
href={toolHref(tool, target)}
class="tool {tool}"
class:off={tool !== 'docs'}
class:current={on}
aria-current={on ? 'page' : undefined}
onclick={() => onnavigate?.()}
>
<span>{t(LABELS[tool])}</span>
{#if tool === 'docs' && count !== null}
<span class="count" aria-hidden="true">{count}</span><span class="visually-hidden">{i18n.plural('nav.docs.count', count)}</span>
{/if}
</a>
</li>
{/each}
</ul>
@@ -76,15 +85,21 @@
<button type="button" class:on={i18n.lang === l} aria-pressed={i18n.lang === l} onclick={() => i18n.set(l)}>{l.toUpperCase()}</button>
{/each}
</div>
<button
class="icon-btn"
type="button"
onclick={() => mood.toggle()}
aria-label={mood.current === 'nuit' ? t('nav.mood.toStone') : t('nav.mood.toNight')}
title={mood.current === 'nuit' ? t('nav.mood.toStone') : t('nav.mood.toNight')}
>
<Icon name={mood.current === 'nuit' ? 'sun' : 'moon'} size={17} />
</button>
<div class="moods" role="radiogroup" aria-label={t('nav.mood')}>
{#each MOODS as m (m)}
<!-- Each swatch carries its own mood: its colours are that mood's tokens. -->
<button
type="button"
role="radio"
class="swatch"
data-mood={m}
aria-checked={mood.current === m}
aria-label={t(`mood.${m}`)}
title={t(`mood.${m}`)}
onclick={() => mood.set(m)}
></button>
{/each}
</div>
</div>
{#if session.me}
@@ -109,17 +124,8 @@
.brand {
display: flex;
align-items: center;
gap: 0.7rem;
color: var(--ink);
text-decoration: none;
padding: 0.2rem 0.4rem;
}
.word {
font-size: 1.08rem;
letter-spacing: 0.01em;
}
.word b {
font-weight: 800;
text-decoration: none;
}
.groups {
padding: 0 0.6rem;
@@ -135,35 +141,47 @@
display: grid;
gap: 0.15rem;
}
/* The four tools look alike; only « Docs de référence » is lit and answers the pointer.
The three others stay dim, without any hover effect, and still lead to their page. */
.tool {
display: flex;
align-items: baseline;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.55rem 0.7rem;
min-height: 2.5rem;
padding: 0.5rem 0.7rem;
border-radius: var(--r-input);
color: var(--ink);
font-weight: 700;
text-decoration: none;
transition: background-color 0.15s ease;
transition: background-color 0.15s ease, color 0.15s ease;
}
a.tool:hover {
.tool.off {
color: var(--ink-faint);
}
.tool.docs:hover,
.tool.current {
background: var(--water-wash);
}
a.tool.current {
background: var(--water-wash);
.tool.docs.current {
color: var(--water);
}
.tool.disabled {
color: var(--ink-faint);
font-weight: 600;
cursor: default;
.tool.off.current {
color: var(--ink-soft);
}
.soon {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--ink-faint);
.count {
min-width: 1.6rem;
padding: 0.05rem 0.45rem;
border-radius: var(--r-pill);
background: var(--surface-2);
color: var(--ink-soft);
font-size: 0.78rem;
font-weight: 800;
text-align: center;
}
.tool.current .count {
background: var(--surface);
color: var(--water);
}
.foot {
margin-top: auto;
@@ -197,6 +215,7 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.4rem;
padding: 0.2rem 0.3rem;
}
.langs {
@@ -209,7 +228,7 @@
border: 0;
background: transparent;
border-radius: var(--r-pill);
min-width: 2.25rem;
min-width: 2.05rem;
min-height: 2rem;
font-size: 0.78rem;
font-weight: 800;
@@ -220,19 +239,33 @@
color: var(--water);
box-shadow: var(--shadow);
}
.icon-btn {
display: grid;
place-items: center;
width: 2.25rem;
height: 2.25rem;
border: 0;
border-radius: var(--r-icon);
background: transparent;
color: var(--ink-soft);
.moods {
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.icon-btn:hover {
background: var(--water-wash);
color: var(--ink);
.swatch {
position: relative;
width: 1.3rem;
height: 1.3rem;
padding: 0;
border: 0;
border-radius: 50%;
/* var(--bg), var(--ink) and var(--cle) are those of the swatch's own mood. */
background: var(--bg);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 32%, transparent);
}
.swatch::after {
content: '';
position: absolute;
inset: 32%;
border-radius: 50%;
background: var(--cle);
}
.swatch[aria-checked='true'] {
/* currentColor is the ink of the page, inherited. */
outline: 2px solid currentColor;
outline-offset: 2px;
}
.who {
display: flex;