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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user