Palette Studio — palette.amia.fr

Auteur : Dr Hamid MADANI <drmdh@msn.com> · Licence : AGPL-3.0-or-later Rôle : la source unique des palettes de l'écosystème. On y compose, l'outil dérive et valide, les applications consomment.

Un sélecteur de couleur ne sait pas dire non. Celui-ci le sait. On ne saisit pas 19 couleurs : on en donne 10 (3 de marque, 4 d'état, 3 surfaces) et 3 réglages de police. Les 31 tokens restants — textes, filets, contours de champ, survols, anneau de focus, fonds d'alerte — sont dérivés, puis validés (WCAG 2.1). Une couleur sous son seuil est corrigée, jamais émise telle quelle.

Table des matières

  1. L'écran
  2. Composer une palette
  3. Le magasin de thèmes
  4. La typographie
  5. Importer une palette existante
  6. Les 4 façons de consommer
  7. Brancher une application (recette complète)
  8. La chaîne Hadhinat : qui suit qui
  9. Le garde-fou de marque
  10. Comment le verdict est calculé
  11. Juger un site, comparer des gabarits
  12. Exploitation, API, dépannage

1. L'écran

Gauche — le compositeurDroite — les aperçus
Projet cible : l'app qui consommera cette palette (hadhinat, atc, salsabil, ou un nouveau)💻 Bureau 1280 px et 📱 Mobile 390 px, côte à côte
Magasin : 16 thèmes prêts (5 préréglages MostaJS + l'état de l'art)De vraies iframes aux largeurs réelles : les media queries s'appliquent pour de bon
10 sélecteurs de couleur → 31 tokens dérivésLa planche des 12 sections du système de design : palette, typo, boutons, champs, cartes, alertes, nav, tableaux, badges, icônes, divers, espacements
Typographie : police, taille de base, échelle des titresRecalcul instantané à chaque mouvement
Table de contrastes : les 16 couples exigés, avec leur verdict AA/AAA
Export : copier · télécharger · dossier ZIP · importer

2. Composer une palette (manipulation)

  1. Choisir le projet cible (en haut du panneau). C'est lui que les apps consommeront : …/theme.css?p=<projet>.
  2. Régler les couleurs — au sélecteur ou en tapant le code hexadécimal. Une saisie invalide est rétablie, jamais émise.
  3. Surveiller la table de contrastes : elle se recalcule à chaque mouvement. AA / AAA = conforme ; ✗ échec = à corriger.
  4. Enregistrer. Une palette non conforme est refusée (422) — on ne peut pas publier une interface illisible.
Créer un projet : sélecteur → + Nouveau projet… → nom en minuscules/chiffres/tirets → Enregistrer.

Permalien (utile pour montrer une variante sans rien enregistrer) : https://palette.amia.fr/?p=hadhinat&brand=%237C3AED&base=16&family=arabic


3. Le magasin de thèmes

Cliquer un thème charge ses couleurs de marque dans les sélecteurs — puis tout est re-dérivé et revalidé. C'est un point de départ, pas un calque.

L'enseignement du magasin (mesuré, cf. docs/01-ETAT-ART-THEMES.md) : 13 accents sur 16 sont illisibles en texte sur blanc — le jaune GOV.UK est à 1,3:1. Ce n'est pas une faute de ces systèmes : un accent n'est pas fait pour porter du texte. Le problème n'apparaît que quand un token nommé --gold finit en couleur de texte. D'où la règle : nommer par la fonction (--text, --focus-ring) et dériver. Les 16 thèmes produisent une palette intégralement conforme.

API : GET /api/gallery (CORS) — pour proposer le même magasin dans une autre app.


4. La typographie

Trois réglages seulement — police, taille de base (11→20 px), échelle des titres (compacte 1,15 · standard 1,23 · ample 1,33). Les 7 niveaux (H1 → légende) sont calculés : impossible d'obtenir un H2 plus petit qu'un H3.

Avec 14 px et l'échelle standard, on retrouve le système de design : 32 / 24 / 20 / 17 / 14 / 12 / 11. Vers le bas, l'échelle est linéaire et plancherée à 11 px (une descente modulaire donnerait une légende à 9 px, illisible).

Polices proposées : Système (Inter) · Grotesque · Serif · Monospace · Arabe (Cairo/Noto).


5. Importer une palette (coller ou téléverser)

Panneau « Importer une palette » : coller un CSS exporté ou un JSON de tokens, ou téléverser le fichier. Ce que l'app exporte, elle sait le relire (round-trip complet).

⚠ On ne lit que les codes hexadécimaux. Un CSS contenant background: url(https://attaquant/…) voit sa règle écartée — seuls les tokens sont retenus. C'est ce qui rend l'import sûr : on ne stocke jamais du CSS, on stocke des tokens, et le CSS est régénéré.


6. Les 4 façons pour une app de consommer une palette

#ModeCe que l'app faitQuand le choisir
1Lien vivant (CSS)<link rel="stylesheet" href="https://palette.amia.fr/theme.css?p=hadhinat">La palette change ici → l'app suit au prochain chargement. Cache : 5 min
2Rendu serveurGET /api/tokens?p=hadhinat au démarrage, puis CSS inline dans la coquilleAucune requête côté visiteur ; pas de FOUC (thème correct au 1er rendu)
3Dossier embarquébash sync-theme.sh (ou GET /export.zip?p=hadhinat) → un fichier de l'appAucun couplage réseau : l'app fonctionne même si Palette Studio est mort
4Lien vivant CLIENT (façon analytics a.js)<script src="https://palette.amia.fr/follow.js?p=hadhinat" defer></script>Mise à jour LIVE sans rechargement : l'onglet ouvert suit les changements tout seul. Marche sur tout site, même statique

Le mode 4 en détail. Une seule ligne <script> dans le <head> : le navigateur va chercher /theme.css?p=<projet>, injecte un <style> gardé en dernier (il surcharge donc le thème de la coquille), puis re-sonde périodiquement (?ttl=<secondes>, défaut 15 s) — tu édites la palette, l'onglet déjà ouvert change tout seul, sans reload ni redéploiement. Réglable : …/follow.js?p=hadhinat&ttl=30.

Recommandé — combiner les modes selon le besoin :

La propagation est automatique, et la panne est sans effet.


7. Brancher une application (recette complète)

Deux fichiers à copier, et rien d'autre — la logique vit dans le CLI de @mostajs/palette.

cd mon-app
cp <une-app-déjà-branchée>/sync-theme.sh .     # IDENTIQUE partout : il remonte l'arborescence
                                                # jusqu'à trouver mostajs/ (aucun ../../.. codé en dur)
cp <une-app-déjà-branchée>/theme.config.mjs .  # le SEUL fichier à adapter

theme.config.mjs :

export default {
  source: process.env.PALETTE_SOURCE || 'https://palette.amia.fr',
  project: 'hadhinat',          // le projet à consommer
  css: 'public/theme.css',      // où écrire la palette
  json: 'public/tokens.json',   // (optionnel) les mêmes valeurs pour le code
};

Puis :

bash sync-theme.sh            # récupère la palette, vérifie les 16 contrastes, écrit les fichiers
bash sync-theme.sh --check    # porte de déploiement : l'app est-elle à jour ?

Comportement de --check — et c'est ce qui garantit qu'une panne ne bloque personne :

SituationRésultat
Palette absente de l'app❌ échec — on ne déploie pas une app sans marque
Palette présente mais périmée❌ échec — bash sync-theme.sh puis redéployez
Source injoignable, palette embarquée présente✅ succès + avertissement — on déploie quand même

Enfin, charger le CSS avant les styles de l'app, et utiliser les tokens au lieu de couleurs en dur : var(--brand) · var(--text) · var(--text-muted) · var(--line) · var(--field-border) · var(--focus-ring) · var(--radius-md) · var(--space-2) · var(--font-h1-size)…

Autres commandes du CLI :

node <…>/mosta-palette/bin/cli.mjs show --project atc     # tokens + table de contrastes
node <…>/mosta-palette/bin/cli.mjs local                  # composer HORS LIGNE (sans source)

8. La chaîne Hadhinat : qui suit qui

La propagation va dans les DEUX sens. On peut composer indifféremment sur Palette Studio ou sur la plateforme — c'est le même résultat, et rien n'est jamais ignoré.

        ┌──────────────────────── palette.amia.fr (LA SOURCE) ────────────────────────┐
        │  projet « hadhinat » : 10 couleurs + 3 réglages de police, VALIDÉS          │
        └───▲───────────────┬───────────────────────────────────┬────────────────────┘
   ① publie │       ② tire  │                                   │ sert (lien vivant)
   ┌────────┴───────────────▼────┐                ┌─────────────▼──────────────────┐
   │  market.amia.fr             │                │  hadhinat.amia.fr (vitrine)    │
   │  /m/theme (admin)           │                │  1. theme.css EMBARQUÉ (filet) │
   │  · themes/*.json            │                │  2. <link> vivant PAR-DESSUS   │
   │  · bascule INSTANTANÉE      │                │     …/theme.css?p=hadhinat     │
   └─────────────────────────────┘                └────────────────────────────────┘

① Vous activez un thème sur market.amia.fr/m/theme

  1. La plateforme bascule immédiatement (la clé active est relue à chaque requête — aucun redémarrage).
  2. Le .env est mis à jour (THEME=…) : le prochain démarrage retombera sur ce thème.
  3. La plateforme publie la palette du thème dans le projet hadhinat de la source.
  4. La vitrine suit toute seule au prochain chargement, via son lien vivant. Aucun redéploiement.
Si la source est hors ligne : l'étape 3 échoue, la page le dit franchement (« activé ici, mais NON publié »), la plateforme bascule quand même, et la vitrine garde sa palette embarquée. On ne bloque jamais l'admin d'une app parce qu'une autre est en panne. Le garde-fou (§9) signale la divergence tant qu'elle dure.

② Vous composez sur palette.amia.fr — le bouton de tirage

Composer ici ne suffit pas à changer la plateforme : elle sert son thème actif, pas la palette du projet. (C'est ce qui donnait l'impression que « rien ne changeait ».) Deux surfaces, deux comportements :

SurfaceRéaction à un enregistrement dans le projet
Vitrineautomatique — elle charge le lien vivant, elle suit au prochain chargement (cache 5 min)
Plateformesur décision — un admin clique le bouton de tirage

Le bouton — sur market.amia.fr/m/theme, en tête de la liste :

⇩ Récupérer la palette du projet « hadhinat » depuis Palette Studio

Ce qu'il fait, dans l'ordre :

  1. GET https://palette.amia.fr/api/tokens?p=hadhinat — récupère couleurs + police.
  2. Valide la palette (contrastes) et l'enregistre comme thème themes/hadhinat.json.
  3. L'active — la plateforme change instantanément : couleur de marque, taille de base et police.
  4. Met à jour le .env, comme toute activation.

Si la source est injoignable : le tirage est refusé proprement et le thème actif n'est pas touché — on ne dégrade jamais une app en production parce qu'une autre ne répond pas.

(Ce comportement est délibéré : la plateforme est une app métier en production. Elle ne doit pas changer d'apparence parce que quelqu'un a enregistré un essai dans la source — un humain valide. La vitrine, elle, est une page de communication : le suivi automatique y est le bon compromis.)

Rafraîchir le filet embarqué de la vitrine

À faire de temps en temps (pas à chaque changement : le lien vivant suffit au quotidien) — c'est le filet qui protège d'une panne de la source :

cd incubator/animation/animation-v06/hadhinat_script_voix_multilang_html
bash sync-theme.sh
rsync -a index.html theme.css hmd@amia.fr:/home/hmd/prod/hadhinat/
git add theme.css && git commit -m "theme: palette mise à jour"

9. Le garde-fou de marque

cd incubator && bash check-brand.sh      # 0 = cohérent · 1 = divergence · 2 = site injoignable

Il compare la marque servie par les trois surfaces : plateforme, vitrine, projet. C'est arrivé le 2026-07-14 — la plateforme en Material 3, la vitrine en navy : un visiteur qui cliquait « Rejoindre » changeait d'univers. À lancer après tout changement de thème (et il est documenté au §6.bis du RUNBOOK.md).

⚠ Un test ne doit JAMAIS écrire dans la production. app/test-scripts/themes.sh force PALETTE_SOURCE=http://127.0.0.1:1 : sans cette ligne, un simple run-tests.sh publierait vers la vraie Palette Studio et repeindrait la vitrine publique. (Ça a failli arriver.)

10. Comment le verdict est calculé

Le bandeau « ✓ Palette conforme » n'est pas une appréciation : c'est WCAG 2.1, appliqué à la lettre. Tout tient dans mosta-palette/src/index.js, en trois temps.

10.1 La luminance relative

Chaque canal sRGB est d'abord linéarisé — l'écran n'est pas linéaire, et faire la moyenne des octets bruts donnerait un contraste faux :

lin(v) = s ≤ 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4     // s = v / 255
L      = 0.2126·R + 0.7152·G + 0.0722·B

Les trois poids ne sont pas égaux, et c'est le cœur de l'affaire : l'œil humain voit le vert bien plus que le bleu. Un jaune et un bleu de même « intensité » à l'écran n'ont pas du tout la même luminance.

10.2 Le rapport de contraste

contrast(a, b) = (L_haut + 0.05) / (L_bas + 0.05)      // 1 (identiques) → 21 (noir sur blanc)

Le + 0.05 représente la lumière ambiante reflétée par l'écran : sans lui, un noir parfait donnerait une division par zéro, et deux teintes très sombres paraîtraient infiniment contrastées alors qu'on ne les distingue pas.

10.3 Trois jeux de seuils, selon ce que la couleur PORTE

sizeCe que c'estAAAAA
normaltexte < 18 px4,57
large≥ 18 px gras, ou ≥ 24 px34,5
uibordures, icônes, anneau de focus33
⚠ Le seuil dépend de l'usage, pas de la couleur. Le même couple passe en large et échoue en normal. C'est pourquoi les jetons sont contrôlés avec leur rôle : un focus-ring est jugé en ui (3), un texte courant en normal (4,5). Annoncer « conforme » sans dire à quel seuil ne veut rien dire.

10.4 Les 16 couples contrôlés

Ils ne sont pas choisis au hasard : ce sont les paires que l'interface met réellement l'une sur l'autre.

CoupleSeuilPourquoi lui
text / surface · text / bgnormalle texte courant, sur les deux fonds qui existent
text-muted / surfacenormalle gris secondaire — le premier à devenir illisible
text-on-brand / brandnormalle libellé d'un bouton d'action
text-on-accent / accentnormalce qui interdit le blanc sur un accent clair
link / surfacenormalun lien doit se lire, pas seulement se deviner
field-border / surfaceuile bord d'un champ : sans lui, on ne sait pas où écrire
focus-ring / surfaceuil'anneau de focus — la navigation au clavier en dépend
success-text/success-bg · warning · error · infonormalles quatre messages d'état
success · warning · error · info / surfaceuiles mêmes en pastille ou en icône
audit(tokens) → { rows, pass, failures }

rows porte un objet par couple — { fg, bg, ratio, aa, aaa, level, size } — et pass n'est vrai que si tous passent AA. Rien d'autre : pas de pondération, pas de moyenne, pas d'indulgence.

⚠ Pourquoi les chiffres d'un modèle de langage ne suffisent pas. Une proposition de palette reçue le 12/09/2026 annonçait cinq ratios : les cinq étaient faux (18,8 pour 16,98 · 6,1 pour 5,12 · 8,2 pour 8,87…), et l'un des verdicts était faux même selon son propre chiffre — « 6,1:1 AAA », alors qu'AAA exige 7,0. Les couleurs proposées étaient bonnes ; les nombres, non. C'est exactement ce que cette page calcule à votre place.

11. Juger un site, comparer des gabarits

11.1 Juger un site

Le panneau « Juger un site » applique la même mesure à une page existante — la vôtre, ou une autre. Aussi en API :

curl 'https://palette.amia.fr/api/juger?url=https://exemple.fr'

Il lit la page et les feuilles qu'elle déclare (<link rel="stylesheet">), puis rend trois choses : les jetons du système reconnus, les couples texte/fond déclarés dans une même règle, et un verdict.

VerdictQuand
conformela palette est reconstituable et tout passe
non-conformeun couple échoue — d'où qu'il vienne
indeterminepas assez de jetons pour reconstituer la palette
⚠ indetermine est un RÉSULTAT, pas une panne. On ne connaît pas les contrastes réels d'une page sans la rendre : héritage, superpositions, dégradés, texte posé sur une image. Seul le couple déclaré dans la même règle se démontre par la lecture. Un outil qui annoncerait « conforme » sur cette base ferait prendre une décision d'accessibilité sur une preuve qui n'en est pas une.
⚠ Une couleur semi-transparente est écartée, jamais convertie. rgba(0,0,0,.5) n'a pas de contraste propre : il dépend de ce qu'il y a dessous. Un chiffre faux est pire qu'aucun chiffre.
⚠ La porte fermée. Aller chercher une URL fournie par un visiteur, c'est lui prêter la place du serveur sur le réseau. Le nom est résolu avant connexion, toute adresse privée, de bouclage ou de lien-local est refusée, et le contrôle est refait à chaque redirection — un domaine public qui redirige vers 127.0.0.1 est le contournement classique.

11.2 Comparer des gabarits

Les projets enregistrés apparaissent dans le magasin sous « Vos gabarits enregistrés », et /comparer?sites=a,b,c les rend côte à côte, chacun dans sa propre palette, avec son verdict dessous.

⚠ Une palette ne se juge pas seule. Ouverte à son tour dans le compositeur, chacune paraît correcte : l'œil s'habitue en trois secondes et perd la référence. Côte à côte, un jaune trop pâle ou un bleu trop froid se voit immédiatement — et le verdict WCAG reste affiché dessous, pour que le goût ne décide pas seul.

12. Exploitation

💡 L'aide en ligne, c'est ce document. Le bouton « ? Aide » (en haut du compositeur) sert ce README même, rendu à la volée sur https://palette.amia.fr/aide et habillé par le thème du projet. Il n'y a donc qu'une seule source de documentation : elle ne peut pas se désynchroniser de l'app.

API

RouteRôle
GET /le compositeur (?p=<projet> + permaliens de couleurs/police)
GET /theme.css?p=<projet>le lien vivant (CORS, cache 5 min)
GET /api/tokens?p=<projet>{ project, input, font, tokens, scales, report, css } (CORS)
GET /export.zip?p=<projet>palette.css + tokens.json + INTEGRATION.md
GET /api/galleryle magasin (16 thèmes)
GET /api/projectsles projets connus
POST /api/save{ project, input, font } → 422 si la palette n'est pas conforme
GET /aidece README, rendu dans l'app (@mostajs/markdown-html) — une seule source de doc
GET /api/health{ ok, service, persistence, projects }

Persistance

ORM @mostajs/orm, dialecte sqljs (SQLite WASM : aucun binaire natif à compiler — cf. le piège mediasoup du RUNBOOK). Base : data/palettes.db. Repli fichier (data/palettes.json) si l'ORM est indisponible : cette app est la source des palettes de tout l'écosystème, une base en panne la dégrade, elle ne la tue pas. GET /api/health dit laquelle est active.

Changer de SGBD : DB_DIALECT + SGBD_URI (19 dialectes supportés).

Commandes

node server.mjs                 # local (PORT=4800)
bash test-scripts/run-tests.sh  # porte qualité : 41 + 24 + 47 = 112 tests
bash deploy.sh --check          # vendor + tests, SANS déployer
bash deploy.sh                  # + rsync + pm2 + health
sudo bash setup-palette.sh      # (une fois, en root) vhost Apache + HTTPS

pm2 palette-studio sur 127.0.0.1:4800, Apache en reverse-proxy + Let's Encrypt. data/ n'est pas transféré au déploiement : la base de production est préservée.

Architecture (DEVRULES §10 — composition)

Les deux modules sont de l'ESM pur sans dépendance : le même code produit le premier rendu (serveur) et le recalcul (navigateur). Zéro build, zéro framework.

Dépannage

SymptômeCause probable
J'ai composé ici, la plateforme n'a pas bougéNormal : elle sert son thème actif. → cliquer ⇩ Récupérer la palette du projet sur /m/theme (§8 ②)
Le thème ne change pas dans une autre appElle consomme le fichier embarqué sans lien vivant → bash sync-theme.sh
La vitrine reste sur l'ancienne paletteCache du lien vivant (5 min), ou publication échouée → voir le message de /m/theme
POST /api/save renvoie 422Palette non conforme : la réponse liste les couples fautifs
sync-theme.sh échoueSource injoignable — aucun fichier n'est écrit (on ne devine pas une palette)

Conception : docs/00-CARTOGRAPHIE-ET-PROPOSITION.md · État de l'art : docs/01-ETAT-ART-THEMES.md · Exploitation générale : ../RUNBOOK.md