Files

226 lines
14 KiB
Markdown
Raw Permalink Normal View History

# SPECS_SVELTE.md — Conventions Svelte/SvelteKit pour « veille-ia »
> **Statut :** ce fichier PRÉVAUT sur tout choix technique frontend (cf. `prompt_kimi_cli_veille_ia.md`).
> Il consolide les recommandations de `doc2sveltekit-transition/` (playbook SvelteKit, charte OKI,
> best practices Svelte 5) adaptées à une **app privée d'administration** self-hosted YunoHost —
> pas un site vitrine public : pas de SEO, pas de motion « Awwwards », pas de PWA.
>
> En cas de conflit : ce document prime sur la recette générique pour la marque et le style ;
> le PRD (`plan_prd_veille_ia.md`) prime pour le fonctionnel ; la recette prime pour la méthode technique.
---
## 1. Stack
- **SvelteKit 2 + Svelte 5** (runes uniquement) + **TypeScript strict** (`strict: true`).
- **`@sveltejs/adapter-node`** — l'app tourne en service systemd derrière nginx (YunoHost).
- **Vite** (fourni par SvelteKit). Aucune configuration exotique.
- **CSS vanilla + tokens OKI** — **pas de Tailwind**, aucun framework CSS lourd (décision : recette §2.3, charte OKI).
- **Zod** pour la validation des schémas YAML (registre, profils, alertes).
- **js-yaml** pour la persistance YAML ; **simple-git** pour les commits automatiques du data_dir.
- **vitest** pour les tests (moteur de recommandation notamment).
- **eslint + prettier** (config SvelteKit standard).
- Gestionnaire de paquets : **npm** (lockfile `package-lock.json`, jamais mélangé avec pnpm/yarn).
- Node : **24 LTS** en dev comme en prod (ressource `nodejs` YunoHost alignée sur le catalogue : nodered, etherpad, hedgedoc utilisent `"24"`).
**Interdictions (recette §0, adaptées) :**
- Jamais de dépendance installée sans être importée et utilisée.
- Jamais de SSR désactivé globalement pour « faire marcher » une lib client.
- Jamais d'appel à un service tiers au chargement (fonts, CDN, analytics) — tout asset est self-hébergé.
- Aucune dépendance propriétaire ou source-available (licences OSI uniquement, projet AGPL-3.0).
## 2. Structure de dossiers (`app/`)
```
app/
├── src/
│ ├── app.html # lang="fr", script theme.js externe (anti-FOUC)
│ ├── app.d.ts # typage Locals (user SSO), env
│ ├── hooks.server.ts # auth SSO (YNH_USER), CSP, paths.base
│ ├── lib/
│ │ ├── styles/
│ │ │ ├── oki-tokens.css # tokens charte OKI (copie versionnée, §4)
│ │ │ └── base.css # reset, primitives, gate reduced-motion
│ │ ├── server/ # code serveur uniquement (jamais importé côté client)
│ │ │ ├── config.ts # variables d'env (DATA_DIR, OLLAMA_URL, ALERTS_TOKEN…)
│ │ │ ├── registre.ts # lecture/écriture YAML + validation Zod
│ │ │ ├── profils.ts
│ │ │ ├── alertes.ts
│ │ │ ├── git.ts # simple-git : commit atomique à chaque écriture
│ │ │ └── recommandation.ts # moteur (pur, testable, sans I/O)
│ │ ├── components/ # Badge, Card, Tag, Icon, DataTable, FormField…
│ │ └── types.ts # types TS partagés (inférés des schémas Zod)
│ └── routes/
│ ├── +layout.svelte # skip-link, nav SSO-aware, flag-bar
│ ├── +layout.server.ts # expose l'utilisateur SSO aux pages
│ ├── +page.svelte # accueil (public minimal si non connecté)
│ ├── +error.svelte # erreurs designées (P5)
│ ├── registre/ # CRUD registre (protégé SSO)
│ ├── profils/ # CRUD profils (protégé SSO)
│ ├── recommander/ # UI moteur de recommandation
│ ├── alertes/ # inbox (phase 2)
│ └── api/
│ ├── recommander/+server.ts
│ └── alerts/+server.ts # POST protégé par token (phase 2)
├── static/
│ ├── fonts/ # Archivo + Inter woff2 self-hébergées + fonts.css
│ ├── theme.js # bascule thème clair/sombre anti-FOUC
│ └── icons.svg # sprite SVG (<symbol>), zéro emoji
├── package.json # private: true
├── svelte.config.js # adapter-node, paths
└── vite.config.ts
```
Règles :
- Tout code qui lit des fichiers, des variables d'env ou fait du fetch sortant vit dans `lib/server/`.
- Le moteur de recommandation est une fonction **pure** (entrées → sorties), testable sans serveur.
- Les données d'exemple/seed vivent dans `app/src/lib/server/seed/` et ne sont copiées dans le
`DATA_DIR` qu'au premier démarrage (jamais d'écrasement de données existantes).
## 3. Conventions Svelte 5 (obligatoires)
- **Runes only.** `$state` uniquement pour du réactif ; gros objets réassignés (réponses API) → `$state.raw`.
- `$derived` pour tout calcul ; `$derived.by` si l'expression est complexe. **Jamais `$effect` pour calculer.**
- `$effect` = escape hatch uniquement (sync avec lib externe), toujours avec cleanup symétrique.
- `$props()` en traitant les props comme changeantes : toute valeur dérivée d'une prop passe par `$derived`.
- Événements : `onclick={...}` (jamais `on:click`) ; listeners globaux via `<svelte:window>` / `<svelte:document>`.
- `{#each}` avec **clé unique** (id métier), jamais l'index ; pas de destructuration si mutation (`bind:`).
- Snippets `{#snippet}` / `{@render}` plutôt que `<slot>`.
- Classes conditionnelles : tableaux/objets style clsx dans `class={...}`, pas de `class:`.
- Liens internes via `resolve()` de `$app/paths` ; assets via `asset()`. Jamais de chemin en dur
(l'app peut être servie sous un sous-chemin YunoHost).
- `{@html}` interdit sauf contenu first-party, commenté comme tel.
- État partagé inter-composants : module `.svelte.ts` avec `$state` + fonctions — pas de stores legacy.
- **Règle des loads :** toute donnée lue côté serveur (YAML, API externe) passe par `+page.server.ts` /
`+layout.server.ts` ou des **form actions** — jamais de fetch direct vers le data_dir côté client.
- Mutations : **form actions SvelteKit** par défaut (progressive enhancement, fonctionne sans JS) ;
`fetch` JSON uniquement pour les endpoints API externes (`/api/*`).
- Persistance de préférences : `?? défaut`, jamais `|| défaut` (un `0`/`false` persisté est légitime).
- Validation systématique via `npx @sveltejs/mcp svelte-autofixer <fichier>` avant de finaliser un composant.
## 4. Style — tokens OKI (charte §2, copie versionnée)
Un seul fichier `src/lib/styles/oki-tokens.css` importé en premier dans `+layout.svelte` (aucune valeur
recopiée à la main dans les composants) :
```css
:root {
/* Noyau (thème sombre = défaut) */
--noir-oki: #0D0D0D; /* background */
--noir-profond: #1A0F1A; /* surface */
--blanc-creme: #FFF8E7; /* foreground */
--line: rgba(255,255,255,0.10);
--or-oki: #FDB813; /* accent : SEULE couleur d'action */
--rouge-oki: #FF1654; /* signal — jamais un lien/bouton */
--vert-oki: #00D66C; /* succès, validation */
--or-clair: #FFE066; /* survol des boutons */
--muted: color-mix(in srgb, var(--blanc-creme) 70%, transparent);
--card-bg: rgba(255,255,255,0.03);
/* Typographie */
--font-display: 'Archivo', 'Arial Black', sans-serif;
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
/* Formes, espacements */
--radius-sm: 3px; --radius-md: 6px;
--border-card: 1px solid var(--line);
--border-btn: 2px solid var(--or-oki);
--space-1: 0.75rem; --space-2: 1rem; --space-3: 1.5rem; --space-4: 2rem; --space-5: 4rem;
--container: 1200px;
/* Motion */
--ease-ka: cubic-bezier(0.22, 1, 0.36, 1);
--ease-syncope: cubic-bezier(0.65, 0, 0.35, 1);
--dur-tanbou: 120ms; --dur-mesure: 480ms; --dur-phrase: 960ms;
}
```
Sémantique des couleurs, adaptée au back-office (rôles **jamais permutés**) :
- **Or = agir** : boutons, liens, focus. Seule couleur d'action.
- **Vert = valider** : statut `production`, licence vérifiée, alerte traitée.
- **Rouge = signaler** : statut `mort`, urgence haute, erreur — jamais cliquable.
- **Badge « ⚠️ à vérifier »** (règle anti-hallucination du PRD) : fond or 8 % + bordure or 1 px
(tag OKI canonique) + texte crème ; rendu par un composant `<BadgeAVerifier>` dédié, utilisé
dès qu'un champ est `null`. Le pictogramme ⚠ fait partie du sprite SVG (règle zéro emoji ci-dessous).
Règles d'usage :
- Thème **sombre par défaut**, clair opt-in via `html.light-theme` posée par `static/theme.js`
(script externe, localStorage, anti-FOUC). Accents assombris en thème clair pour WCAG AA.
- Texte secondaire = crème 7085 % d'opacité ; filets = blanc 10 %, jamais de gris plein.
- Titres h1h4 : Archivo **majuscules**, `letter-spacing: -0.01em`. Boutons : Archivo 700 uppercase,
bordure 2 px or, survol `--or-clair`. Corps : Inter, `line-height: 1.5`.
- Cartes : fond `--card-bg`, liseré gauche 4 px d'accent, rayon ≤ 6 px. Angles nets partout.
- **Flag-bar** (6 px, 4 segments francs noir/or/vert/rouge) sous la navigation — **une occurrence
visible par écran maximum**.
- Iconographie : sprite `static/icons.svg` (`<symbol>` 24×24, stroke 2 px, `currentColor`) +
`<svg><use href="...#id"/></svg>`. **Zéro emoji en production** — y compris dans les badges et
messages d'état (le glyph ⚠ est fourni par le sprite, pas par un emoji Unicode).
- Fonts **self-hébergées** : woff2 dans `static/fonts/`, `@font-face` avec `font-display: swap`,
preload de la display uniquement. Aucun appel Google Fonts.
- Motion : micro-interactions sobres uniquement (`--dur-tanbou` sur hover/focus, `--dur-mesure`
sur transitions de vues). Uniquement `transform` + `opacity`. Durées lues depuis les tokens,
jamais hardcodées. **Gate unique `prefers-reduced-motion`** dans `base.css` (contenu statique complet).
- Styles scopés dans les composants ; personnalisation parent→enfant via custom properties ;
`:global` en dernier recours.
- Layout « app » : `height: 100dvh` + overflow par panneau si pertinent ; cibles tactiles ≥ 44 px.
- Listes longues (registre, inbox) : `content-visibility: auto` + `contain-intrinsic-size` d'abord,
pagination « charger plus » avant toute virtualisation.
## 5. Accessibilité (non négociable)
- Navigation clavier complète : tab order logique, **focus visible** (outline or 2 px), skip-link
« Aller au contenu » en premier élément du layout.
- Contrôles de formulaire tous labellisés (`<label for>`), erreurs de validation annoncées
(`aria-describedby`, `aria-invalid`).
- Contrastes WCAG AA vérifiés sur les deux thèmes (texte courant ≥ 4,5:1).
- Icônes décoratives `aria-hidden="true"` ; icônes seules dans un bouton → `aria-label`.
- Tableaux de données sémantiques (`<th scope>`), tri annoncé via `aria-sort`.
- Focus déplacé sur le `<h1>` après chaque navigation ; `lang="fr"` sur `<html>`.
- Le badge « à vérifier » ne repose jamais sur la couleur seule (pictogramme + texte).
- Zéro warning d'accessibilité svelte-check toléré.
## 6. i18n
- **Français d'abord** : tous les libellés UI en français, regroupés dans `src/lib/i18n/fr.ts`
(objet typé clé → libellé), jamais de chaîne en dur dans les templates.
- Structure prête pour créole/anglais : un fichier par locale, sélection par `data-locale`
(pas de routing i18n au MVP — app monoprivée).
- Microcopy signature en kréyòl admise (404, états vides) avec attribut `lang="gcf"` sur le span.
## 7. Sécurité & intégration YunoHost (côté app)
- **SSO :** `hooks.server.ts` lit le header **`Ynh-User`** injecté par SSOwat via `proxy_params_with_auth`
(conf nginx du package ; le header est vidé côté nginx avant d'être renseigné après authentification,
donc non spoofable). Sans header valide → seule la page d'accueil publique minimale est accessible ;
toutes les routes `/registre`, `/profils`, `/recommander`, `/alertes` et les form actions exigent un
utilisateur. En dev local, `DEV_USER` simule le header (documenté, ignoré en production).
- **CSP via la config SvelteKit** (`kit.csp`, mode `nonce`, dans `vite.config.ts` — SvelteKit ≥ 2.62
n'utilise plus `svelte.config.js`) :
`default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';
object-src 'none'` + `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`.
**Aucun script inline**`theme.js` est un fichier externe.
- **Sous-chemin :** l'app doit fonctionner servie sous `https://domaine.tld/veille` :
`paths.relative: false` dans `svelte.config.js`, base injectée par variable d'env au build/runtime,
et tous les liens via `resolve()`/`asset()`.
- **`POST /api/alerts`** : token `ALERTS_TOKEN` (header `Authorization: Bearer`), généré à l'install
YunoHost, jamais commité. Aucune route mutative sans vérification (SSO ou token).
- Toutes les entrées (YAML, formulaires, webhooks) validées par **Zod** côté serveur avant tout traitement.
- Journaux : `console` structuré (JSON), récupéré par journald via systemd.
## 8. Qualité — définition de « terminé » (chaque commit)
- `npm run check` (svelte-check) : **0 erreur, 0 warning**.
- `npm run lint` (eslint + prettier --check) : propre.
- `npm run test` (vitest) : vert — moteur de recommandation couvert (cas nominaux + cas limites :
licence `null`, seuil de revenus dépassé, modèle statut `mort`, champ `null` pénalisé).
- `npm run build` : vert.
- Grep de vérité avant livraison : zéro domaine tiers dans le build, zéro emoji dans le HTML buildé.
- Revue clavier + `prefers-reduced-motion` sur les pages ajoutées.
- Commits git **atomiques et explicites, en français** ; `README.md` à jour à chaque phase.
## 9. Explicitement hors périmètre (app privée)
- SEO, OG/Twitter cards, sitemap, robots — pas d'indexation, app derrière SSO.
- PWA / service worker / mode hors-ligne.
- Motion riche (Lenis, GSAP, WebGL, kinetic type) — la charte motion OKI s'applique en version sobre (§4).
- Imagerie afrofuturiste, footer fédéré public — interface utilitaire ; le favicon OKI suffit.
- Multi-utilisateurs, notifications push natives (PRD §10).