feat(oki): fondations de la migration vers la stack souveraine OKI

Architecture décidée et documentée dans ADR.md : SvelteKit natif en
fichiers plats plutôt que Strapi (55 outils, données statiques,
souveraineté et budget performance prioritaires — doctrine §1.2, §4.4).

- frontend/ : SvelteKit 2 + Svelte 5 (runes), adapter-static, 58 pages
  prérendues, CSS natif en @layer (reset→tokens→base→layouts→components),
  tokens oklch(), container queries, typo fluide clamp(), 2 polices WOFF2
  auto-hébergées (Atkinson Hyperlegible, Fraunces), zéro CDN tiers
- i18n FR/gcf typé (clé manquante = erreur de build), bascule Kréyòl
  persistée en localStorage, micro-textes créoles (« Ou pa manké ayen »)
- Composants : ToolCard, DifficultyBadge (forme+texte+couleur, jamais la
  couleur seule), CategoryNav (≤5 entrées, divulgation progressive),
  SearchBar (raccourci /), LanguageSwitcher, EndMarker
- content/ : 55 fiches migrées depuis src/ (script frontend/scripts/
  migrate-tools.mjs, idempotent), licences SPDX reprises de directory.json
- docs/ : DESIGN_SYSTEM.md et CONTENT_GUIDE.md

