gestion_table_tournante_libre/src/interface/Bandeau.svelte
Mathieu Benoit 4d34039f47 [ADD] interface: visual foundations, pills, cards, icons, navy header
Screens need colour that separates information without ever carrying it
alone. Tokens gain state, membership and status tints whose marks hold
3:1 and texts 4.5:1 in both themes and under three dichromacies, and
every hue stays 30 degrees from the reserved conflict hues. Commands get
three levels; Pastille, Carte and an icon module join the primitives;
memberships map to six stable tints over sorted names. A pill label
wraps and never truncates, and keeps one line beside a nowrap neighbour.
Checked: node 3949; browser 767, only the walkthrough captures differ.

--- FR ---

[ADD] interface : fondations, pastilles, cartes, icônes, bandeau marine

Les écrans ont besoin d'une couleur qui distingue l'information sans
jamais la porter seule. Les jetons gagnent des teintes d'état,
d'appartenance et de statut dont les marques tiennent 3:1 et les textes
4,5:1 dans les deux thèmes et sous trois dichromaties, chaque teinte à
30 degrés des teintes de conflit réservées. Les commandes ont trois
niveaux ; Pastille, Carte et un module d'icônes rejoignent les
primitives ; les appartenances reçoivent six teintes stables sur les
noms triés. Un libellé de pastille se replie, jamais tronqué.
Vérifié : node 3949 ; navigateur 767, seules les captures du parcours.

Assisted-by: Claude Opus 5.5
2026-10-10 22:59:59 -04:00

201 lines
7.1 KiB
Svelte

