226 lines
14 KiB
Markdown
226 lines
14 KiB
Markdown
# 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 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` (`<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).
|