forked from yvv/decision
Une version retenue par une objection le dit sous l'article, à chacun selon son rôle : la personne qui vote voit qu'elle ne se vote plus (les votes faits restent) ; l'auteur, quoi faire (« Modifier l'article visé » pour une redite, « Reformuler » pour une contradiction) ; la personne qui objecte, lever ou réitérer avant l'échéance. Le panier la montre sans choix et ne l'envoie jamais. « Détails » : faire une objection (redit / contredit), les objections et où elles en sont, les coquilles proposées par d'autres avec leur diff resserré, valider / refuser / revenir, l'historique dit qui a validé. Une coquille se propose sur toute version ouverte ou en vigueur. Boîte « Messages » dans le menu, avec le nombre de non-lus (pastille sur le bouton du menu en mobile) : objet, texte, gestes, échéance ; un message traité n'a plus de geste. Répondre à une objection lit son message. Trilingue (fr, en, es). C6 suit la règle : la coquille d'un administrateur sur un texte du seed passe tout de suite ; C28–C31 couvrent le reste. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
191 lines
4.7 KiB
Svelte
191 lines
4.7 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;
|
|
if (session.ready) void session.refreshUnread();
|
|
});
|
|
</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')}{#if session.unread} · {i18n.plural('nav.messages.count', session.unread)}{/if}</span>
|
|
{#if session.unread}<span class="badge" aria-hidden="true">{session.unread}</span>{/if}
|
|
</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 {
|
|
position: relative;
|
|
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);
|
|
}
|
|
/* Unread messages, seen without opening the menu. */
|
|
.badge {
|
|
position: absolute;
|
|
top: -0.3rem;
|
|
right: -0.3rem;
|
|
min-width: 1.25rem;
|
|
height: 1.25rem;
|
|
padding: 0 0.3rem;
|
|
border-radius: var(--r-pill);
|
|
background: var(--ochre);
|
|
color: var(--on-water);
|
|
font-size: 0.72rem;
|
|
font-weight: 800;
|
|
line-height: 1.25rem;
|
|
text-align: center;
|
|
}
|
|
}
|
|
</style>
|