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:
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
// The documents in progress of a group, each a way into its reader.
|
||||
import type { GroupView } from '$lib/group';
|
||||
import { i18n, t } from '$lib/i18n/index.svelte';
|
||||
import type { LangMap } from '$lib/types';
|
||||
|
||||
let { group, docs, summary = true }: { group: string; docs: GroupView['docs']; summary?: boolean } = $props();
|
||||
const pick = (m: LangMap) => m[i18n.lang] ?? m.fr ?? '';
|
||||
</script>
|
||||
|
||||
{#if docs.length}
|
||||
<ul class="docs">
|
||||
{#each docs as d (d.id)}
|
||||
<li>
|
||||
<a class="doc card" href="/g/{group}/docs/{d.slug}">
|
||||
<span class="title">{pick(d.title)}</span>
|
||||
{#if summary}<span class="summary">{pick(d.summary)}</span>{/if}
|
||||
<span class="meta faint">{i18n.plural('docs.inForce', d.inForce)} · {i18n.plural('docs.open', d.open)}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="muted">{t('group.current.none')}</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.docs {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
.doc {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
padding: 1.35rem 1.5rem;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
.doc:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.title {
|
||||
font-family: var(--font-text);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.summary {
|
||||
color: var(--ink-soft);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.meta {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<script lang="ts">
|
||||
// The choice of a group: Monnaie libre first and large, then the groups that use the same
|
||||
// key, then the test groups; a group one does not open shows its lock.
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import Guard from '$lib/components/Guard.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { t } from '$lib/i18n/index.svelte';
|
||||
import { Remote } from '$lib/load.svelte';
|
||||
import type { GroupSummary } from '$lib/types';
|
||||
|
||||
const data = new Remote(() => api.get<{ groups: GroupSummary[] }>('/groups'));
|
||||
onMount(() => void data.load());
|
||||
|
||||
const all = $derived(data.value?.groups ?? []);
|
||||
const root = $derived(all.find((g) => g.slug === 'monnaie-libre'));
|
||||
const sameKey = $derived(all.filter((g) => g.parent === 'monnaie-libre'));
|
||||
const others = $derived(all.filter((g) => g.slug !== 'monnaie-libre' && g.parent !== 'monnaie-libre'));
|
||||
</script>
|
||||
|
||||
<section class="choice" aria-labelledby="groups-title">
|
||||
<h2 class="eyebrow" id="groups-title">{t('groups.title')}</h2>
|
||||
|
||||
{#if data.error}
|
||||
<Guard code={data.error} onretry={() => data.load()} />
|
||||
{:else if data.value}
|
||||
{#if root}
|
||||
<a class="root card" class:locked={!root.open} href={root.open ? `/g/${root.slug}` : undefined}>
|
||||
<span class="eyebrow">Ğ1</span>
|
||||
<span class="name">{root.name}</span>
|
||||
{#if root.open}<span class="go" aria-hidden="true"><Icon name="chevron" size={20} /></span>{/if}
|
||||
{#if !root.open}<span class="lock"><Icon name="lock" size={16} />{t('groups.locked')}</span>{/if}
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if sameKey.length}
|
||||
<h3 class="eyebrow sub">{t('groups.same.key')}</h3>
|
||||
<ul class="list">
|
||||
{#each sameKey as g (g.slug)}
|
||||
<li>
|
||||
{#if g.open}
|
||||
<a class="row card" href="/g/{g.slug}"><span class="name">{g.name}</span><Icon name="chevron" size={16} /></a>
|
||||
{:else}
|
||||
<div class="row card locked" title={t('groups.locked.long')}>
|
||||
<span class="name">{g.name}</span>
|
||||
<span class="lock"><Icon name="lock" size={15} />{t('groups.locked')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if sameKey.some((g) => !g.open)}<p class="faint note">{t('groups.locked.long')}</p>{/if}
|
||||
{/if}
|
||||
|
||||
{#if others.length}
|
||||
<h3 class="eyebrow sub">{t('groups.test')}</h3>
|
||||
<ul class="list">
|
||||
{#each others as g (g.slug)}
|
||||
<li>
|
||||
<a class="row card" href={g.open ? `/g/${g.slug}` : undefined} class:locked={!g.open}>
|
||||
<span class="name">{g.name}</span><Icon name={g.open ? 'chevron' : 'lock'} size={16} />
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{#if !all.length}<p class="muted">{t('groups.empty')}</p>{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.choice {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
.choice > .eyebrow {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.root {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
padding: 1.6rem 1.6rem 1.7rem;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
a.root[href]:hover,
|
||||
a.row[href]:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.root .name {
|
||||
font-family: var(--font-text);
|
||||
font-size: clamp(1.7rem, 1.4rem + 1.2vw, 2.2rem);
|
||||
font-weight: 500;
|
||||
}
|
||||
.go {
|
||||
position: absolute;
|
||||
right: 1.4rem;
|
||||
top: 50%;
|
||||
translate: 0 -50%;
|
||||
color: var(--water);
|
||||
}
|
||||
.sub {
|
||||
margin-top: 1rem;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.2rem;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
.row .name {
|
||||
font-weight: 750;
|
||||
}
|
||||
.locked {
|
||||
background: transparent;
|
||||
box-shadow: inset 0 0 0 1px var(--surface-2);
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
.lock {
|
||||
display: inline-flex;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.note {
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
// The logotype « Clé de voûte » (DA, validated by Yvv on 2026-10-28): the sign — stones in
|
||||
// the ink of the mood, the keystone in `--cle` — then *libre* in Literata italic, the colour
|
||||
// of the stones, and **Decision** in roman, the colour of the keystone. Never tilted.
|
||||
// Proportions of the DA's lockup (prod_cle.py), for a font size S: the stones run from 0.98 S
|
||||
// above the baseline to 0.06 S below it, then a gap of 0.3 S. The SVG keeps the DA's 3-unit
|
||||
// margin around 59 units of stone height, hence the sizes below.
|
||||
import sign from '$lib/assets/cle-de-voute.svg?raw';
|
||||
|
||||
let { size = 1.45 }: { size?: number } = $props();
|
||||
</script>
|
||||
|
||||
<span class="logo" style:--s="{size}rem">
|
||||
<span class="sign" aria-hidden="true">{@html sign}</span><span class="word"><span class="libre">libre</span><span class="decision">Decision</span></span>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
display: inline-block;
|
||||
font-size: var(--s);
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sign {
|
||||
display: inline-block;
|
||||
color: var(--ink);
|
||||
/* 1.04 S of stones, plus the 3-unit margin above and below (65 / 59); the bottom of the
|
||||
stones 0.06 S under the baseline. */
|
||||
height: calc(1.04em * 65 / 59);
|
||||
margin-right: calc(0.3em - 1.04em * 3 / 59);
|
||||
vertical-align: calc(-0.06em - 1.04em * 3 / 59);
|
||||
}
|
||||
.sign :global(svg) {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
.word {
|
||||
font-family: var(--font-text);
|
||||
font-variation-settings: 'opsz' 72;
|
||||
}
|
||||
.libre {
|
||||
font-style: italic;
|
||||
font-weight: 420;
|
||||
letter-spacing: -0.004em;
|
||||
color: var(--ink);
|
||||
}
|
||||
.decision {
|
||||
font-weight: 620;
|
||||
letter-spacing: -0.006em;
|
||||
margin-left: 0.02em;
|
||||
color: var(--cle);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
// The four pillars of a group: name, one sentence, « ouvert » or « à venir ». Each leads to its
|
||||
// tool; the open one answers the pointer, the others stay dim, like in the menu.
|
||||
import { TOOLS, type Tool } from '$lib/group';
|
||||
import { t, type Key } from '$lib/i18n/index.svelte';
|
||||
|
||||
let { group, tools }: { group: string; tools: { key: string; active: boolean }[] } = $props();
|
||||
|
||||
const NAMES: Record<Tool, Key> = { decisions: 'nav.decisions', docs: 'nav.docs', mandats: 'nav.mandates', protocoles: 'nav.protocols' };
|
||||
const TEXTS: Record<Tool, Key> = {
|
||||
decisions: 'pillar.decisions',
|
||||
docs: 'pillar.docs',
|
||||
mandats: 'pillar.mandates',
|
||||
protocoles: 'pillar.protocols',
|
||||
};
|
||||
const active = $derived(new Set(tools.filter((x) => x.active).map((x) => x.key)));
|
||||
</script>
|
||||
|
||||
<ul class="pillars">
|
||||
{#each TOOLS as tool (tool)}
|
||||
{@const open = active.has(tool)}
|
||||
<li>
|
||||
<a class="pillar" class:open href="/g/{group}/{tool}">
|
||||
<span class="head">
|
||||
<h3>{t(NAMES[tool])}</h3>
|
||||
<span class="tag" class:water={open}>{open ? t('pillar.open') : t('pillar.soon')}</span>
|
||||
</span>
|
||||
<span class="text">{t(TEXTS[tool])}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.pillars {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
background: var(--surface-2);
|
||||
border-radius: var(--r-card);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
li {
|
||||
display: grid;
|
||||
}
|
||||
.pillar {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
align-content: start;
|
||||
padding: 1.4rem 1.5rem 1.5rem;
|
||||
background: var(--surface);
|
||||
color: var(--ink-faint);
|
||||
text-decoration: none;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
.pillar.open {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.pillar.open:hover {
|
||||
background: color-mix(in srgb, var(--water) 5%, var(--surface));
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.08rem;
|
||||
font-weight: 800;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.open h3 {
|
||||
color: var(--ink);
|
||||
}
|
||||
.text {
|
||||
font-size: 0.96rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.pillars {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
// « À venir »: what is announced in a tool of the group — dim, and not a way in yet.
|
||||
import type { UpcomingItem } from '$lib/group';
|
||||
import { i18n, t } from '$lib/i18n/index.svelte';
|
||||
import type { LangMap } from '$lib/types';
|
||||
|
||||
let { items }: { items: UpcomingItem[] } = $props();
|
||||
const id = $props.id();
|
||||
const pick = (m: LangMap) => m[i18n.lang] ?? m.fr ?? '';
|
||||
</script>
|
||||
|
||||
<section class="soon" aria-labelledby="{id}-soon">
|
||||
<h2 class="eyebrow" id="{id}-soon">{t('group.upcoming')}</h2>
|
||||
{#if items.length}
|
||||
<ul>
|
||||
{#each items as item (item.key)}
|
||||
<li>{pick(item.title)}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="faint">{t('group.upcoming.none')}</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.soon {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
h2 {
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
li {
|
||||
padding: 1.05rem 1.5rem;
|
||||
border-radius: var(--r-card);
|
||||
background: color-mix(in srgb, var(--surface) 45%, transparent);
|
||||
color: var(--ink-faint);
|
||||
font-family: var(--font-text);
|
||||
font-size: 1.2rem;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
// The logo stamp: the six lines of hexagram 48 (The Well), small, tilted.
|
||||
let { size = 2.2 }: { size?: number } = $props();
|
||||
const lines: boolean[] = [false, true, true, false, true, false];
|
||||
</script>
|
||||
|
||||
<span class="stamp" style:--s="{size}rem" aria-hidden="true">
|
||||
<svg viewBox="0 0 112 105">
|
||||
{#each lines as yang, i (i)}
|
||||
{@const y = 91 - i * 17}
|
||||
{#if yang}<rect x="6" {y} width="100" height="9" />{:else}<rect x="6" {y} width="42" height="9" /><rect x="64" {y} width="42" height="9" />{/if}
|
||||
{/each}
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.stamp {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: var(--s);
|
||||
height: var(--s);
|
||||
border-radius: var(--r-icon);
|
||||
background: var(--water-wash);
|
||||
transform: rotate(-10deg);
|
||||
transition: transform 0.2s var(--ease);
|
||||
}
|
||||
svg {
|
||||
width: 56%;
|
||||
fill: var(--water);
|
||||
}
|
||||
:global(a:hover) > .stamp {
|
||||
transform: rotate(-2deg) scale(1.06);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
// The page of a tool not open yet in a group (Mandats, Protocoles): what it is for, and what is
|
||||
// announced there, dim.
|
||||
import { page } from '$app/state';
|
||||
import Guard from '$lib/components/Guard.svelte';
|
||||
import SoonList from '$lib/components/SoonList.svelte';
|
||||
import { groups } from '$lib/groups.svelte';
|
||||
import { t, type Key } from '$lib/i18n/index.svelte';
|
||||
import { Remote } from '$lib/load.svelte';
|
||||
|
||||
let { tool }: { tool: 'mandats' | 'protocoles' } = $props();
|
||||
const NAMES: Record<typeof tool, Key> = { mandats: 'nav.mandates', protocoles: 'nav.protocols' };
|
||||
const TEXTS: Record<typeof tool, Key> = { mandats: 'pillar.mandates', protocoles: 'pillar.protocols' };
|
||||
|
||||
const slug = $derived(page.params.group ?? '');
|
||||
const data = new Remote(() => groups.load(slug));
|
||||
$effect(() => {
|
||||
void slug;
|
||||
void data.load();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if data.error}
|
||||
<Guard code={data.error} onretry={() => data.load()} />
|
||||
{:else if data.value}
|
||||
{@const g = data.value.group}
|
||||
<div class="page">
|
||||
<a class="eyebrow back" href="/g/{g.slug}">{g.name}</a>
|
||||
<h1>{t(NAMES[tool])}</h1>
|
||||
{#if !g.open}
|
||||
<p class="muted">{t('groups.locked.long')}</p>
|
||||
{:else}
|
||||
<p class="lead">{t(TEXTS[tool])}</p>
|
||||
<SoonList items={data.value.upcoming[tool]} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.page {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
padding: clamp(2.5rem, 8vh, 5rem) 1.4rem 5rem;
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
.back {
|
||||
text-decoration: none;
|
||||
}
|
||||
h1 {
|
||||
font-family: var(--font-text);
|
||||
font-weight: 500;
|
||||
font-size: clamp(2rem, 1.5rem + 2vw, 2.8rem);
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.lead {
|
||||
color: var(--ink-faint);
|
||||
max-width: 44ch;
|
||||
line-height: 1.55;
|
||||
margin-bottom: 1.8rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,24 @@
|
||||
import type { LangMap } from './types';
|
||||
|
||||
/** The tools of a group, in the order of the menu. */
|
||||
export const TOOLS = ['decisions', 'docs', 'mandats', 'protocoles'] as const;
|
||||
export type Tool = (typeof TOOLS)[number];
|
||||
|
||||
/** Announced in a tool, not open yet (`seeds/upcoming/<group>.json` on the server). */
|
||||
export interface UpcomingItem {
|
||||
key: string;
|
||||
title: LangMap;
|
||||
}
|
||||
|
||||
export interface GroupView {
|
||||
group: { slug: string; name: string; parent: string | null; policy: string; open: boolean };
|
||||
tools: { key: string; active: boolean }[];
|
||||
docs: { id: string; slug: string; title: LangMap; summary: LangMap; inForce: number; open: number }[];
|
||||
upcoming: { docs: UpcomingItem[]; mandats: UpcomingItem[]; protocoles: UpcomingItem[] };
|
||||
}
|
||||
|
||||
/** « Décisions »: one line per document in progress. */
|
||||
export interface DecisionsView {
|
||||
canVote: boolean;
|
||||
docs: { id: string; slug: string; title: LangMap; toVote: number; withoutMyVote: number | null }[];
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
+27
-15
@@ -10,30 +10,29 @@ export const en: Messages = {
|
||||
'nav.docs': 'Reference documents',
|
||||
'nav.mandates': 'Mandates',
|
||||
'nav.protocols': 'Protocols',
|
||||
'nav.soon': 'coming soon',
|
||||
'nav.groups': 'My groups',
|
||||
'nav.docs.count.one': '{n} document in progress',
|
||||
'nav.docs.count.other': '{n} documents in progress',
|
||||
'nav.access': 'Access',
|
||||
'nav.access.hint': 'trustinWallet',
|
||||
'nav.feedback': 'Give feedback',
|
||||
'nav.signin': 'Sign in',
|
||||
'nav.signout': 'Sign out',
|
||||
'nav.mood.toNight': 'Night mood',
|
||||
'nav.mood.toStone': 'Stone mood',
|
||||
'nav.mood': 'Mood',
|
||||
'mood.pierre': 'Stone',
|
||||
'mood.calcaire': 'Limestone',
|
||||
'mood.nuit': 'Night',
|
||||
'mood.ardoise': 'Slate',
|
||||
'nav.lang': 'Language',
|
||||
'nav.menu': 'Menu',
|
||||
'nav.close': 'Close',
|
||||
|
||||
'home.title': 'Making collective decisions possible',
|
||||
'home.subtitle': 'depending on the context, processes and methods, a toolbox',
|
||||
'home.decisions': 'A guided path to choose the right decision methods, and to keep the decision log.',
|
||||
'home.docs': 'Commitment act, license, charter, constitution: living texts, always open for voting.',
|
||||
'home.mandates': 'Create and revoke any kind of mandate.',
|
||||
'home.protocols': 'Workflows to organize without a statutory body: rotating roles and technical automations.',
|
||||
'home.open': 'open',
|
||||
'home.soon': 'coming soon',
|
||||
'home.leitmotiv.context': 'context and framing',
|
||||
'home.leitmotiv.process': 'processes and methods',
|
||||
'home.leitmotiv.tools': 'a toolbox',
|
||||
'home.signin': 'Sign in with your Ğ1 key',
|
||||
'home.signin.note': 'The same access as the Monnaie libre forum: you sign with Ğecko, no password needed.',
|
||||
'home.continue': 'Open my groups',
|
||||
|
||||
'signin.title': 'Sign in',
|
||||
'signin.g1': 'Sign in with Ğ1',
|
||||
@@ -77,8 +76,23 @@ export const en: Messages = {
|
||||
'groups.empty': 'No group is visible yet.',
|
||||
|
||||
'group.tools': 'Tools',
|
||||
'group.tool.soon': 'This tool is coming later.',
|
||||
'group.docs.empty': 'No reference documents in this group yet.',
|
||||
'group.current': 'In progress',
|
||||
'group.current.none': 'No document in progress in this group yet.',
|
||||
'group.upcoming': 'Coming up',
|
||||
'group.upcoming.none': 'Nothing is announced here yet.',
|
||||
|
||||
'pillar.decisions': 'A guided path to choose the right decision methods, and to keep the decision log.',
|
||||
'pillar.docs': 'Commitment act, license, charter, constitution: living texts, always open for voting.',
|
||||
'pillar.mandates': 'Create and revoke any kind of mandate.',
|
||||
'pillar.protocols': 'Workflows to organize without a statutory body: rotating roles and technical automations.',
|
||||
'pillar.open': 'open',
|
||||
'pillar.soon': 'coming soon',
|
||||
|
||||
'decisions.note': 'A module is an article of the document. It remains to be voted on until one of its versions has been adopted by a vote.',
|
||||
'decisions.toVote.one': '{n} module to vote on',
|
||||
'decisions.toVote.other': '{n} modules to vote on',
|
||||
'decisions.withoutMyVote.one': 'of which {n} without your vote',
|
||||
'decisions.withoutMyVote.other': 'of which {n} without your vote',
|
||||
|
||||
'docs.title': 'Reference documents',
|
||||
'docs.inForce.one': '{n} article in force',
|
||||
@@ -331,8 +345,6 @@ export const en: Messages = {
|
||||
'access.publication.queued': 'Publication started again.',
|
||||
'access.denied': 'Access management is for trustinWallet administrators only.',
|
||||
|
||||
'soon.title': 'Coming soon',
|
||||
'soon.body': 'This tool is coming later. Tools open one at a time: reference documents first.',
|
||||
|
||||
'error.unauthenticated': 'Sign in to continue.',
|
||||
'error.forbidden': "You don't have access to this page.",
|
||||
|
||||
+27
-15
@@ -10,30 +10,29 @@ export const es: Messages = {
|
||||
'nav.docs': 'Documentos de referencia',
|
||||
'nav.mandates': 'Mandatos',
|
||||
'nav.protocols': 'Protocolos',
|
||||
'nav.soon': 'próximamente',
|
||||
'nav.groups': 'Mis grupos',
|
||||
'nav.docs.count.one': '{n} documento en curso',
|
||||
'nav.docs.count.other': '{n} documentos en curso',
|
||||
'nav.access': 'Accesos',
|
||||
'nav.access.hint': 'trustinWallet',
|
||||
'nav.feedback': 'Dejar un comentario',
|
||||
'nav.signin': 'Iniciar sesión',
|
||||
'nav.signout': 'Cerrar sesión',
|
||||
'nav.mood.toNight': 'Ambiente noche',
|
||||
'nav.mood.toStone': 'Ambiente piedra',
|
||||
'nav.mood': 'Ambiente',
|
||||
'mood.pierre': 'Piedra',
|
||||
'mood.calcaire': 'Caliza',
|
||||
'mood.nuit': 'Noche',
|
||||
'mood.ardoise': 'Pizarra',
|
||||
'nav.lang': 'Idioma',
|
||||
'nav.menu': 'Menú',
|
||||
'nav.close': 'Cerrar',
|
||||
|
||||
'home.title': 'Hacer posible la decisión colectiva',
|
||||
'home.subtitle': 'según el contexto, procesos y modalidades, caja de herramientas',
|
||||
'home.decisions': 'Un recorrido guiado para elegir las modalidades de decisión adecuadas y llevar el registro de las decisiones.',
|
||||
'home.docs': 'Acta de compromiso, licencia, carta, constitución: textos vivos, en votación permanente.',
|
||||
'home.mandates': 'Crear y revocar todo tipo de mandato.',
|
||||
'home.protocols': 'Flujos de trabajo para organizarse sin órgano estatutario: roles rotativos y automatizaciones técnicas.',
|
||||
'home.open': 'abierto',
|
||||
'home.soon': 'próximamente',
|
||||
'home.leitmotiv.context': 'contexto y encuadre',
|
||||
'home.leitmotiv.process': 'procesos y modalidades',
|
||||
'home.leitmotiv.tools': 'caja de herramientas',
|
||||
'home.signin': 'Iniciar sesión con tu clave Ğ1',
|
||||
'home.signin.note': 'El mismo acceso que el foro Monnaie libre: firmas en Ğecko, sin contraseña.',
|
||||
'home.continue': 'Abrir mis grupos',
|
||||
|
||||
'signin.title': 'Iniciar sesión',
|
||||
'signin.g1': 'Iniciar sesión con Ğ1',
|
||||
@@ -77,8 +76,23 @@ export const es: Messages = {
|
||||
'groups.empty': 'Por ahora no hay ningún grupo visible.',
|
||||
|
||||
'group.tools': 'Herramientas',
|
||||
'group.tool.soon': 'Esta herramienta llegará más adelante.',
|
||||
'group.docs.empty': 'Por ahora no hay ningún documento de referencia en este grupo.',
|
||||
'group.current': 'En curso',
|
||||
'group.current.none': 'Por ahora no hay ningún documento en curso en este grupo.',
|
||||
'group.upcoming': 'Próximamente',
|
||||
'group.upcoming.none': 'Por ahora no hay nada anunciado aquí.',
|
||||
|
||||
'pillar.decisions': 'Un recorrido guiado para elegir las modalidades de decisión adecuadas y llevar el registro de las decisiones.',
|
||||
'pillar.docs': 'Acta de compromiso, licencia, carta, constitución: textos vivos, en votación permanente.',
|
||||
'pillar.mandates': 'Crear y revocar todo tipo de mandato.',
|
||||
'pillar.protocols': 'Flujos de trabajo para organizarse sin órgano estatutario: roles rotativos y automatizaciones técnicas.',
|
||||
'pillar.open': 'abierto',
|
||||
'pillar.soon': 'próximamente',
|
||||
|
||||
'decisions.note': 'Un módulo es un artículo del documento. Queda por votar mientras ninguna de sus versiones haya sido adoptada por votación.',
|
||||
'decisions.toVote.one': '{n} módulo por votar',
|
||||
'decisions.toVote.other': '{n} módulos por votar',
|
||||
'decisions.withoutMyVote.one': 'de ellos, {n} sin tu voto',
|
||||
'decisions.withoutMyVote.other': 'de ellos, {n} sin tu voto',
|
||||
|
||||
'docs.title': 'Documentos de referencia',
|
||||
'docs.inForce.one': '{n} artículo en vigor',
|
||||
@@ -331,8 +345,6 @@ export const es: Messages = {
|
||||
'access.publication.queued': 'Publicación relanzada.',
|
||||
'access.denied': 'La gestión de accesos está reservada a los administradores de trustinWallet.',
|
||||
|
||||
'soon.title': 'Próximamente',
|
||||
'soon.body': 'Esta herramienta llegará más adelante. Las herramientas se abren una a una: primero, los documentos de referencia.',
|
||||
|
||||
'error.unauthenticated': 'Inicia sesión para continuar.',
|
||||
'error.forbidden': 'No tienes acceso a esta página.',
|
||||
|
||||
+27
-15
@@ -13,30 +13,29 @@ export const fr = {
|
||||
'nav.docs': 'Docs de référence',
|
||||
'nav.mandates': 'Mandats',
|
||||
'nav.protocols': 'Protocoles',
|
||||
'nav.soon': 'à venir',
|
||||
'nav.groups': 'Mes groupes',
|
||||
'nav.docs.count.one': '{n} doc en cours',
|
||||
'nav.docs.count.other': '{n} docs en cours',
|
||||
'nav.access': 'Accès',
|
||||
'nav.access.hint': 'trustinWallet',
|
||||
'nav.feedback': 'Faire un retour',
|
||||
'nav.signin': 'Se connecter',
|
||||
'nav.signout': 'Se déconnecter',
|
||||
'nav.mood.toNight': 'Ambiance nuit',
|
||||
'nav.mood.toStone': 'Ambiance pierre',
|
||||
'nav.mood': 'Ambiance',
|
||||
'mood.pierre': 'Pierre',
|
||||
'mood.calcaire': 'Calcaire',
|
||||
'mood.nuit': 'Nuit',
|
||||
'mood.ardoise': 'Ardoise',
|
||||
'nav.lang': 'Langue',
|
||||
'nav.menu': 'Menu',
|
||||
'nav.close': 'Fermer',
|
||||
|
||||
'home.title': 'Rendre possible la décision collective',
|
||||
'home.subtitle': 'en fonction du contexte, processus et modalités, boîte à outils',
|
||||
'home.decisions': "Un parcours assisté pour choisir les bonnes modalités de décision, et tenir le journal des décisions.",
|
||||
'home.docs': "Acte d'engagement, licence, charte, constitution : des textes vivants, sous vote permanent.",
|
||||
'home.mandates': 'Créer et révoquer tout type de mandat.',
|
||||
'home.protocols': "Des workflows pour s'organiser sans organe statutaire : rôles tournants et automatisations techniques.",
|
||||
'home.open': 'ouvert',
|
||||
'home.soon': 'à venir',
|
||||
'home.leitmotiv.context': 'contexte et cadrage',
|
||||
'home.leitmotiv.process': 'processus et modalités',
|
||||
'home.leitmotiv.tools': 'boîte à outils',
|
||||
'home.signin': 'Se connecter avec sa clé Ğ1',
|
||||
'home.signin.note': 'Le même accès que le forum Monnaie libre : tu signes dans Ğecko, sans mot de passe.',
|
||||
'home.continue': 'Ouvrir mes groupes',
|
||||
|
||||
'signin.title': 'Se connecter',
|
||||
'signin.g1': 'Se connecter avec Ğ1',
|
||||
@@ -80,8 +79,23 @@ export const fr = {
|
||||
'groups.empty': "Aucun groupe n'est visible pour l'instant.",
|
||||
|
||||
'group.tools': 'Outils',
|
||||
'group.tool.soon': 'Cet outil arrive plus tard.',
|
||||
'group.docs.empty': "Aucun document de référence dans ce groupe pour l'instant.",
|
||||
'group.current': 'En cours',
|
||||
'group.current.none': "Aucun doc en cours dans ce groupe pour l'instant.",
|
||||
'group.upcoming': 'À venir',
|
||||
'group.upcoming.none': "Rien n'est annoncé ici pour l'instant.",
|
||||
|
||||
'pillar.decisions': "Un parcours assisté pour choisir les bonnes modalités de décision, et tenir le journal des décisions.",
|
||||
'pillar.docs': "Acte d'engagement, licence, charte, constitution : des textes vivants, sous vote permanent.",
|
||||
'pillar.mandates': 'Créer et révoquer tout type de mandat.',
|
||||
'pillar.protocols': "Des workflows pour s'organiser sans organe statutaire : rôles tournants et automatisations techniques.",
|
||||
'pillar.open': 'ouvert',
|
||||
'pillar.soon': 'à venir',
|
||||
|
||||
'decisions.note': "Un module est un article du doc. Il reste à voter tant qu'aucune de ses versions n'a été adoptée par un vote.",
|
||||
'decisions.toVote.one': '{n} module à voter',
|
||||
'decisions.toVote.other': '{n} modules à voter',
|
||||
'decisions.withoutMyVote.one': 'dont {n} sans ton vote',
|
||||
'decisions.withoutMyVote.other': 'dont {n} sans ton vote',
|
||||
|
||||
'docs.title': 'Docs de référence',
|
||||
'docs.inForce.one': '{n} article en vigueur',
|
||||
@@ -334,8 +348,6 @@ export const fr = {
|
||||
'access.publication.queued': 'Publication relancée.',
|
||||
'access.denied': 'La gestion des accès est réservée aux administrateurs de trustinWallet.',
|
||||
|
||||
'soon.title': 'À venir',
|
||||
'soon.body': "Cet outil arrive plus tard. Les chantiers s'ouvrent un par un : d'abord les docs de référence.",
|
||||
|
||||
'error.unauthenticated': 'Connecte-toi pour continuer.',
|
||||
'error.forbidden': "Tu n'as pas accès à cette page.",
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
// Two moods, light and dark: « pierre » (stone) and « nuit » (night).
|
||||
export type Mood = 'pierre' | 'nuit';
|
||||
// Four moods, two light and two dark: « pierre » and « nuit » (the water),
|
||||
// « calcaire » and « ardoise » (the ochre). Set before the first paint by
|
||||
// app.html; a first visit follows the device's light or dark setting.
|
||||
export const MOODS = ['pierre', 'calcaire', 'nuit', 'ardoise'] as const;
|
||||
export type Mood = (typeof MOODS)[number];
|
||||
|
||||
const isMood = (m: string | undefined): m is Mood => MOODS.includes(m as Mood);
|
||||
|
||||
class MoodState {
|
||||
current = $state<Mood>('pierre');
|
||||
|
||||
init() {
|
||||
const m = document.documentElement.dataset.mood;
|
||||
this.current = m === 'nuit' ? 'nuit' : 'pierre';
|
||||
this.current = isMood(m) ? m : 'pierre';
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.current = this.current === 'nuit' ? 'pierre' : 'nuit';
|
||||
document.documentElement.dataset.mood = this.current;
|
||||
set(m: Mood) {
|
||||
this.current = m;
|
||||
const root = document.documentElement;
|
||||
root.dataset.mood = m;
|
||||
const bg = getComputedStyle(root).getPropertyValue('--bg').trim();
|
||||
if (bg) document.querySelector('meta[name="theme-color"]')?.setAttribute('content', bg);
|
||||
try {
|
||||
localStorage.setItem('ld:mood', this.current);
|
||||
localStorage.setItem('ld:mood', m);
|
||||
} catch {
|
||||
/* private mode: the mood just won't persist */
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user