# 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 (), 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 `` / ``. - `{#each}` avec **clé unique** (id métier), jamais l'index ; pas de destructuration si mutation (`bind:`). - Snippets `{#snippet}` / `{@render}` plutôt que ``. - 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 ` 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 `` 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 70–85 % d'opacité ; filets = blanc 10 %, jamais de gris plein. - Titres h1–h4 : 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` (`` 24×24, stroke 2 px, `currentColor`) + ``. **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 (`