<!-- © 2026 TechnoLibre (http://www.technolibre.ca)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script>
// Le bandeau d'application, présent sur tout écran (§ 8.4, § 18.6) : le
// nom, la version — ou la marque de provenance d'une construction de
// documentation —, la provenance, et, quand un événement est ouvert, le
// libellé de mode, l'annonce de format du fichier et la commande « Liste
// des événements ». Il porte aussi le réglage de thème, deux commandes à
// aria-pressed, et l'ouverture du panneau « À propos ». Collé en haut de la
// fenêtre, il ne défile pas.
//
// Son habillage : la marque — le dessin d'une table ronde et de ses six
// chaises, caché aux technologies d'assistance —, le nom, la version et la
// provenance en pilules, le libellé de mode en pilule grasse, puis, à
// droite, « Liste des événements » précédée de son icône, le thème en
// commutateur segmenté — le retenu plein de l'encre du bandeau, en gras —
// et « À propos ». Chaque élément garde la hauteur d'une ligne de commande
// du bandeau : le bandeau tient sur une ligne à 1 366 px, l'événement
// ouvert, et la surface de travail garde sa hauteur.
//
// Le bandeau ne décide de rien : App lui donne l'état de la séance, la
// version et la provenance telles qu'elles s'affichent, le thème et les
// gestes à appeler ; mode.js choisit les clés du libellé de mode et de
// l'annonce. Le refus de « Liste des événements » passe par la primitive
// Refus, qui pose son texte et son remède en Commande — écrire ailleurs
// sous un support rompu. Ce refus se pose sous les commandes, hors du
// flux : le bandeau garde sa hauteur, et l'écran de l'événement, sa place
// fixe comprise, ne bouge pas (§ 8.4). Le refus est un bloc : les
// commandes qui le portent sont un <div>, non un <span>.
import { getContext } from 'svelte';
import { cleAnnonceFormat, cleDeMode } from './mode.js';
import Icone from './primitives/Icone.svelte';
import Refus from './primitives/Refus.svelte';
import { THEMES, cleDeTheme } from './theme.js';
const t = getContext('t');
let { etat, version, provenance, theme, refusListe, choisirTheme, ouvrirApropos, revenirALaListe } = $props();
const cleMode = $derived(cleDeMode(etat));
const cleAnnonce = $derived(cleAnnonceFormat(etat));
</script>
<header class="bandeau" data-bandeau>
<svg class="marque" width="24" height="24" viewBox="0 0 30 30" aria-hidden="true" focusable="false">
<circle cx="15" cy="15" r="7" fill="none" stroke="currentColor" stroke-width="2" />
<circle cx="15" cy="3.5" r="2.5" fill="currentColor" />
<circle cx="25" cy="9.5" r="2.5" fill="currentColor" />
<circle cx="25" cy="20.5" r="2.5" fill="currentColor" />
<circle cx="15" cy="26.5" r="2.5" fill="currentColor" />
<circle cx="5" cy="20.5" r="2.5" fill="currentColor" />
<circle cx="5" cy="9.5" r="2.5" fill="currentColor" />
</svg>
<span class="nom">{t('coquille.nom')}</span>
<span class="pilule version">{version}</span>
<span class="pilule provenance">{provenance}</span>
{#if cleMode !== null}
<span class="pilule mode" data-mode-libelle="bandeau">{t(cleMode)}</span>
{/if}
{#if cleAnnonce !== null}
<span class="pilule annonce" data-avertissement="annonceFormat">{t(cleAnnonce)}</span>
{/if}
<div class="commandes">
{#if etat.evenement !== null}
<button type="button" data-commande="liste" onclick={revenirALaListe}
><Icone nom="liste" />{t('coquille.liste')}</button
>
{#if refusListe !== null}
<div class="refus-liste" data-refus="liste">
<Refus refus={refusListe} />
</div>
{/if}
{/if}
<span class="themes" role="group" aria-label={t('coquille.theme.titre')}>
{#each THEMES as choix (choix)}
<button
type="button"
data-commande="theme"
data-theme-choix={choix}
aria-pressed={theme === choix}
onclick={() => choisirTheme(choix)}>{t(cleDeTheme(choix))}</button
>
{/each}
</span>
<button type="button" data-commande="apropos" onclick={ouvrirApropos}>{t('coquille.apropos.titre')}</button>
</div>
</header>
<style>
.bandeau {
position: sticky;
top: 0;
z-index: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--espace-2) var(--espace-3);
padding: var(--espace-2) var(--espace-4);
border-bottom: 1px solid var(--couleur-bandeau-filet);
background-color: var(--couleur-bandeau-fond);
color: var(--couleur-bandeau-texte);
font-size: var(--taille-texte-bandeau);
}
.marque {
flex: none;
color: var(--couleur-bandeau-marque);
}
.nom {
font-weight: bold;
}
/* La version, la provenance, le libellé de mode et l'annonce : des
pilules à filet. */
.pilule {
padding: 0.125rem var(--espace-2);
border: 1px solid var(--couleur-bandeau-filet);
border-radius: var(--rayon-pilule);
font-size: var(--taille-texte-petit);
}
.version,
.provenance {
color: var(--couleur-bandeau-texte-secondaire);
}
.mode {
font-weight: bold;
}
/* Le refus garde les couleurs de la page, sur lesquelles sa couleur et
celles de sa commande tiennent leur contraste : le fond du bandeau ne
passe pas sous lui. Posé sous les commandes, aligné à leur droite, il
recouvre l'écran au lieu de le pousser. Sa largeur bornée le tient à
droite, loin de la place fixe, qui ouvre l'écran à gauche, et dans la
fenêtre. */
.refus-liste {
position: absolute;
top: 100%;
right: 0;
width: max-content;
max-width: min(32rem, calc(100vw - 2 * var(--espace-4)));
margin-top: var(--espace-1);
border-radius: var(--rayon-2);
background-color: var(--couleur-page-fond);
color: var(--couleur-page-texte);
box-shadow: var(--ombre-2);
}
.commandes {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--espace-2);
margin-left: auto;
}
button {
display: inline-flex;
align-items: center;
gap: var(--espace-1);
padding: var(--espace-1) var(--espace-2);
border: 1px solid var(--couleur-bandeau-filet);
border-radius: var(--rayon-2);
background-color: var(--couleur-bandeau-fond);
color: var(--couleur-bandeau-texte);
font: inherit;
cursor: pointer;
}
/* Le commutateur de thème : un groupe bordé, sans fond, dont les deux
choix se touchent. Le retenu se distingue par sa forme autant que par
ses couleurs : plein, aux couleurs du bandeau inversées, et en gras. */
.themes {
display: inline-flex;
padding: 0.0625rem;
border: 1px solid var(--couleur-bandeau-filet);
border-radius: var(--rayon-2);
}
.themes button {
padding: 0.1875rem var(--espace-2);
border: none;
border-radius: var(--rayon-1);
background-color: transparent;
}
.themes button[aria-pressed='true'] {
background-color: var(--couleur-bandeau-texte);
color: var(--couleur-bandeau-fond);
font-weight: bold;
}
button:focus-visible {
outline: 2px solid var(--couleur-focus);
outline-offset: 2px;
}
</style>