DESIGN-SKILL.md
Le langage de design d'Open Analytics dans un seul fichier, écrit pour les agents IA et les humains qui les dirigent. Chaque valeur ici est livrée dans le produit : codes hexadécimaux, constantes de ressort, rayons, assouplissements. Collez ce fichier dans le contexte de votre agent (ou placez-le dans votre dépôt en tant que fichier de règles) et demandez des écrans « dans le style OA ».
Il s'agit de l'édition compilée en un seul fichier, conçue pour être copiée.
Le même langage est également livré dans ce dépôt en tant que skill d'agent
propre (skills/oa-design/ : recettes par composant avec le source complet
vérifié par type) et en tant que CLI (npx getopen-design add <composant>) ;
privilégiez ces versions pour Claude Code et les autres agents capables de
lire les skills.
Le look en une phrase : des surfaces blanches aux coins à courbure continue, reposant sur une scène grise silencieuse, dessinées avec une seule encre, mues par un seul ressort.
1. Les dix règles
- Une seule encre, tout est dérivé. Tout le système neutre est une couleur mélangée à de la transparence à des pourcentages fixes. Ne choisissez jamais un second gris.
- Deux couches, et l'écart est la page. Les surfaces sont un cadre blanc contenant un insert gris en retrait ; les sections sont des plaques et l'arrière-plan de la page entre elles est le seul séparateur. Pas de règles horizontales.
- Des squircles pour les surfaces, des pilules pour les actions. Les cartes ont des coins à courbure continue ; tout ce qui est cliquable qui n'est pas une carte est une pilule.
- Une famille de ressorts. Sept ressorts nommés couvrent tout le produit. N'en inventez pas un huitième.
- Le chrome n'attend jamais. La mise en page et les titres s'affichent instantanément ; seuls les changements de données à partir d'un squelette dont les pixels correspondent, arrivant par un flou, pas par un affichage soudain.
- Le poids s'arrête à 500. Pas de gras. La hiérarchie provient de la taille, de la couleur et de l'espacement.
- Un accent, dépensé à un seul endroit. Un seul bleu pour les actions primaires et la ligne de graphique primaire ; les couleurs sémantiques sont des teintes de texte, pas des remplissages.
- Les états sont des pilules, les événements se terminent seuls. Les conditions permanentes sont rendues sous forme de bandes non désactivables qui vivent exactement aussi longtemps que l'état ; les résultats ponctuels sont des toasts qui disparaissent seuls.
- Le texte fait partie du design. Utilisation de la casse des phrases ; les boutons disent ce qui se passe ; les erreurs nomment la cause et la solution, sans blâme.
- Seuil de qualité, toujours. Anneaux de focus,
role="status", décorationsaria-hidden,prefers-reduced-motion, et aucun défilement horizontal de page.
2. Tokens
Clair
:root {
--ink: #292929;
--background: #f6f6f6; /* la scène grise entre les plaques */
--card: #ffffff;
--popover: #ffffff;
/* neutres dérivés de l'encre : les pourcentages SONT le système */
--border: color-mix(in srgb, var(--ink) 12%, transparent);
--input: color-mix(in srgb, var(--ink) 14%, transparent);
--accent: color-mix(in srgb, var(--ink) 5%, transparent); /* lavis de survol */
--muted: color-mix(in srgb, var(--ink) 5%, transparent);
--muted-foreground: #6d6d6d;
--foreground: var(--ink);
--primary: #305dde; /* l'unique accent */
--primary-foreground: #ffffff;
--ring: #3ba6f1;
--secondary: #e9e9e9; /* contrôles gris plats, sans bordure */
--secondary-foreground: var(--ink);
/* sémantiques, en tant que teintes TEXT sur fond clair (échelle Tailwind, ligne 700) */
--destructive: var(--color-red-500);
--destructive-foreground: var(--color-red-700);
--success-foreground: var(--color-emerald-700);
--warning-foreground: var(--color-amber-700);
--info-foreground: var(--color-blue-700);
--chart-1: #296FF0; /* ligne de graphique primaire */
--radius: 0.625rem;
}
Besoin d'un séparateur plus fort ? Augmentez le pourcentage d'encre (le produit utilise seulement 4, 5, 10, 12, 14). Les couleurs sémantiques vont sur les mots et les points de 8px, jamais sur les remplissages de panneaux.
Sombre
Même recette, réencrée, avec deux changements systématiques : les pourcentages de cheveux augmentent de 2 points, et le texte sémantique passe de la ligne 700 à la ligne 400.
.dark {
--ink: #ededed;
--background: #191919;
--card: #212121;
--popover: #242424;
--border: color-mix(in srgb, var(--ink) 14%, transparent);
--input: color-mix(in srgb, var(--ink) 16%, transparent);
--muted-foreground: #969696;
--secondary: #2e2e2e;
--primary: #296FF0;
--destructive-foreground: var(--color-red-400);
--success-foreground: var(--color-emerald-400);
}
Rayon
Une base, multipliée ; jamais de rayon sur mesure :
--radius: 0.625rem; /* sm ×0.6, md ×0.8, lg ×1, xl ×1.4,
2xl ×1.8, 3xl ×2.2, 4xl ×2.6 */
rounded-full est réservé aux pilules. Les surfaces de type squircle ont
leurs propres rayons plus grands (section 4).
Typographie
- Inter Tight, variable, poids 300 à 500, auto-hébergé (les CDN de polices échouent dans les builds ; conservez le woff2 dans le dépôt).
- Geist Mono pour
code, kbd, samp, pre, les touches et les extraits. - Le plafond de poids est l'identité : rien de plus lourd que 500 n'est
rendu. La hiérarchie provient de la taille, de
text-foreground/80vstext-muted-foregroundet de l'espacement. - Titres :
font-medium tracking-tight; les titres marketing ajoutenttext-wrap: balance. - Données :
tabular-numssur tout nombre dans une colonne ou mis à jour sur place. - La voix UI par défaut est 14px ; les lignes secondaires 12px, avec
leading-5/leading-6.
Ombres
L'élévation est binaire :
/* carte au repos */ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
/* chrome flottant */ box-shadow: 0 1px 2px rgba(0,0,0,0.08),
0 8px 24px rgba(0,0,0,0.08);
Discipline de fond
Le body est blanc (le dépassement montre le corps ; le gris qui
apparaît derrière un écran blanc donne l'impression d'une page différente).
Les écrans qui veulent la scène grise peignent leur propre décor. La racine
obtient overflow-x: clip.
3. Mise en page
Plaques. Les sections sont des panneaux autonomes ; l'arrière-plan de la
page qui apparaît entre elles est la seule limite. Les espaces verticaux
sont généreux (gap-12 sm:gap-20 marketing, gap-6 dans l'application), le
remplissage latéral plus serré (px-4 sm:px-6). Les éléments frères sont
espacés par le gap du parent, jamais par des marges par élément. Pas de
<hr>, pas de border-b entre les sections.
Largeurs par portée : marketing 76rem (6.5xl), pages du site de
l'application max-w-6xl, écrans de liste/sélection max-w-5xl, surfaces de
lecture ~65ch. Centré, mx-auto.
Anatomie d'une page d'application :
en-tête fixe (h-14) → conteneur pt-14
pilule flottante optionnelle (fixed top-16, centrée)
<main class="px-4 pb-36 pt-8 sm:px-6 bg-white">
ligne de titre : h1 text-xl font-medium tracking-tight + UN contrôle à droite
bande(s) d'avis optionnelle(s)
plaques : flex flex-col gap-6
La ligne de titre contient exactement un contrôle. pb-36 maintient le
contenu éloigné du bord de la fenêtre.
Grilles : grid gap-3, colonnes réactives (grid-cols-2 sm:grid-cols-4
pour les statistiques). Les cartes partagent une hauteur de contenu fixe et
défilent en interne ; la ligne de base de la grille ne se brise jamais.
Réactif : conception à 390px et 1280px. Les paires horizontales
s'empilent sous sm: avec l'action en dernier. Le contenu large défile dans
son propre conteneur overflow-x-auto ; la page ne défile jamais
horizontalement.
Paramètres : rail d'onglets étroit + colonne de contenu des panneaux de
section ; chaque section s'ouvre avec un bloc d'en-tête (titre + une phrase
en gris, action unique optionnelle à droite). Onglets à lien profond via
?tab=<id>.
4. Composants
La surface squircle
Coins à courbure continue via un clip-path shape() CSS paramétré par deux
variables, avec corner-shape: squircle là où c'est pris en charge :
.squircle {
border-radius: 26px;
--card-clip-radius: 14px;
--card-clip-handle: 2.25px;
clip-path: shape(
from var(--card-clip-radius) 0px,
line to calc(100% - var(--card-clip-radius)) 0px,
curve to 100% var(--card-clip-radius)
with calc(100% - var(--card-clip-handle)) 0px / 100% var(--card-clip-handle),
line to 100% calc(100% - var(--card-clip-radius)),
curve to calc(100% - var(--card-clip-radius)) 100%
with 100% calc(100% - var(--card-clip-handle)) / calc(100% - var(--card-clip-handle)) 100%,
line to var(--card-clip-radius) 100%,
curve to 0px calc(100% - var(--card-clip-radius))
with var(--card-clip-handle) 100% / 0px calc(100% - var(--card-clip-handle)),
line to 0px var(--card-clip-radius),
curve to var(--card-clip-radius) 0px
with 0px var(--card-clip-handle) / var(--card-clip-handle) 0px,
close
);
corner-shape: squircle;
}
@media (min-width: 640px) {
.squircle { border-radius: 50px; --card-clip-radius: 20px; --card-clip-handle: 3px; }
}
La mini carte
squircle externe : bg-card, border, p-1, ombre au repos
├── bande d'en-tête (pb-2 pl-3.5 pr-2 pt-1.5) :
│ icône atténuée 16px + titre text-sm font-medium text-foreground/80
│ puce optionnelle sur l'axe central du titre
│ "Voir tout ›" (text-xs muted ; survol : lavis d'accent, flèche glisse de 2px)
└── squircle interne : bg #f6f6f6, border, rounded-[22px] sm:rounded-[44px],
hauteur FIXE (h-44 = cinq lignes de 32px), overflow hidden
Les cartes ne grandissent jamais avec les données ; les listes plus longues
défilent à l'intérieur. Le débordement montre une puce de chevron de 24px en
bas à droite, avec un mouvement de y:[0,2.5,0] (boucle 1.4s easeInOut),
s'estompant (0.18s, échelle 0.7) à la fin du défilement. Lignes : 32px,
survol avec lavis d'accent à pleine largeur, valeurs tabular-nums.
Boutons
Pilules, font-medium, physiquement enfoncées :
active:translate-y-px active:scale-[0.98].
- primaire : bleu d'accent avec un biseau (fond/bordure mélangés vers un
indigo profond,
inset 0 1px 0 rgba(255,255,255,0.22)en haut, bordure sombre en bas) ; le survol éclaircit vers le primaire pur. - secondaire :
--secondaryplat, bordure transparente, le survol mélange 5% d'encre. Le cheval de bataille. - fantôme : texte atténué uniquement. destructeur : remplissage rouge, uniquement à l'étape de confirmation.
- Tailles h-9 / h-8 / h-7 ; le chrome de l'application utilise surtout les petites.
- En chargement, le libellé est conservé et un spinner annulaire de 14px
(
border-2 border-current border-t-transparent) est ajouté ; la largeur ne saute jamais. focus-visible:ring-[3px]dans--ring/50. Jamais supprimé.
Panneaux déroulants (menus, sélecteurs, commutateurs)
Pilule de déclenchement → panneau flottant (ombre flottante, bordure, blanc, rounded-2xl+), entrée par ressort à PANEL (550/38) avec un petit décalage y/échelle, sortie plus rapide. Éléments de 32px rounded-lg avec survol en lavis d'accent ; la surbrillance active est un seul élément partagé qui se déplace (ressorts layoutId ou largeur/hauteur), jamais de repeints par élément. Fermeture sur pointerdown à l'extérieur et Escape.
Modales
Le fond se dissout en ~0.15s ; le panneau entre par ressort à POP (400/26)
depuis opacity 0, scale .96, sort à 380/28. Les dialogues à plusieurs
étapes conservent un seul panneau dont la hauteur s'anime vers la taille
mesurée de chaque étape (LAYOUT 550/40 + ResizeObserver) pendant que les
étapes glissent (x: ±12px, popLayout, 0.16s easeOut fades). Pied de page :
bande h-12, Retour (secondaire xs) + primaire xs, aligné à droite.
Barre d'onglets
La pilule active est une fenêtre fixe ; les libellés glissent derrière elle. Le libellé entrant vient de la direction de déplacement à 110% de sa propre largeur ; le libellé sortant sort à 130px fixes de l'autre côté (un pourcentage échoue lorsque la pilule grandit vers un mot plus long). La largeur/hauteur de la pilule suit le libellé mesuré sur les ressorts LAYOUT. Opacité totale tout au long ; le masque fait le travail.
Squelettes
- Le chrome n'attend jamais ; seule la zone de données est un squelette.
- Correspondance pixel : une barre se trouve dans le même emplacement de
hauteur de ligne que le texte qu'elle représente (
flex h-6 items-centerentourant une barreh-3) ; l'échange ne déplace rien. - Les données arrivent par le focus : une animation unique de
opacity ~0.4 + blur(4px)vers la netteté. Une animation, pas une transition (le contenu est monté déjà final ; une transition n'a pas de valeur précédente). Gardezfilter: blur(0)après.
Barre : rounded bg-muted-foreground/15, balayage lent de la position de
fond (2s linéaire infini, décalage de départ -1s pour que les barres
soient déphasées).
Avis, états vides, la pilule flottante
Bande d'avis au-dessus du contenu qu'elle explique :
<div class="flex flex-col gap-2 rounded-2xl border border-border bg-card
px-4 py-3 sm:flex-row sm:items-center sm:justify-between"
role="status">
<p class="text-sm leading-6 text-muted-foreground">
<span class="font-medium text-foreground">Aucun événement pour l'instant.</span>
Si l'extrait est actif, parcourez votre site et nous capterons la
première page vue en quelques secondes.
</p>
<button class="…pilule secondaire…">Vérifier l'installation</button>
</div>
Une affirmation en gras, une phrase en gris, une action secondaire ;
s'empile sous sm: ; vit exactement aussi longtemps que l'état ; pas de
licenciement.
Pilule flottante pour les conditions permanentes dans toute
l'application : fixe top-16, centrée, rounded-full bg-card py-1.5 pl-3 pr-4 text-xs font-medium, ombre flottante, point d'état de 8px (pulsant en
cas d'urgence), entrée par ressort à BANNER (400/30) depuis y:-8, sortie
0.15s. La pilule entière est un lien vers l'endroit où l'état est résolu. Les
pilules d'état ne se désactivent jamais d'elles-mêmes ; les pilules
d'événement disparaissent après ~6s.
Toasts
Moins de 0.35s, assouplissement cubic-bezier(0.5, 1, 0.89, 1) :
- Succès : pulsation
scale 1 → 1.025 → 0.99 → 1(0.32s). - Erreur : secousse
x 0 → -3 → 3 → -3 → 0px (0.28s).
Alterner les copies d'images clés paires/im.paires pour que les répétitions redémarrent.
5. Mouvement
| Nom | Valeur | Utilisé pour |
|---|---|---|
| PANEL | 550 / 38 | déroulants, menus, tableaux, bascules |
| LAYOUT | 550 / 40 | hauteur/largeur mesurée, pilules coulissantes |
| POP | 400 / 26 | entrée de modale |
| POP_EXIT | 380 / 28 | sortie de modale |
| BANNER | 400 / 30 | pilules flottantes, bannières de page |
| FLICK | 900 / 50 | micro-mouvements d'icône |
| CHART | 300 / 25-30 | infobulles de graphique, réticule |
Micro-fondu : 0.1s à 0.18s easeOut. Rien dans le chrome de l'application ne s'anime plus de 0.2s ; si cela semble lent, réduisez l'amortissement, n'augmentez pas la durée.
Mouvements signature : chorégraphie de hauteur mesurée (un panneau constant, étapes qui glissent), la surbrillance mobile (un seul élément actif partagé, jamais de repeints par élément), asymétrie entrée/sortie (les sorties sont toujours plus rapides), survol uniquement par couleur (plus au maximum un glissement de flèche de 2px ; les cartes ne se soulèvent ni ne se redimensionnent jamais), et au maximum une minuscule boucle ambiante par écran.
Règles : AnimatePresence pour tout ce qui se démonte ; respecter
prefers-reduced-motion (les révélations se réduisent à visibles, les
boucles s'arrêtent) ; ne jamais animer une mise en page non mesurée ; ne
jamais lire l'horloge murale pendant le rendu (s'abonner à un magasin qui
tique toutes les minutes).
6. Pages d'atterrissage
Mêmes tokens, s
…
[tronqué pour la traduction]