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>
172 lines
4.1 KiB
Svelte
172 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import '../app.css';
|
|
import { onMount, type Snippet } from 'svelte';
|
|
import { afterNavigate } from '$app/navigation';
|
|
import FeedbackDialog from '$lib/components/FeedbackDialog.svelte';
|
|
import Icon from '$lib/components/Icon.svelte';
|
|
import Seal from '$lib/components/Seal.svelte';
|
|
import Logo from '$lib/components/Logo.svelte';
|
|
import Sidebar from '$lib/components/Sidebar.svelte';
|
|
import Toasts from '$lib/components/Toasts.svelte';
|
|
import { groups } from '$lib/groups.svelte';
|
|
import { i18n, t } from '$lib/i18n/index.svelte';
|
|
import { mood } from '$lib/mood.svelte';
|
|
import { session } from '$lib/session.svelte';
|
|
|
|
let { children }: { children: Snippet } = $props();
|
|
let feedback: FeedbackDialog | undefined = $state();
|
|
let drawer = $state(false);
|
|
|
|
onMount(() => {
|
|
i18n.init();
|
|
mood.init();
|
|
groups.init();
|
|
void session.load();
|
|
});
|
|
|
|
afterNavigate(() => {
|
|
drawer = false;
|
|
});
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{t('app.name')}</title>
|
|
</svelte:head>
|
|
|
|
<a class="skip" href="#main">{t('app.skip')}</a>
|
|
|
|
<div class="shell">
|
|
<aside class="side">
|
|
<Sidebar onfeedback={() => feedback?.open()} />
|
|
</aside>
|
|
|
|
<header class="topbar">
|
|
<a class="brand" href="/"><Logo size={1.2} /></a>
|
|
<button class="menu" type="button" aria-expanded={drawer} aria-controls="drawer" onclick={() => (drawer = true)}>
|
|
<Icon name="menu" /><span class="visually-hidden">{t('nav.menu')}</span>
|
|
</button>
|
|
</header>
|
|
|
|
{#if drawer}
|
|
<div class="veil" role="presentation" onclick={() => (drawer = false)}></div>
|
|
<div class="drawer card" id="drawer" role="dialog" aria-modal="true" aria-label={t('nav.label')}>
|
|
<button class="close" type="button" onclick={() => (drawer = false)} aria-label={t('nav.close')}><Icon name="close" /></button>
|
|
<Sidebar onfeedback={() => { drawer = false; feedback?.open(); }} onnavigate={() => (drawer = false)} />
|
|
</div>
|
|
{/if}
|
|
|
|
<main id="main" tabindex="-1">
|
|
{#if session.ready}
|
|
{@render children()}
|
|
{/if}
|
|
</main>
|
|
</div>
|
|
|
|
<FeedbackDialog bind:this={feedback} />
|
|
<Toasts />
|
|
<Seal />
|
|
|
|
<svelte:window onkeydown={(e) => e.key === 'Escape' && (drawer = false)} />
|
|
|
|
<style>
|
|
.skip {
|
|
position: absolute;
|
|
left: -999px;
|
|
top: 0.5rem;
|
|
z-index: 100;
|
|
background: var(--surface);
|
|
padding: 0.5rem 1rem;
|
|
border-radius: var(--r-pill);
|
|
}
|
|
.skip:focus {
|
|
left: 0.5rem;
|
|
}
|
|
.shell {
|
|
min-height: 100dvh;
|
|
}
|
|
.side {
|
|
position: fixed;
|
|
inset: 0 auto 0 0;
|
|
width: var(--sidebar);
|
|
background: var(--surface);
|
|
box-shadow: var(--shadow);
|
|
z-index: 20;
|
|
overflow-y: auto;
|
|
}
|
|
main {
|
|
margin-left: var(--sidebar);
|
|
min-height: 100dvh;
|
|
outline: none;
|
|
}
|
|
.topbar {
|
|
display: none;
|
|
}
|
|
.veil {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--veil);
|
|
z-index: 40;
|
|
}
|
|
.drawer {
|
|
position: fixed;
|
|
inset: 0.6rem auto 0.6rem 0.6rem;
|
|
width: min(84vw, 18rem);
|
|
z-index: 41;
|
|
overflow-y: auto;
|
|
animation: slide 0.22s var(--ease);
|
|
}
|
|
.close {
|
|
position: absolute;
|
|
top: 1.2rem;
|
|
right: 0.8rem;
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--ink-soft);
|
|
width: 2.25rem;
|
|
height: 2.25rem;
|
|
border-radius: var(--r-icon);
|
|
}
|
|
@keyframes slide {
|
|
from {
|
|
transform: translateX(-12px);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
@media (max-width: 899px) {
|
|
.side {
|
|
display: none;
|
|
}
|
|
main {
|
|
margin-left: 0;
|
|
}
|
|
.topbar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 30;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.6rem 0.9rem;
|
|
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.15rem 0.1rem;
|
|
text-decoration: none;
|
|
}
|
|
.menu {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
border: 0;
|
|
border-radius: var(--r-icon);
|
|
background: var(--surface);
|
|
box-shadow: var(--shadow);
|
|
color: var(--ink);
|
|
}
|
|
}
|
|
</style>
|