feat(r0.11): apps/web — connexion + sélecteur démo, coquille, /design, client typé

- React 19 + Vite + Tailwind v4 : tokens.css copié tel quel (source de
  vérité), classes composants extraites de la maquette validée, Manrope
  auto-hébergée, primitives shadcn-style possédées (Button cva, menu Radix)
- connexion fidèle à la maquette ; liste démo affichée seulement si
  /auth/demo-accounts répond (404 ⇒ rien — ADR-002, l'API seule source)
- coquille : sidebar 4 groupes avec rail safran actif (écrans futurs marqués
  R1-R3), topbar recherche/thème/chip DÉMO + sélecteur de rôle (bascule 101 ms,
  critère < 3 s) ; page /design = référence vivante des revues pixel
- règle d'or : schema.d.ts généré depuis docs/openapi.json et committé
  (openapi-typescript + openapi-fetch)
- tokens.css : tokens --nav-* ajoutés (valeurs de la maquette validée) ;
  seed réaligné sur les personas de la maquette
- vérifié en navigateur réel (Playwright + Chrome headless) : 7 captures,
  parcours démo complet, bi-thème, zéro erreur console ; typecheck + vitest + build OK

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-15 22:09:40 +01:00
parent 8adb561b63
commit 4f3af5af4f
27 changed files with 4733 additions and 22 deletions

556
apps/web/src/styles/app.css Normal file
View File

@@ -0,0 +1,556 @@
/*
* SIOP V2 — styles applicatifs.
* tokens.css est copié TEL QUEL depuis docs/02-design/ (source de vérité).
* Les classes composants ci-dessous sont EXTRAITES de la maquette validée
* (docs/02-design/maquettes/maquette-web.html) — seule adaptation : la
* coquille remplit le viewport au lieu du cadre de revue 1280 px.
* Règle : aucune valeur visuelle en dur — tout vient des tokens.
*/
@import 'tailwindcss';
@import './tokens.css';
@theme inline {
--color-fond: var(--fond);
--color-surface: var(--surface);
--color-surface-2: var(--surface-2);
--color-bordure: var(--bordure);
--color-bordure-forte: var(--bordure-forte);
--color-encre: var(--encre);
--color-encre-2: var(--encre-2);
--color-encre-3: var(--encre-3);
--color-primaire: var(--primaire);
--color-primaire-doux: var(--primaire-doux);
--color-safran: var(--safran);
--color-safran-doux: var(--safran-doux);
--color-succes: var(--succes);
--color-alerte: var(--alerte);
--color-danger: var(--danger);
--color-info: var(--info);
--font-ui: var(--police-ui);
--radius-carte: var(--rayon);
--radius-controle: var(--rayon-controle);
}
/* ═══ Base (maquette §Base) ═══ */
html,
body {
background: var(--fond);
}
body {
font-family: var(--police-ui);
color: var(--encre);
font-size: 14px;
line-height: 1.5;
}
button {
font: inherit;
cursor: pointer;
}
button:focus-visible,
a:focus-visible {
outline: 2px solid var(--primaire);
outline-offset: 2px;
border-radius: 4px;
}
table {
border-collapse: collapse;
font-variant-numeric: tabular-nums;
}
.num {
font-variant-numeric: tabular-nums;
}
/* ═══ Coquille applicative (plein viewport) ═══ */
.app {
display: grid;
grid-template-columns: 232px 1fr;
min-height: 100vh;
background: var(--fond);
}
/* Sidebar (toujours sombre — identité) */
.nav {
background: var(--nav-fond);
color: var(--nav-encre);
display: flex;
flex-direction: column;
padding: 16px 0;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
padding: 2px 18px 16px;
color: var(--nav-encre-vif);
}
.logo .marque {
display: flex;
flex-direction: column;
line-height: 0.55;
font-size: 11px;
color: var(--safran);
}
.logo b {
font-size: 19px;
letter-spacing: 0.04em;
}
.logo small {
color: var(--nav-encre);
font-weight: 400;
font-size: 11px;
display: block;
margin-top: 2px;
}
.nav .groupe {
padding: 12px 18px 4px;
font-size: 10.5px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #6b7f9e;
}
.nav a {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 18px;
color: var(--nav-encre);
text-decoration: none;
font-size: 13.5px;
border-left: var(--rail) solid transparent;
transition: background var(--transition);
}
.nav a:hover {
background: rgb(255 255 255 / 0.06);
color: var(--nav-encre-vif);
}
.nav a.actif {
background: rgb(255 255 255 / 0.09);
color: var(--nav-encre-vif);
border-left-color: var(--safran);
}
.nav a[aria-disabled='true'] {
opacity: 0.45;
cursor: default;
}
.nav a .badge-nav {
margin-left: auto;
background: var(--prio-bloque);
color: #fff;
font-size: 11px;
padding: 1px 7px;
border-radius: var(--rayon-pastille);
font-weight: 700;
}
.nav a .badge-release {
margin-left: auto;
font-size: 9.5px;
letter-spacing: 0.06em;
color: #6b7f9e;
border: 1px solid var(--nav-bordure);
padding: 1px 6px;
border-radius: var(--rayon-pastille);
}
.nav svg {
width: 17px;
height: 17px;
stroke-width: 1.75;
flex: none;
}
/* Topbar */
.contenu {
display: flex;
flex-direction: column;
min-width: 0;
}
.topbar {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
background: var(--surface);
border-bottom: 1px solid var(--bordure);
}
.recherche {
flex: 1;
max-width: 420px;
display: flex;
align-items: center;
gap: 8px;
background: var(--surface-2);
border: 1px solid var(--bordure);
border-radius: var(--rayon-controle);
padding: 7px 12px;
color: var(--encre-3);
font-size: 13px;
}
.recherche svg {
width: 15px;
height: 15px;
}
.compte {
margin-left: auto;
display: flex;
align-items: center;
gap: 10px;
}
.chip-demo {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.05em;
color: var(--safran);
background: var(--safran-doux);
padding: 3px 9px;
border-radius: var(--rayon-pastille);
border: 1px dashed var(--safran);
}
.avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background: var(--primaire);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex: none;
}
.compte .qui {
line-height: 1.15;
font-size: 12.5px;
text-align: left;
}
.compte .qui small {
color: var(--encre-2);
}
/* Corps de page */
.page {
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.entete-page {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.entete-page h1 {
font-size: 21px;
font-weight: 700;
letter-spacing: -0.01em;
text-wrap: balance;
}
.entete-page .actions {
margin-left: auto;
display: flex;
gap: 8px;
}
/* Boutons (variants shadcn-style pilotés par ces classes) */
.btn {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 14px;
border-radius: var(--rayon-controle);
font-size: 13.5px;
font-weight: 600;
border: 1px solid var(--bordure-forte);
background: var(--surface);
color: var(--encre);
transition: filter var(--transition);
}
.btn:hover {
filter: brightness(0.97);
}
.btn.prim {
background: var(--primaire);
border-color: var(--primaire);
color: #fff;
}
.btn.succes {
background: var(--succes);
border-color: var(--succes);
color: #fff;
}
.btn.danger-l {
color: var(--danger);
border-color: color-mix(in srgb, var(--danger) 40%, transparent);
background: transparent;
}
.btn.ghost {
border-color: transparent;
background: transparent;
color: var(--encre-2);
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn svg {
width: 15px;
height: 15px;
stroke-width: 2;
}
/* Cartes */
.carte {
background: var(--surface);
border: 1px solid var(--bordure);
border-radius: var(--rayon);
padding: 16px 18px;
}
.carte h2 {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--encre-2);
margin-bottom: 12px;
}
.grille-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 900px) {
.grille-2 {
grid-template-columns: 1fr;
}
}
/* Pastilles statut / priorité (réservées — jamais réutilisées ailleurs) */
.st {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 10px;
border-radius: var(--rayon-pastille);
font-size: 12px;
font-weight: 700;
}
.st::before {
content: '';
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
}
.st.ouvert { color: var(--st-ouvert); background: var(--st-ouvert-fond); }
.st.encours { color: var(--st-encours); background: var(--st-encours-fond); }
.st.attente { color: var(--st-attente); background: var(--st-attente-fond); }
.st.termine { color: var(--st-termine); background: var(--st-termine-fond); }
.st.annule { color: var(--st-annule); background: var(--st-annule-fond); }
.prio {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
font-weight: 600;
}
.prio i {
width: 8px;
height: 8px;
border-radius: 50%;
flex: none;
}
.point-pulse {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--prio-bloque);
animation: pulse 1.4s ease-out infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--prio-bloque) 45%, transparent); }
70% { box-shadow: 0 0 0 8px transparent; }
100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) {
.point-pulse { animation: none; }
}
.prio.bloque { color: var(--prio-bloque); }
.prio.bloque i { background: var(--prio-bloque); animation: pulse 1.4s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
.prio.bloque i { animation: none; }
}
.prio.haute { color: var(--prio-haute); }
.prio.haute i { background: var(--prio-haute); }
.prio.moyenne { color: var(--prio-moyenne); }
.prio.moyenne i { background: var(--prio-moyenne); }
.prio.basse { color: var(--prio-basse); }
.prio.basse i { background: var(--prio-basse); }
/* ═══ Connexion ═══ */
.conn {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(
160deg,
var(--fond) 55%,
color-mix(in srgb, var(--primaire) 7%, var(--fond))
);
}
.carte-conn {
width: 400px;
max-width: 94%;
background: var(--surface);
border: 1px solid var(--bordure);
border-radius: 14px;
padding: 32px;
display: flex;
flex-direction: column;
gap: 14px;
}
.carte-conn .logo {
padding: 0 0 4px;
color: var(--encre);
}
.carte-conn .logo small {
color: var(--encre-2);
}
.champ label {
display: block;
font-size: 12.5px;
font-weight: 600;
color: var(--encre-2);
margin-bottom: 5px;
}
.champ input {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--bordure-forte);
border-radius: var(--rayon-controle);
font: inherit;
background: var(--surface);
color: var(--encre);
}
.champ input::placeholder {
color: var(--encre-3);
}
.sep {
display: flex;
align-items: center;
gap: 10px;
color: var(--encre-3);
font-size: 11.5px;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-top: 6px;
}
.sep::before,
.sep::after {
content: '';
flex: 1;
height: 1px;
background: var(--bordure);
}
.demo-liste {
display: flex;
flex-direction: column;
gap: 6px;
}
.demo-liste button {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 1px solid var(--bordure);
border-radius: var(--rayon-controle);
background: var(--surface);
color: var(--encre);
text-align: left;
transition: border-color var(--transition);
}
.demo-liste button:hover {
border-color: var(--primaire);
background: var(--primaire-doux);
}
.demo-liste .role {
margin-left: auto;
font-size: 11.5px;
color: var(--encre-2);
}
.erreur-form {
color: var(--danger);
font-size: 12.5px;
font-weight: 600;
}
/* ═══ Menu (compte / bascule démo) ═══ */
.menu {
min-width: 230px;
background: var(--surface);
border: 1px solid var(--bordure);
border-radius: var(--rayon);
box-shadow: var(--ombre-menu);
padding: 6px;
z-index: 60;
}
.menu .titre-menu {
padding: 7px 10px 3px;
font-size: 10.5px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--encre-3);
}
.menu .item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 7px 10px;
border-radius: 7px;
font-size: 13px;
color: var(--encre);
text-align: left;
border: 0;
background: none;
outline: none;
}
.menu .item:hover,
.menu .item[data-highlighted] {
background: var(--primaire-doux);
}
.menu .item .role {
margin-left: auto;
font-size: 11px;
color: var(--encre-2);
}
.menu .item svg {
width: 15px;
height: 15px;
stroke-width: 1.75;
color: var(--encre-2);
}
.menu .separateur {
height: 1px;
background: var(--bordure);
margin: 6px 4px;
}
/* ═══ Page /design : nuanciers ═══ */
.nuancier {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.puce-c {
width: 84px;
font-size: 10.5px;
color: var(--encre-2);
}
.puce-c i {
display: block;
height: 40px;
border-radius: var(--rayon-controle);
border: 1px solid var(--bordure);
margin-bottom: 4px;
}