Budgets vérifiés : JS 41 Ko gzip, CSS 2,9 Ko gzip, svelte-check 0 erreur.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
sucupira
2026-07-10 12:51:17 -04:00
parent f4e09ca886
commit db5a997e50
99 changed files with 5356 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
/*
* base.css — HTML sémantique nu (doctrine §4.3 : l'accessibilité est
* d'abord un problème de HTML correct).
*/
body {
font-family: var(--font-text);
font-size: var(--text-1);
line-height: var(--leading);
color: var(--oki-ink);
background: var(--oki-surface);
}
h1,
h2,
h3 {
font-family: var(--font-display);
line-height: 1.15;
text-wrap: balance;
}
h1 {
font-size: var(--text-5);
}
h2 {
font-size: var(--text-4);
}
h3 {
font-size: var(--text-2);
}
p,
li {
max-width: var(--measure);
}
a {
color: var(--accent-ink);
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
}
a:hover {
text-decoration-thickness: 2px;
}
/* focus visible sur TOUT élément interactif (doctrine §4.3) */
:focus-visible {
outline: var(--focus-ring);
outline-offset: 2px;
border-radius: var(--radius-1);
}
button {
min-height: var(--tap-target); /* Fitts, doctrine §1.3 */
padding-inline: var(--space-3);
border: 1px solid var(--oki-line);
border-radius: var(--radius-2);
background: var(--oki-surface-2);
cursor: pointer;
}
code {
font-size: 0.9em;
background: var(--oki-surface-2);
padding: 0.1em 0.35em;
border-radius: var(--radius-1);
}
pre {
overflow-x: auto;
padding: var(--space-3);
background: var(--oki-surface-2);
border-radius: var(--radius-2);
}
pre code {
background: none;
padding: 0;
}
::selection {
background: var(--oki-jaune);
color: var(--oki-noir);
}
/* lien d'évitement clavier */
.skip-link {
position: absolute;
inset-inline-start: var(--space-3);
inset-block-start: var(--space-3);
translate: 0 -300%;
background: var(--oki-surface);
padding: var(--space-2) var(--space-3);
z-index: 10;
}
.skip-link:focus {
translate: 0 0;
}
+20
View File
@@ -0,0 +1,20 @@
/*
* Polices auto-hébergées, servies depuis notre domaine — jamais de Google
* Fonts (doctrine §5.3 : performance, RGPD, souveraineté).
* Budget : 2 fichiers WOFF2 maximum. Le gras du corps est synthétisé.
*/
@font-face {
font-family: 'Atkinson Hyperlegible';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/atkinson-hyperlegible-latin-400-normal.woff2') format('woff2');
}
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/fraunces-latin-700-normal.woff2') format('woff2');
}
+63
View File
@@ -0,0 +1,63 @@
/*
* layouts.css — primitives de composition (approche « Every Layout » :
* des compositions, pas des pages — doctrine §5.2).
* Le responsive passe par container queries, jamais par media queries
* de largeur d'écran (doctrine §2.2 point 6).
*/
.stack {
display: flex;
flex-direction: column;
gap: var(--gap, var(--space-3));
}
.cluster {
display: flex;
flex-wrap: wrap;
gap: var(--gap, var(--space-2));
align-items: var(--align, center);
}
.center {
box-sizing: content-box;
max-inline-size: var(--max, 70rem);
margin-inline: auto;
padding-inline: var(--space-3);
}
.prose {
max-inline-size: var(--measure);
}
/* grille de cartes : pilotée par le conteneur, pas par l'écran */
.card-grid {
container-type: inline-size;
}
.card-grid > ul {
list-style: none;
padding: 0;
display: grid;
gap: var(--space-3);
grid-template-columns: 1fr;
}
@container (min-width: 44rem) {
.card-grid > ul {
grid-template-columns: repeat(2, 1fr);
}
}
@container (min-width: 68rem) {
.card-grid > ul {
grid-template-columns: repeat(3, 1fr);
}
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
+54
View File
@@ -0,0 +1,54 @@
/* Reset moderne minimal (doctrine §5.2 — ~30 lignes, rien de plus). */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
html {
/* jamais de font-size en px sur html : respect de la taille utilisateur (doctrine §4.3) */
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
min-height: 100dvh;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
img,
picture,
svg,
video {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
color: inherit;
}
p,
h1,
h2,
h3,
h4 {
overflow-wrap: break-word;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
+77
View File
@@ -0,0 +1,77 @@
/*
* tokens.css — design tokens OKI (doctrine §5.2).
* Toute valeur visuelle du site vit ici ; les composants Svelte consomment
* les tokens, jamais de valeur brute (couleur, taille) dans un composant.
* Couleurs en oklch() : luminance perceptuelle maîtrisée => contraste WCAG
* calculable (méthode modus-vivendi, doctrine §4.3).
*/
:root {
color-scheme: light dark;
/* ── Identité panafricaine OKI ─────────────────────────────────────── */
--oki-ink: light-dark(oklch(18% 0.02 260), oklch(94% 0.01 260));
--oki-surface: light-dark(oklch(97% 0.005 90), oklch(14% 0.02 260));
--oki-surface-2: light-dark(oklch(93% 0.008 90), oklch(20% 0.02 260));
--oki-line: light-dark(oklch(85% 0.01 90), oklch(32% 0.02 260));
--oki-ink-2: light-dark(oklch(38% 0.02 260), oklch(75% 0.01 260));
/* Couleurs identitaires (drapeau panafricain — jaune/vert/rouge/noir) */
--oki-jaune: oklch(75% 0.18 85);
--oki-vert: oklch(55% 0.18 145);
--oki-rouge: oklch(55% 0.22 25);
--oki-noir: oklch(15% 0.01 260);
/* Accent principal : le jaune OKI, décliné pour rester AA sur chaque fond */
--accent: light-dark(oklch(52% 0.14 85), var(--oki-jaune));
--accent-ink: light-dark(oklch(35% 0.1 85), oklch(85% 0.16 85));
/* ── Difficulté — jamais portée par la couleur seule (doctrine §1.1) ──
Chaque niveau = couleur + texte + icône dans DifficultyBadge. */
--diff-beginner: oklch(65% 0.15 145);
--diff-intermediate: oklch(75% 0.16 85);
--diff-advanced: oklch(55% 0.18 25);
/* variantes texte, contrastées AA sur --oki-surface */
--diff-beginner-ink: light-dark(oklch(42% 0.14 145), oklch(78% 0.15 145));
--diff-intermediate-ink: light-dark(oklch(48% 0.13 85), oklch(80% 0.15 85));
--diff-advanced-ink: light-dark(oklch(46% 0.18 25), oklch(72% 0.16 25));
/* ── Typographie fluide (clamp, zéro media query — doctrine §5.2) ────
Échelle modulaire ratio 1.25, corps 16px mobile → 18px desktop. */
--text-0: clamp(0.875rem, 0.85rem + 0.15vw, 0.9375rem);
--text-1: clamp(1rem, 0.95rem + 0.4vw, 1.125rem);
--text-2: clamp(1.25rem, 1.17rem + 0.5vw, 1.4rem);
--text-3: clamp(1.56rem, 1.42rem + 0.8vw, 1.75rem);
--text-4: clamp(1.95rem, 1.7rem + 1.4vw, 2.44rem);
--text-5: clamp(2.44rem, 2rem + 2.4vw, 3.4rem);
--font-text: 'Atkinson Hyperlegible', system-ui, sans-serif;
--font-display: 'Fraunces', var(--font-text);
--measure: 65ch; /* largeur de lecture 45-75ch (doctrine §2.2) */
--leading: 1.6;
/* ── Espacement modulaire ────────────────────────────────────────── */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 1rem;
--space-4: 1.5rem;
--space-5: 2.5rem;
--space-6: 4rem;
/* ── Interaction ─────────────────────────────────────────────────── */
--tap-target: 48px; /* Fitts (doctrine §1.3) */
--radius-1: 4px;
--radius-2: 10px;
--duration-1: 120ms;
--duration-2: 240ms;
--ease-out: cubic-bezier(0.2, 0, 0, 1);
--focus-ring: 3px solid var(--accent);
}
@media (prefers-contrast: more) {
:root {
--oki-ink: light-dark(oklch(5% 0.01 260), oklch(99% 0 0));
--oki-line: light-dark(oklch(40% 0.01 260), oklch(70% 0.01 260));
}
}