Files
annu-kute-ced/doc2sveltekit-transition/playbook-oki-sveltekit.md
T

495 lines
41 KiB
Markdown
Raw Normal View History

# PLAYBOOK UNIFIÉ — OKI × SVELTEKIT
### Document de référence opérationnel pour agent de code — marque OKI, méthode Awwwards-grade, conventions Svelte 5, briques réutilisables
> **Nature :** fusion de `charte-oki-design-system.md`, `recette-sveltekit-playbook-agent.md`, `recette-sveltekit-15-sites-awwwards.md`, `svelte-5_code_writer.md`, `svelte_core_bestpractices.md` et `AGENTS.md`, enrichie des leçons réelles de **6 missions** (juillet 2026) : migration o-k-i.net, refonte atlas-fediverse, finalisation exitchatcontrol, migration gwada-sirius, refonte UX/motion du jeu JWE, réparation et modernisation d'oki-podcast-reader (voir §5, §5b et §5c). Document **local, non versionné** (`.gitignore`).
>
> **Usage :** à fournir à l'agent pour toute demande du type « transforme ce projet web en projet Svelte aux couleurs OKI, style Awwwards » ou « crée un site/PWA dans cette charte ». Compléter avec le brief de mission du §7.
>
> **Priorités en cas de conflit :** la marque (§1) prime pour couleurs, typo, voix, iconographie · la méthode (§2) prime pour la technique · les budgets et l'accessibilité ne se négocient jamais.
---
## 0. TYPES DE MISSION
**A. Transformation** d'un site existant (HTML statique, WordPress, Eleventy, SPA sans DA) → SvelteKit aux couleurs OKI. Toujours commencer par l'audit (§2.4 phase 1) — le descriptif du site source est **toujours** à vérifier contre le dépôt réel (la migration o-k-i.net partait d'un brief « HTML statique » alors que le site était Eleventy + 38 JSON i18n).
**B. Création** d'un nouveau site/PWA dans la charte OKI. Commencer au §2.4 phase 2, en copiant les briques du §4.
**C. Finalisation** — le chantier est déjà avancé (migration à 90 %, WIP non commité). Le travail est de **finir, pas recommencer** : builder d'abord, committer l'existant cohérent, puis combler les manques (typiquement SEO et régressions du changement de générateur).
**D. Refonte ciblée UX/motion** — la stack est déjà SvelteKit. Le travail est chirurgical : remplacer les pièces défaillantes (fond, carte, timeline, layout) et appliquer charte + motion, sans tout réécrire.
Quatre règles transverses, apprises sur 5 projets :
1. **Contenu éditorial jamais réécrit** sans instruction explicite ; les données priment sur les suppositions (ex. KUTE = Castopod dans les JSON, pas « app PHP » comme le brief le supposait).
2. **Toujours vérifier l'existence de git en premier** (atlas-fediverse n'avait AUCUN dépôt — `git init` + commit de l'existant avant toute modification, sinon travail irréversible).
3. **Charte §7 « harmoniser ≠ tout refaire »** : si le projet a déjà un design system de la famille OKI (tokens panafricains, polices accessibilité), on ne rebrande pas — on applique méthode et qualité (exitchatcontrol, gwada-sirius). La charte complète ne s'applique qu'aux projets sans identité propre (atlas).
4. **Vérifier l'état réel avant de scoper** : sur 5 projets, 2 étaient déjà SvelteKit (atlas, JWE), 1 en migration à 90 % (exitchatcontrol), 1 à 80 % (gwada-sirius) — une seule vraie migration complète.
---
## 1. MARQUE OKI (canonique — prime sur tout le reste)
### 1.1 Identité en une phrase
Un studio web militant caribéen dont la marque est un **drapeau panafricain — noir, or, vert, rouge — posé sur un fond presque noir, en capitales Archivo**. Afrofuturiste caribéen : un drapeau, pas une charte corporate.
Trois conséquences non négociables :
1. **Thème sombre = identité par défaut** ; le clair est un opt-in (`html.light-theme`) qui assombrit les accents pour WCAG AA.
2. **L'or porte toute l'interaction** — seule couleur d'action.
3. **Angles nets partout** — aucune forme organique, aucune bulle très arrondie.
### 1.2 Tokens couleurs (mesurés sur le site réel)
```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); /* filets — jamais de gris plein */
--or-oki: #FDB813; /* accent : SEULE couleur d'action */
--rouge-oki: #FF1654; /* signal, ponctuation — jamais un lien/bouton */
/* Étendue (parcimonie) */
--vert-oki: #00D66C; /* succès, validation, dons mensuels */
--turquoise-caraibes: #00CED1;
--jaune-soleil: #FFD700;
--orange-flamme: #FF6B35;
--violet-nuit: #6B2D5C;
--bleu-ocean: #0077BE;
--or-clair: #FFE066; /* survol des boutons */
--gris-sombre: #2D1B2E;
/* Sémantique dérivée */
--muted: color-mix(in srgb, var(--blanc-creme) 70%, transparent);
--card-bg: rgba(255,255,255,0.03);
}
/* Thème clair — opt-in, accents assombris WCAG AA */
html.light-theme {
--noir-oki: #FFF8E7; --blanc-creme: #0D0D0D;
--noir-profond: #F5F0E8; --gris-sombre: #E8DDD0;
--or-oki: #B87A00; --vert-oki: #006B3D; --rouge-oki: #A01030;
--turquoise-caraibes: #006B75; --bleu-ocean: #004B7F;
--or-clair: #D99000; --jaune-soleil: #B87A00; --orange-flamme: #C85000;
--line: rgba(0,0,0,0.12); --card-bg: rgba(0,0,0,0.03);
}
```
Règles d'usage : texte secondaire = crème à 70-85 % (`--muted`), pas de couleur dédiée · filets = blanc 10 % · **Rouge = signaler · Vert = valider · Or = agir — ne jamais permuter**.
### 1.3 Typographie
```css
--font-display: 'Archivo', 'Arial Black', sans-serif; /* 600-900 : TITRES + BOUTONS */
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; /* 400-800 */
```
- Titres h1-h4 : Archivo, MAJUSCULES, `letter-spacing: -0.01em`.
- Boutons : Archivo 700, uppercase, `letter-spacing: 0.03em`, bordure 2 px.
- Corps : Inter, `line-height: 1.5`.
- **Self-hébergement obligatoire** : woff2 dans `static/fonts/` + `fonts.css`, `font-display: swap`, preload de la display 700. Aucun appel Google Fonts (doctrine souveraineté). Source pratique : `@fontsource/archivo` + `@fontsource/inter` (npm), copier les `*-latin-*-normal.woff2`.
### 1.4 Formes, espacements, layout
```css
--radius-sm: 3px; --radius-md: 6px; /* jamais plus */
--border-card: 1px solid var(--line);
--border-btn: 2px solid var(--or-oki);
--space-1: .75rem; --space-2: 1rem; --space-3: 1.5rem; --space-4: 2rem; --space-5: 4rem;
--container: 1200px;
```
- Sections : `padding: var(--space-5) 0` · Grilles : `repeat(auto-fit, minmax(300px, 1fr))`.
- **Carte canonique** : fond `--card-bg`, liseré gauche 4 px d'accent, survol `translateY(-2px à -4px)` + bordure d'accent.
- **Tag canonique** : fond or 8 % + bordure or 1 px, rayon 3 px.
- **Bouton canonique** : balayage or `::after scaleX(0→1)` origin left au hover, `--ease-syncope`.
### 1.5 Flag-bar — signature n°1
Bandeau **6 px, 4 segments francs** : noir 0-25 % · or 25-50 % · vert 50-75 % · rouge 75-100 %. Arrêts nets, **jamais de dégradé entre segments**.
```css
.flag-bar { height: 6px; background: linear-gradient(to right,
#0D0D0D 0 25%, var(--or-oki) 25% 50%, var(--vert-oki) 50% 75%, var(--rouge-oki) 75% 100%); }
```
Placement : bord inférieur de la nav fixe + haut du footer. **Une occurrence pleine largeur visible par écran max.** Pour ponctuer ailleurs (titres de section, loader), utiliser le **flag chip** : même 4 segments, 56 × 6 px (voir §4). En thème clair, le segment noir devient `#000` pur.
### 1.6 Logotype
Monogramme « OKI » : arche or, point vert, accent rouge sur noir. Fichier canonique 512×512 transparent (aussi og:image, apple-touch-icon, maskable PWA). Ne jamais utiliser un logo de partenaire comme logo OKI.
### 1.7 Motion — les cadences gwoka
```css
--ease-ka: cubic-bezier(0.22, 1, 0.36, 1); /* temps fort : attaque franche, longue tenue */
--ease-syncope: cubic-bezier(0.65, 0, 0.35, 1); /* contretemps : symétrique, urgent */
--dur-tanbou: 120ms; /* double-croche — micro-interactions, hovers */
--dur-mesure: 480ms; /* une mesure — entrées de section */
--dur-phrase: 960ms; /* deux mesures — transitions de page, moments solennels */
```
1. **Le stagger de marque est syncopé (3+3+2)** : délais `[0, 120, 300, 360, 600, 660, 900, 960]` ms pour 8 éléments (puis cycles de +1200 ms). Le rythme est une identité, pas un effet.
2. Uniquement `transform` + `opacity` (+ `clip-path` pour les masques).
3. **Gate unique `prefers-reduced-motion`** au niveau global, contenu statique complet sans JS ni motion.
4. Durées toujours lues depuis les tokens, jamais hardcodées.
### 1.8 Iconographie — zéro emoji en production
Set SVG OKI : viewBox 24×24, **stroke 2 px**, angles nets, `currentColor` (or par défaut), remplissage réservé aux ≤ 16 px, **noms kréyòl**. Livré en `<symbol>` dans un sprite `icons.svg` + `<svg class="icon"><use href="/icons.svg#ID"/></svg>`.
Set de référence (existant dans o-k-i.net) : `ka` (tambour, icône maîtresse, 404/loader) · `lambi` (conque — annonces) · `zetwal` (étoile 4 branches — navigation, instances) · `mawon` (brisure de chaîne — souveraineté) · `lakanmou` (flamme — engagement, dons) · `jaden` (pousse — solidarité, projets) · `kannen` (canne — patrimoine, institutions) · `lanme` (vague — international) · `glo` (poing — luttes, tarifs solidaires) · `kle` (cadenas ouvert — liberté, open-source) · `pawol` (bulle angulaire — langues, traduction) · `mizik-note` (note — musique).
**Pattern contenu** : si des emojis vivent dans les textes (JSON), utiliser un helper `splitLeadingEmoji()` qui convertit l'emoji décoratif en picto du sprite sans toucher au texte (implémenté dans o-k-i.net `sections/pictos.ts`).
### 1.9 Voix & ton
Vouvoiement direct et empathique, phrases courtes. La cause du visiteur d'abord, l'outil ensuite. **À dire :** sur mesure · tarifs adaptés à vos moyens · organisations engagées · transition numérique · écosystème numérique libre. **À éviter :** « template générique », « clé en main », jargon corporate, startup-speak. Piliers : ORGANISATION KA · INTERNATIONALE · Des solutions pour chaque besoin · Le budget ne doit jamais être un frein. **Langues :** FR par défaut ; KA (kréyòl) en signature et microcopy (404, loader, remerciements — attribut `lang="gcf"`) ; EN en version dédiée.
### 1.10 Imagerie
Illustration afrofuturiste caribéenne : couleurs chaudes saturées sur fonds sombres. Jamais de stock corporate, jamais d'illustration startup générique. AVIF/WebP self-hébergés, `loading="lazy"` hors LCP.
### 1.11 Écosystème
- Nom KA + logiciel en sous-titre (« BOKANTE — Mastodon ») : la transparence est un pilier.
- Footer fédéré sur tous les sous-domaines : `[monogramme] Un service libre opéré par ORGANISATION KA INTERNATIONALE · o-k-i.net` + flag-bar 6 px au-dessus.
- Favicon = monogramme partout. `*.o-k-i.net` = outils/fédivers ; `pawol.nu` = projets culturels (motion plus riche autorisé).
---
## 2. MÉTHODE TECHNIQUE (la recette)
### 2.1 Les 5 principes
- **P1 — Le scroll est le moteur.** Une seule instance Lenis au layout racine, cadencée par le ticker GSAP unique. Jamais de double rAF.
- **P2 — Bake au build, pas au runtime.** Tout ce qui ne dépend pas d'une entrée live est précalculé : images responsive (vite-imagetools), textures, polices.
- **P3 — DOM d'abord, WebGL seulement où ça paie.** Zentry a fait du « 3D » primé en pur DOM (clip-path, preserve-3d, masks). Pour l'audience OKI (mobile 4G, Mali-G52) : **DOM/SVG uniquement** par défaut.
- **P4 — Le motion se designe avant les pages** (tokens §1.7 d'abord, composants ensuite).
- **P5 — Chaque état est un moment designé** : loader, 404, vide, hors-ligne — en KA avec le tambour `ka`.
### 2.2 Interdictions absolues
- Jamais de dépendance installée sans être importée et utilisée (les outils d'audit — lighthouse, puppeteer — s'installent en `--no-save` ou se retirent).
- Jamais de SSR désactivé globalement pour « faire marcher » une lib client.
- Jamais animer autre chose que `transform`/`opacity`/`clip-path` en JS.
- Jamais de page dont le contenu est invisible sans JavaScript.
- Jamais de service tiers (fonts, CDN, analytics) — tout asset se self-héberge.
### 2.3 Stack cible
```
SvelteKit 2 + Svelte 5 (runes) + TypeScript strict
@sveltejs/adapter-static (prerender intégral, trailingSlash 'always')
vite-imagetools (AVIF/WebP responsive au build)
vite-plugin-pwa (generateSW — shell hors-ligne)
lenis + gsap (imports dynamiques uniquement)
@fontsource/* (polices woff2 copiées dans static/fonts/)
CSS vanilla : oki-tokens.css + base.css + styles scopés — pas de Tailwind
```
### 2.4 Workflow en 7 phases
1. **Audit** (toujours) : pages, sections, assets, liens externes, stack mesurée, bugs de production listés AVANT toute refonte, contradictions doctrine/outillage (ex. discours anti-GAFAM + Google Fonts). Rapport validé avant de coder.
2. **Tokens & thème** : `oki-tokens.css` (§1), thème sombre défaut + clair opt-in, fonts self-hébergées, anti-FOUC par script externe (CSP).
3. **Architecture** : routes, i18n, SEO, layout. `prerender = true` partout. URLs historiques conservées.
4. **Composants** : uniquement ceux pertinents pour CE site. Chacun avec cleanup et garde reduced-motion.
5. **Motion** : transitions Svelte natives · scrub via `animation-timeline: view()/scroll()` natif d'abord, fallback GSAP ScrollTrigger en import dynamique · une seule horloge (gsap.ticker → Lenis).
6. **Performance** : budgets §2.7, pipeline images, mesure sur profil mobile.
7. **Accessibilité & dégradation** : ladder §2.8, revue clavier, contenu canvas/SVG doublé en DOM.
### 2.5 Architecture de référence (éprouvée sur o-k-i.net)
```
src/
app.html # %lang% + scripts externes (theme, lang-redirect, registerSW)
hooks.server.ts # lang fr/en via transformPageChunk (replaceAll '%lang%')
app.d.ts / imagetools.d.ts
lib/
styles/oki-tokens.css # charte §1 — LE fichier de tokens partagé
styles/base.css # reset, primitives (btn, card, tag, flag-bar, icon), gate reduced-motion
i18n/ # bundles JSON par locale + index.ts (getBundle, localeFromPath, alternatePath)
assets/images/ # sources pour vite-imagetools
motion/ # tokens.ts, scroll.ts (Lenis), reveal.ts, tilt.ts
components/ # Seo, Nav, Footer, ResponsiveImage, motion/, icons/, sections/
routes/
+layout.svelte # skip-link, loader, progress, nav, footer, View Transitions, Lenis
+layout.ts # prerender = true, trailingSlash = 'always'
+error.svelte # 404/erreurs designée (KA)
offline/+page.svelte # cible navigateFallback du SW
static/
fonts/ icons.svg images/ theme.js lang-redirect.js registerSW.js
manifest.webmanifest 404.html robots.txt sitemap.xml _headers .htaccess
```
### 2.6 Composants motion (contrats)
- **KineticText** — titrage au scroll, split par **mots** (jamais caractères — apostrophes), `aria-label` sur titres h1-h4 (jamais sur span : prohibé), scrub `animation-timeline: view()` + fallback ScrollTrigger dynamique, stagger syncopé converti en plages de scroll.
- **ScrollProgressBar** — `scaleX` via rAF + écriture DOM directe (jamais d'état réactif par frame), `role="progressbar"`.
- **PageTransition** — `onNavigate` + View Transitions API, durée lue des tokens ; fallback sobre = navigation instantanée.
- **IntroLoader** — cérémonie 1re visite (`sessionStorage`), skippable, 100 % CSS pilotée par une classe posée par le script externe anti-FOUC (pas de JS inline — CSP).
- **Marquee** — bande typographique CSS pure, contenu dupliqué `aria-hidden`, pause au hover, coupée en reduced-motion.
- **use:reveal** — action IO, cascade syncopée via `--d`, état caché **uniquement** sous `html.js` + `prefers-reduced-motion: no-preference`.
- **use:tilt** — tilt 3D pointeur via variables CSS `--rx/--ry` + rAF, off tactile + reduced-motion.
- **FlagChip** — mini flag-bar 56×6 px qui se dessine (`scaleX`) à l'entrée du titre.
- **Village écosystème** — scène SVG isométrique des services, chaque bâtiment = `<a>` focusable (`aria-label` « NOM — Logiciel »), hover/focus = liseré or + label, fallback grille accessible (reduced-motion + mobile étroit, bascule `display: none` pour éviter les doubles tab stops).
- **CursorTracker, WebGL** : non retenus pour l'audience OKI (mobile-dominante).
### 2.7 Budgets (mobile 4G, Android entrée de gamme)
| Ressource | Budget | Mesuré o-k-i.net |
|---|---|---|
| JS initial compressé | ≤ 170 Ko | 69 Ko gzip (+45 Ko dynamiques) |
| Poids total accueil | ≤ 2 Mo | ~0,9 Mo |
| Média hero avant interaction | ≤ 400 Ko | ~12 Ko (logo) |
| Lighthouse mobile | ≥ 90/95/95/95 | 91/100/100/100 |
| Animations JS concurrentes | ≤ 8-12, transform/opacity | OK |
| LCP / INP / CLS | < 2,5 s / < 200 ms / < 0,05 | 2,9 s / 59 ms TBT / 0,001 |
### 2.8 Accessibilité & ladder de dégradation
1. `saveData`/`effectiveType` ≤ 3g → images statiques, zéro préchargement.
2. JS désactivé → contenu SSR complet et lisible (états cachés conditionnés à `html.js`).
3. `prefers-reduced-motion` → gate unique : pas de Lenis, pas de loader, pas de scrub, contenu statique complet.
4. Canvas/SVG décoratifs `aria-hidden` + miroir DOM sémantique.
5. `lang` correct à chaque bascule ; focus déplacé sur le contenu après transition de route ; skip-link ; focus visible or.
6. Pièges éprouvés : `aria-label` interdit sur `<span>` générique · `<figcaption>` enfant direct de `<figure>` · liens dans le texte soulignés (scoper la règle à `main` pour ne pas toucher nav/footer).
### 2.9 PWA légère
- `static/manifest.webmanifest` manuel (nom KA, monogramme any + maskable, `theme_color`/`background_color` `--noir-oki`, `display: standalone`).
- `vite-plugin-pwa` : `registerType: 'autoUpdate'`, `injectRegister: false`, `manifest: false`, workbox `navigateFallback: '/offline/index.html'`, `navigateFallbackDenylist` pour les assets binaires, `globIgnores` pour les gros fichiers (PDF), `maximumFileSizeToCacheInBytes: 3 Mo`, `cleanupOutdatedCaches`, `clientsClaim`, `skipWaiting`.
- Enregistrement par **fichier statique** `static/registerSW.js` avec chemins absolus (`navigator.serviceWorker.register('/sw.js', { scope: '/' })`), lié dans `app.html`. Ne PAS utiliser l'injection du plugin : sans `@vite-pwa/sveltekit` elle n'atteint pas le HTML pré-rendu, et le `registerSW.js` généré utilise un chemin relatif `./sw.js` cassé sur les routes imbriquées (`/en/`, `/dons/`).
### 2.10 Sécurité & CSP
CSP de référence (tout self-hébergé) :
```
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'
```
+ HSTS `max-age=31536000`, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy` restrictive. Livrée en **deux formats** : `static/.htaccess` (Apache/o2switch : + `ErrorDocument 404 /404.html`, redirect HTTPS, cache immutable `/_app/` et `/fonts/`) et `static/_headers` (Cloudflare Pages). Règles : **aucun script inline** (snippets thème/langue en fichiers externes) ; `style` attributes OK (`unsafe-inline` style) ; 404 statique autonome (`static/404.html`, zéro dépendance).
### 2.11 SEO
Composant `Seo.svelte` : title/description par page, canonical, hreflang fr/en/x-default via mapping de paires d'URLs, OG ×10 (og:image self-hébergée), Twitter Card, JSON-LD (Organization + entités du projet). `robots.txt` + `sitemap.xml` statiques à jour. Astuce : les `</script>` dans les template literals `{@html}` doivent être échappés (`<\/script>`).
---
## 3. CONVENTIONS SVELTE 5 (obligatoires)
- **Runes only** : `$state` (uniquement pour du réactif — sinon variable plain ; gros objets réassignés : `$state.raw`), `$derived` (jamais d'`$effect` pour calculer), `$props` (traiter les props comme changeantes : valeurs dépendantes en `$derived`).
- **Événements** : `onclick={...}` (jamais `on:click`) ; listeners window/document via `<svelte:window>` / `<svelte:document>`.
- **`{#each}` à clé unique** (jamais l'index).
- **CSS** : variables JS → directive `style:--var={val}` ; styles enfants via custom properties, `:global` en dernier recours ; états globaux (`html.js`, `html.light-theme`, `html.intro-pending`) via `:global(...)`.
- **États réactifs par-frame interdits** : scrollY/pointeur → variables plain + flush rAF (ou variables CSS directement).
- **Init/cleanup** : GSAP/Lenis/IO dans `onMount`/`$effect` avec cleanup symétrique (`kill()`/`revert()`/`destroy()`/`disconnect()`).
- **Liens internes** : `resolve()` / assets via `asset()` de `$app/paths` ; externes en `rel="external noopener noreferrer"` + `target="_blank"`.
- **{@html}** : uniquement sur contenu first-party (JSON du dépôt), commenté comme tel.
- **Autofixer (AGENTS.md)** : après tout composant modifié —
```bash
npx @sveltejs/mcp list-sections # doc Svelte 5
npx @sveltejs/mcp get-documentation "$state,$derived"
npx @sveltejs/mcp svelte-autofixer ./src/lib/MonComposant.svelte
```
(Échapper `\$` dans le code inline. La sortie est un objet JS, pas du JSON strict.) `npm run check` à **0 erreur / 0 warning** avant tout commit.
---
## 4. BRIQUES RÉUTILISABLES (référence : dépôt o-k-i.net, branche `svelte`)
Copier et adapter, ne pas réécrire. Chaque brique a un contrat stable.
| Brique | Chemin dans o-k-i.net | Contrat / usage |
|---|---|---|
| Tokens charte | `src/lib/styles/oki-tokens.css` | importé en premier dans `+layout.svelte` |
| Primitives | `src/lib/styles/base.css` | `.btn .card .tag .icon .flag-bar .container .section`, gate reduced-motion, reveal |
| Fonts | `static/fonts/` + `fonts.css` | Archivo 600-900, Inter 400-800, woff2 latin, preload 700 |
| Anti-FOUC thème | `static/theme.js` | pose `.js`, `.light-theme`, `.intro-pending` ; localStorage `oki-theme` |
| Redirection langue | `static/lang-redirect.js` | préférence `oki-lang-pref`, sinon `navigator.language` |
| i18n | `src/lib/i18n/index.ts` | bundles JSON par locale ; `getBundle(locale)`, `localeFromPath`, `alternatePath` (paires FR↔EN) |
| SEO | `src/lib/components/Seo.svelte` | `<Seo title? description? path locale />` — meta + OG + Twitter + JSON-LD |
| Lang par route | `src/hooks.server.ts` | `%lang%` dans `app.html` + `transformPageChunk` |
| Lenis | `src/lib/motion/scroll.ts` | `initSmoothScroll()` (dynamic), `scrollToAnchor(hash)` |
| Tokens motion | `src/lib/motion/tokens.ts` | `prefersReducedMotion()`, `syncopatedDelay(i)` (3+3+2) |
| Reveal | `src/lib/motion/reveal.ts` | `use:reveal={index}` — cascade gwoka |
| Tilt | `src/lib/motion/tilt.ts` | `use:tilt` — CSS `--rx/--ry` |
| KineticText | `components/motion/KineticText.svelte` | `<KineticText text as="h2" class="section-title" />` |
| ScrollProgressBar | `components/motion/ScrollProgressBar.svelte` | global, gradient or→vert |
| IntroLoader | `components/motion/IntroLoader.svelte` | cérémonie 1re visite, skippable |
| Marquee | `components/motion/Marquee.svelte` | `<Marquee {t} />` |
| FlagChip | `components/motion/FlagChip.svelte` | ornement de titre, dessin au scroll |
| Images | `components/ResponsiveImage.svelte` + `imagetools.d.ts` | `import pic from './x.png?format=avif;webp;png&w=…&as=picture'` → `<ResponsiveImage picture={pic} alt />`. **Le wildcard `declare module '*&as=picture'` doit vivre dans un `.d.ts` NON-module** |
| Sprite OKI | `static/icons.svg` | 12 pictos KA (§1.8) — `<use href="/icons.svg#ID">` |
| Logos marques | `components/icons/BrandIcon.svelte` | `<BrandIcon name="mastodon|peertube|nextcloud|gitea|castopod|discord|telegram|whatsapp|email|stoat|tiktok|x" />` |
| Village | `components/sections/HostingVillage.svelte` | scène SVG écosystème + fallback grille |
| Nav/Footer | `components/Nav.svelte`, `Footer.svelte` | nav active (IO), dropdown accessible, thème, langue ; footer fédéré |
| Emojis→pictos | `components/sections/pictos.ts` | `splitLeadingEmoji(str)`, `pictoFor(emoji)` |
| Bridge tokens | `atlas-fediverse/src/styles/oki-bridge.css` | alias des anciens noms de tokens vers la charte — applique la charte à un projet existant **sans retoucher chaque composant** |
| Layout baké | `atlas-fediverse/scripts/build-layout.mjs` | d3-force en script node → positions déterministes figées en JSON (init hashée FNV-1a + ticks fixes = bit-identique). Rejouer quand les données changent |
| État partagé inter-sections | `atlas-fediverse/src/lib/software-modal.svelte.ts` | module `$state` + `requestX()/consumeX()` : ouvrir une modale d'un composant depuis une autre section (timeline → catalogue) |
| Layout app 100dvh | `JWE/app/src/lib/styles/jeu.css` | `height: 100dvh` (pas min-height) + overflow par panneau + overlays consolidés (une puce de statut, rangées de boutons hors de la carte, barre d'actions opaque z-indexée) — zéro scroll, zéro superposition |
| Count-up score | `JWE/app/src/lib/motion/countup.ts` | `use:compte={cible}` : rAF → écriture DOM directe, span animé `aria-hidden` + valeur finale en `.sr-only` (région `aria-live`) |
| Reveal SSR | `JWE/app/src/lib/motion/reveal.ts` | variante dont l'état initial est rendu côté serveur, masqué uniquement sous `html.js` + `no-preference` |
| i18n dossiers | `gwada-sirius/src/lib/i18n/index.ts` | catalogues au format Paraglide/inlang consommés par un routeur maison (pas de middleware) — statique-friendly, l'outillage inlang reste utilisable |
| Support volume iOS | `svelte-podcast/src/lib/volume-support.ts` | `can_set_volume()` : sonde l'écriture de `volume` sur un élément jetable (mémoïsée) — détecte iOS par capacité, pas par UA sniffing |
| PWA | `vite.config.ts` (bloc VitePWA) + `static/registerSW.js` + `static/manifest.webmanifest` | voir §2.9 |
| Headers | `static/.htaccess` + `static/_headers` | voir §2.10 |
| 404 / offline | `static/404.html` + `routes/+error.svelte` + `routes/offline/` | KA + tambour `ka`, autonomes |
---
## 5. LEÇONS DE LA MIGRATION o-k-i.net (pièges déjà résolus — ne pas les ré-apprendre)
1. **Toujours auditer le dépôt réel** : le brief disait « HTML statique », c'était Eleventy + i18n JSON. Le contenu vivait dans les données — les reprendre, pas réécrire.
2. **Les données priment sur le brief** : le village suit `services.json` (4 instances), pas les exemples du brief ; la section cible se décide d'après le contenu JSON, pas le titre supposé.
3. **vite preview meurt si l'on rebuilde pendant qu'il tourne** (il sert `.svelte-kit/output`) — redémarrer après chaque build.
4. **CSP `script-src 'self'`** : anti-FOUC, lang-redirect et registerSW en fichiers externes statiques ; aucun inline.
5. **Preview headless** : `--virtual-time-budget` fige les animations d'entrée (faux négatifs visuels) — valider le motion en temps réel via puppeteer (`--no-save`), pas en screenshot one-shot.
6. **Puppeteer QA** : `page.emulateMediaFeatures` pour reduced-motion ; le tilt se teste par `dispatchEvent(PointerEvent)` (le scroll Lenis décale `mouse.move`).
7. **eslint/autofixer `no-navigation-without-resolve`** : `resolve()` pour routes internes, `asset()` pour fichiers statiques, `rel="external"` pour sortir du scope de la règle — sans jamais envelopper une URL externe ou une ancre `#`.
8. **Grep de vérité** avant de livrer : zéro domaine tiers dans `build/` (hors liens `<a>` métier), zéro emoji dans le HTML buildé, toutes les URLs en 200, `og:` ×10, JSON-LD présents.
9. **Lighthouse après chaque passe** (mobile) et corriger ce qui est remonté — les deux points gagnés sur o-k-i.net : `aria-label` sur span (KineticText) et liens non soulignés.
10. **Lighthouse `link-in-text-block`** : scoper `text-decoration: underline` à `main` — sinon la nav et le footer héritent de soulignés partout.
## 5b. LEÇONS DES MISSIONS atlas / exitchatcontrol / gwada-sirius / JWE
**Gouvernance & process**
1. **git d'abord** : atlas n'avait aucun dépôt — `git init` + commit de l'existant avant toute ligne de code.
2. **WIP non commité d'autrui** : builder d'abord ; si c'est cohérent, committer tel quel, puis améliorer par commits séparés (exitchatcontrol).
3. **Orchestration d'agents parallèles** : sérialiser tout ce qui touche `package.json` / npm (un seul agent ou le parent) ; définir les contrats (props des composants, noms de tokens, chemins) AVANT de lancer — les agents s'intègrent alors sans conflit, et découvrent même le travail des autres (module `software-modal` réutilisé en vol).
4. **Conflits de ports** : avant tout audit automatisé, vérifier ce qui tourne (`vite preview` d'un autre projet sur le même port a faussé un audit axe — vérifier le `<title>` servi).
**Technique SvelteKit**
5. **`paths.relative: false`** quand `paths.base` est utilisé : par défaut `$app/paths.base` vaut `".."` au prerender, ce qui casse toutes les comparaisons de pathname (`startsWith(base)`, switcher de langue, `aria-current`) et produit des hrefs relatifs bizarres dans le HTML prérendu. L'hydratation masque le bug — le vérifier dans le HTML buildé, pas seulement au clic.
6. **`lang` par route avec base path** : retirer `base` dans `hooks.server.ts` AVANT de déduire la locale du premier segment.
7. **i18n sans middleware** : un routeur par dossiers (`/`, `/en/`, `/ht/`) + loader JSON maison bat Paraglide-officiel pour le statique (URLs et prerender sous contrôle total) tout en gardant les catalogues au format inlang.
8. **Îlots → composants natifs** : le montage manuel (`mount.js` + IntersectionObserver) disparaît ; le fallback SSR (table, liste) devient le markup même du composant — meilleur pour no-JS ET pour le CLS.
9. **Leaflet sous SSR** : `await import('leaflet')` dans `onMount` (l'import statique plante côté Node), CSS en import statique OK, `L.divIcon` CSS au lieu d'images de marqueurs. Tuiles OSM = exception documentée au zéro-tiers, à faire trancher par le propriétaire.
10. **CSP d'un jeu/app riche** (adapter-node → `hooks.server.ts`) : énumérer les domaines réels en inspectant le code ET les node_modules (Mapillary a besoin de `graph.` + `tiles.` + `images.mapillary.com`, Wikimedia d'`upload.` + `commons.`) — puis jouer une manche complète en capturant la console : zéro violation exigée.
11. **Layout « app » sans scroll** : `height: 100dvh` (jamais `min-height`) + overflow géré par panneau + indices dans des panneaux scrollables internes. Les bugs de superposition mobile viennent d'overlays absolus empilés : consolider les badges en une puce, sortir les rangées de boutons de la carte, barre d'actions opaque avec `z-index` explicite, `pointer-events: none` sur tout conteneur décoratif. Tester chaque bouton par `elementFromPoint`.
12. **Le « moment de révélation »** (jeux) : ligne de distance dessinée + score en count-up + marqueur qui tombe. Count-up : rAF → DOM direct, jamais d'état réactif par frame ; span animé `aria-hidden` + valeur finale `.sr-only` dans une région `aria-live`.
13. **CSP hashée** (script postbuild qui hashe les inline) : un JSON-LD inline ajouté est hashé automatiquement — mais vérifier le compte d'empreintes après build.
14. **Icônes PNG lourdes** : `PIL.Image.quantize(256)` suffit pour des logos (121 Ko → 9 Ko, perte invisible) — toujours relire l'image après.
**Contenu & données**
15. **Audit factuel des données** en plus de l'audit technique : la timeline d'atlas contenait 4 dates fausses et un compteur périmé (Twitter 2021→2022, Mastodon 2015→2016, PeerTube 2015→2017, Bluesky 2023→2024, « 55+ »→105). Les champs de données inutilisés (`month`, `softwareId`) sont souvent des fonctionnalités gratuites.
16. **Régressions SEO typiques au changement de générateur** : sitemap, OG hors fiches, og:image, canonical — checklist à passer systématiquement (exitchatcontrol).
**Déploiement YunoHost**
17. Le package ne consomme que des **archives de tag** : taguer l'app (`vX.Y.Z`), pousser le tag, télécharger l'archive `/archive/<tag>.tar.gz` depuis la forge, recalculer son sha256, mettre à jour `manifest.toml` (version `X.Y.Z~ynhN` + url + sha256), commit/push le package. `autoupdate.strategy = "latest_forgejo_tag"` détecte les tags suivants.
## 5c. LEÇONS DE LA MISSION oki-podcast-reader (lecteur audio)
**Gouvernance & forks**
1. **Sécuriser le WIP AVANT tout** : toute la transformation applicative était non commitée sur un fork — premier commit de protection avant la première analyse détaillée (règle générale : `git add -A && git commit "WIP sécurisé"` dès qu'un working tree contient du travail non versionné).
2. **Fork de librairie → application** : purger la dette de packaging (champs `exports`/`files`/`peerDependencies`, changesets, publint, workflows CI de la lib) — le `package.json` d'une app est `private: true` et nu.
3. **Le remote `origin` d'un fork pointe chez l'auteur original** : le renommer `upstream` avant d'ajouter le remote de déploiement, sinon le push part chez l'original (échoue ou pire).
**Upgrades de framework**
4. **Le mode compat Svelte 5 est une voie complète** : SvelteKit 1→2 + Svelte 4→5 SANS réécriture en runes — la syntaxe legacy (`on:click`, `$:`, `export let`, `<slot>`) compile à 0 erreur/0 warning. Upgrader les paquets, traiter uniquement les breaking changes (imports `vitePreprocess`, options TS supprimées, sérialisation), ne JAMAIS réécrire la syntaxe dans la même passe. La réécriture en runes est une mission séparée, facultative.
5. **Respecter le gestionnaire de paquets du projet** (yarn.lock ≠ package-lock.json) — ne pas mélanger.
**Règle d'architecture des loads (la plus généralisable)**
6. **Universal load (`+page.ts`) = le corps brut des fetch est inliné dans le HTML** : un flux RSS de 10 Mo produisait une page de 11,9 Mo. **Server load (`+page.server.ts`) garde le fetch côté serveur** — règle d'un coup : bloat supprimé (88 %), ET les problèmes CORS en navigation client disparus (le navigateur ne refait jamais le fetch : il lit `__data.json`). Toute donnée tierce fetchée appartient à `+page.server.ts`. Corollaire : les pages qui en dépendent ne buildent pas hors-ligne — à documenter.
7. Les redirections CORS se contrôlent à chaque saut : une 302 sans `Access-Control-Allow-Origin` tue le fetch navigateur même si la destination finale l'autorise — raison de plus pour fetcher côté serveur.
**Audio / média**
8. **Les préférences média persistées doivent être re-appliquées en continu** (souscription), pas seulement au chargement de la source — et `?? 1`, jamais `|| 1` (un 0 persisté est une valeur légitime).
9. **`HTMLMediaElement.volume` est en lecture seule sur iOS** : détecter par capacité (sonder l'écriture sur un élément jetable, mémoïser), masquer le slider, garder le mute (`el.muted`, lui, fonctionne). Source de vérité unique dans le store persisté, synchronisée dans les deux sens.
10. **Cibles tactiles sur les lecteurs** : épaisseur visuelle ≠ zone de hit (piste fine de 10 px OK si la zone fait 44 px). Un `<input type="range">` ne repositionne PAS le thumb au tap tactile — handlers `touchstart`/`touchmove` explicites requis (tester avec `page.tap` + `hasTouch`).
11. **Media Session API est non négociable pour une app audio** (contrôles écran verrouillé) : `metadata` à chaque piste + handlers `play`/`pause`/`previoustrack`/`nexttrack`, gardé par `'mediaSession' in navigator`.
12. **SW d'une app audio** : ne JAMAIS intercepter les streams (requêtes `Range`, extensions média) — précache shell, cache-first sur pochettes uniquement.
**Perf de listes**
13. **`content-visibility: auto` + `contain-intrinsic-size`** : le gain de perf de listes le moins cher qui existe (CSS seul). Avant toute virtualisation : pagination « charger plus » 50×50 (1022 lignes DOM → 50).
---
## 6. PATTERNS AWWWARDS (distillé des 15 sites — quoi voler, à quel coût)
| Pattern (source) | Coût | Statut pour OKI |
|---|---|---|
| DOM-only masks/clip-path « 3D » (Zentry) | 5 % du coût GPU | **Adopté** (wipes pochettes) |
| Motion system spec'd avant les pages (Zentry, P4) | 0 | **Adopté** (tokens gwoka) |
| Hover craft budgété — 5 interactions intentionnelles (Noomo) | faible | **Adopté** (balayage boutons, tilt, soulignés, village) |
| Loader-as-fiction (KPR) | faible | **Adopté** (IntroLoader) |
| États designés : 404, vide, offline (Studio375) | faible | **Adopté** (KA + tambour) |
| Dollhouse IA — sections = lieux physiques (Kriss.ai) | moyen | **Adopté** (village créole) |
| Constellation zetwal (charte §7 — prescription portails fédivers) | faible | **Adopté** (hero atlas, remplace un shader Three.js de 708 Ko) |
| Layout baké au build — positions de graphe figées (déclinaison P2) | faible | **Adopté** (carte atlas : SVG 2D déterministe, 750 Ko de JS) |
| Moment de révélation designé — ligne de distance + count-up + chute de marqueur (jeux) | faible | **Adopté** (JWE) |
| Bilingual kinetic type — la bascule de langue comme événement (Nudot) | faible | **Candidat** (évolution du lang-switch FR/KA) |
| Chaptered scroll-comic (ten.375.studio) | moyen | **Candidat** (lore DJANGOKAM, patrimoine) |
| Scroll flipbook connection-aware (Apple) | moyen | **Candidat** si séquence visuelle un jour (gating `effectiveType` obligatoire) |
| Bake noise/textures offline (David Whyte) | faible | **Candidat** (fonds génératifs bakés) |
| Backstage / process public (Immersive Garden) | faible | **Candidat** (contenu pédagogique = souveraineté) |
| Cursor-as-light, velocity particles (Unseen, Igloo) | élevé | **Rejeté** (audience tactile) |
| WebGL full-UI (Igloo), skeletal scroll camera (HAPE), flipbook géant | très élevé | **Rejeté** (budgets 4G/Mali-G52) |
| Resolution gating (KPR) | — | **Interdit** (hostile mobile) |
---
## 7. TEMPLATE DE BRIEF DE MISSION (à remplir par projet)
```markdown
# MISSION : [Transformer X / Créer Y] selon le PLAYBOOK UNIFIÉ OKI × SVELTEKIT
## 1. Contexte
- Site/projet : [URL/dépôt] — stack actuelle mesurée : [à auditer, §2.4 phase 1]
- Identité : [association/projet, positionnement]
- Écosystème lié (liens externes à préserver) : [liste]
- Bugs de production confirmés : [liste audit]
## 2. Contenu éditorial
- [Reprendre à l'identique / réécriture autorisée : périmètre]
- Langues : [FR / EN / KA — URLs]
## 3. Exigences techniques
- Stack : §2.3 du playbook (défaut) — écarts éventuels : [liste]
- URLs à conserver : [liste + ancres]
- Hébergement cible : [o2switch statique / Cloudflare Pages]
- PWA : [oui/non]
## 4. Motion & composants (sélection depuis §2.6 — rester sobre)
- [ ] KineticText titres [ ] IntroLoader [ ] Marquee [ ] Village/scène
- [ ] Autre : [préciser] — WebGL : [non par défaut]
## 5. Périmètre de cette session
- [ ] …
## 6. Critères d'acceptation
- DoD §8 intégralement vérifiée + [spécifiques projet]
```
---
## 8. DOD — DÉFINITION DE « TERMINÉ » (tout doit être vrai)
**Fondations**
- [ ] Rapport d'audit livré ; bugs pré-existants corrigés ou explicitement reportés · **git vérifié/initialisé avant tout travail**.
- [ ] Tokens charte importés (fichier partagé, pas de valeurs recopiées) ; thème sombre défaut, clair opt-in AA.
- [ ] Fonts self-hébergées ; zéro requête tierce au chargement (vérifié onglet réseau + grep du build).
- [ ] `npm run check` : 0 erreur, 0 warning · `npm run build` vert · `svelte-autofixer` propre sur les fichiers modifiés.
- [ ] Si `paths.base` : `paths.relative: false` et hrefs du HTML prérendu inspectés (pas de `../` ni de double préfixe).
- [ ] **Audit factuel des données** (dates, compteurs, champs inutilisés) en plus de l'audit technique.
- [ ] Toute donnée tierce fetchée passe par `+page.server.ts` (un universal load inline le corps brut des fetch dans le HTML).
**Marque**
- [ ] Or = seule couleur d'action ; rouge/vert dans leurs rôles · angles nets (≤ 6 px).
- [ ] Flag-bar segments francs, ≤ 1 pleine largeur par écran · zéro emoji en interface (sprite SVG).
- [ ] Tokens motion gwoka + stagger syncopé · gate `prefers-reduced-motion` vérifiée manuellement.
- [ ] Voix conforme (test : aucune occurrence de « clé en main », « solution innovante », « disruptive »).
**Expérience & accessibilité**
- [ ] Navigation clavier complète (tab order, focus visible, skip-link, village/scène/carte focusable).
- [ ] **Zéro superposition** : chaque bouton testé par `elementFromPoint` (mobile 390 px) ; `pointer-events: none` sur tout conteneur décoratif.
- [ ] Layout app : `height: 100dvh` si « tout visible sans scroll » est exigé — `scrollHeight === innerHeight` mesuré mobile ET desktop.
- [ ] Page fonctionnelle et lisible avec JS désactivé.
- [ ] 404, vide et hors-ligne designés (KA + tambour) · loader designé si présent, skippable.
- [ ] `lang` correct par locale · hreflang · OG complet self-hébergé · JSON-LD.
**Mesures**
- [ ] Lighthouse mobile : Performance ≥ 90, Accessibilité ≥ 95, Best Practices ≥ 95, SEO ≥ 95.
- [ ] JS initial ≤ 170 Ko gzip · accueil ≤ 2 Mo · images optimisées (aucune > 200 Ko hors hero, lazy hors LCP).
- [ ] Toutes les URLs historiques préservées ou redirigées · sitemap/robots à jour.