P1 fondation : tokens layerés, reset, thèmes (base/renovation/aep/tmip), 9 primitives Astro, pont Tailwind 4, démo kitchen sink
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.astro/
|
||||||
|
npm-debug.log*
|
||||||
85
PROTOCOLE.md
Normal file
85
PROTOCOLE.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# PROTOCOLE — design system trans-former.fr
|
||||||
|
|
||||||
|
La « matrice » : les règles que tout site de la constellation applique. Un build n'est pas fini tant que la Definition-of-Done en bas n'est pas cochée.
|
||||||
|
|
||||||
|
## 1. Mobile-first — règle de base
|
||||||
|
|
||||||
|
- Les styles de base ciblent **360px**. On monte ensuite avec des media queries **`min-width` uniquement** (jamais `max-width` pour la structure ; une exception encadrée : l'état hamburger de la Nav).
|
||||||
|
- Typographie **fluide** en `clamp()` — l'échelle `--fs-*` est calibrée entre 360px et 1280px, ne pas la redéfinir par breakpoint.
|
||||||
|
- Cibles tactiles **≥ 44px** (boutons, liens de nav, toggles). Sur `pointer: coarse`, aucune taille ne descend en dessous.
|
||||||
|
|
||||||
|
### Breakpoints — constantes
|
||||||
|
|
||||||
|
Les variables CSS ne fonctionnent pas dans `@media` : ces valeurs sont des **constantes de la constellation**, à utiliser telles quelles partout.
|
||||||
|
|
||||||
|
| Nom | px | rem | Usage type |
|
||||||
|
|-----|----|-----|------------|
|
||||||
|
| base | 360 | — | styles par défaut (pas de media query) |
|
||||||
|
| sm | 480 | 30rem | ajustements petits écrans larges |
|
||||||
|
| md | 768 | 48rem | passage nav inline, grilles 2-3 col |
|
||||||
|
| lg | 1024 | 64rem | layouts larges |
|
||||||
|
| xl | 1280 | 80rem | plafond de l'échelle fluide |
|
||||||
|
|
||||||
|
Écrire les media queries en **rem** (`@media (min-width: 48rem)`).
|
||||||
|
|
||||||
|
## 2. Motion charter
|
||||||
|
|
||||||
|
- **Motion piloté-utilisateur** : toute animation répond à une action (scroll, hover, focus, clic). **Jamais d'auto-play** — pas de carrousel automatique, pas d'animation en boucle — sans pause au survol/focus **et** contrôles visibles.
|
||||||
|
- `@media (prefers-reduced-motion: reduce)` **coupe tout mouvement non essentiel**. Le kill-switch global est dans `reset.css` ; tout JS d'animation doit aussi tester `matchMedia("(prefers-reduced-motion: reduce)")` (cf. `js/reveal.js`).
|
||||||
|
- **Budget reveal** : fade + translate court (≤ 12px), durées `--dur-base`/`--dur-slow` max, easing `--ease-out`. Pas de zoom, pas de rotation, pas de parallaxe décorative.
|
||||||
|
- Le JS est **progressif** : sans JS, rien n'est masqué, rien n'est cassé (garde `html[data-js]`).
|
||||||
|
|
||||||
|
## 3. Lois design (anti-patterns) — appliquées sans exception
|
||||||
|
|
||||||
|
- ❌ `border-left`/`border-right` > 1px colorée comme accent (card, callout)
|
||||||
|
- ❌ gradient text (`background-clip: text`)
|
||||||
|
- ❌ glassmorphisme décoratif par défaut
|
||||||
|
- ❌ template hero-metric (grand chiffre + petit label + gradient accent)
|
||||||
|
- ❌ grilles de cards identiques icon+heading+text
|
||||||
|
- ❌ modal comme premier réflexe UX
|
||||||
|
- ❌ em-dashes (`—` / `--`) dans les textes UI
|
||||||
|
- ❌ Inter par défaut sans raison
|
||||||
|
- ❌ gradients violets sans justification
|
||||||
|
- ❌ dark glow décoratif
|
||||||
|
|
||||||
|
## 4. Tokens — conventions de nommage
|
||||||
|
|
||||||
|
- Nommage par **rôle**, jamais par couleur brute : `--primary`, pas `--green`. Un thème peut changer toute la palette sans toucher un composant.
|
||||||
|
- Préfixes d'échelle : `--fs-*` (font-size), `--lh-*` (line-height), `--fw-*` (weight), `--space-*`, `--radius-*`, `--shadow-*`, `--dur-*`, `--ease-*`, `--z-*`.
|
||||||
|
- Paires de contraste : `--primary`/`--on-primary`, `--accent`/`--on-accent`. Toujours utiliser le `on-*` correspondant comme couleur de texte sur ces fonds.
|
||||||
|
- **Layers** : `tokens.css` vit dans `@layer tf-tokens`, `reset.css` dans `@layer tf-reset`. Les thèmes et le CSS des sites restent **non-layerés** → ils gagnent toujours sur la fondation, quel que soit l'ordre de chargement. Ne jamais mettre un thème dans un layer. (Exception voulue : le kill-switch reduced-motion du reset est en `!important` layeré, il bat donc tout le monde.)
|
||||||
|
- Le schéma (noms + échelles) est **universel** et gelé : en ajouter demande une mise à jour ici + dans `tokens.css` + dans les thèmes. En retirer/renommer = breaking change pour tous les sites.
|
||||||
|
- **Tailwind 4** (site racine) : les tokens sont mappés via `tailwind.css` (`@theme inline`). Les namespaces en collision de nom (`--font-*`, `--radius-*`, `--shadow-*`, `--ease-*`) s'utilisent en syntaxe var arbitraire : `rounded-(--radius-md)`, `shadow-(--shadow-md)`, `ease-(--ease-out)`, `font-(family-name:--font-heading)`.
|
||||||
|
|
||||||
|
## 5. Ajouter un thème (nouveau site)
|
||||||
|
|
||||||
|
1. `cp theme/base.css theme/mon-site.css`
|
||||||
|
2. Remplacer palette claire, palette sombre (bloc `[data-theme="dark"]` **et** duplication `@media (prefers-color-scheme: dark)` avec le garde `:not([data-theme="light"])`), et les deux stacks de polices.
|
||||||
|
3. Ne toucher à **rien d'autre** : pas d'échelles, pas de spacing, pas de motion dans un thème.
|
||||||
|
4. Vérifier les contrastes **AA** (4.5:1 texte courant, 3:1 grands titres/UI) dans les deux modes — y compris `--on-primary` sur `--primary` et `--on-accent` sur `--accent`.
|
||||||
|
5. Charger les `@font-face` côté site (self-host / fontsource) ; le thème ne définit que les stacks.
|
||||||
|
6. L'ajouter à la page démo (`src/pages/demo/mon-site.astro`, 4 lignes) et valider le kitchen sink.
|
||||||
|
|
||||||
|
Clair/sombre : mode clair par défaut + sombre via préférence système, le toggle explicite (`data-theme`) **gagne toujours dans les deux sens**. Un site peut être sombre par défaut (cf. `theme/aep.css` : `:root` sombre, variante claire sur `[data-theme="light"]` seulement).
|
||||||
|
|
||||||
|
## 6. Accessibilité — socle
|
||||||
|
|
||||||
|
- Contraste **AA** partout, dans les deux modes.
|
||||||
|
- **Focus visible** partout : `:focus-visible` global dans `reset.css`, ne jamais le supprimer sans remplacement équivalent.
|
||||||
|
- Navigation **clavier** complète : hamburger avec `aria-expanded`/`aria-controls`, fermeture Échap, focus trap panneau ouvert, skip-link (`BaseLayout`).
|
||||||
|
- `lang` défini sur `<html>` (prop de `BaseLayout`), `aria-current="page"` sur le lien actif, `alt` sur toute image porteuse de sens.
|
||||||
|
|
||||||
|
## 7. Definition-of-Done — checklist de fin de build
|
||||||
|
|
||||||
|
Avant de déclarer un site « fini » :
|
||||||
|
|
||||||
|
- [ ] Testé à **360, 768, 1280** px — aucun débordement horizontal, hiérarchie lisible aux trois tailles.
|
||||||
|
- [ ] Testé en **clair ET sombre** (préférence système + toggle) — contrastes AA dans les deux.
|
||||||
|
- [ ] Testé avec **prefers-reduced-motion: reduce** — plus aucun mouvement non essentiel.
|
||||||
|
- [ ] Testé **sans JS** — contenu et navigation accessibles (liens visibles, rien de masqué).
|
||||||
|
- [ ] Navigation **clavier seul** : tout est atteignable, focus visible, hamburger Échap + trap OK.
|
||||||
|
- [ ] Cibles tactiles ≥ 44px sur mobile.
|
||||||
|
- [ ] Zéro valeur en dur qui duplique un token (couleur hex, taille de police, spacing) dans le CSS du site.
|
||||||
|
- [ ] Aucune violation des lois design (§3).
|
||||||
|
- [ ] Le site n'importe qu'**un seul** `theme/*.css` ; en changer suffit à changer le look.
|
||||||
|
- [ ] Lighthouse a11y ≥ 95, aucune erreur console.
|
||||||
100
README.md
Normal file
100
README.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# @transformer/ui
|
||||||
|
|
||||||
|
Design system partagé de la constellation `trans-former.fr` (~6 sites Astro 5 statiques). Une fondation, pas un site : **tokens** (schéma universel), **thèmes** (palette + polices par site), **primitives Astro** (comportements définis une seule fois).
|
||||||
|
|
||||||
|
- Règles et checklist : [`PROTOCOLE.md`](./PROTOCOLE.md)
|
||||||
|
- Démo kitchen sink : `npm run dev` → `/demo` (neutre), `/demo/renovation`, `/demo/aep`
|
||||||
|
|
||||||
|
## Installation dans un site
|
||||||
|
|
||||||
|
### Voie principale — dépendance npm depuis Gitea
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// package.json du site
|
||||||
|
"dependencies": {
|
||||||
|
"@transformer/ui": "git+https://git.trans-former.fr/jules/design-system.git"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Épingler une version : `git+https://git.trans-former.fr/jules/design-system.git#v0.1.0` (tag) ou `#<commit>`. Mettre à jour : `npm update @transformer/ui` (ou réinstaller si épinglé).
|
||||||
|
|
||||||
|
### Fallback — git submodule
|
||||||
|
|
||||||
|
Si npm-from-git pose souci (réseau, auth CI) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule add https://git.trans-former.fr/jules/design-system.git vendor/ui
|
||||||
|
git submodule update --init
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis dans le `package.json` du site : `"@transformer/ui": "file:./vendor/ui"`.
|
||||||
|
Mise à jour : `git -C vendor/ui pull && git add vendor/ui && git commit`.
|
||||||
|
|
||||||
|
## Consommation
|
||||||
|
|
||||||
|
### Site vanilla (TMIP, renovation…)
|
||||||
|
|
||||||
|
Dans le layout du site :
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import BaseLayout from "@transformer/ui/BaseLayout.astro";
|
||||||
|
import Nav from "@transformer/ui/Nav.astro";
|
||||||
|
import Footer from "@transformer/ui/Footer.astro";
|
||||||
|
// tokens.css et reset.css vivent dans des @layer : le thème (non-layeré)
|
||||||
|
// gagne toujours, quel que soit l'ordre de bundling.
|
||||||
|
import "@transformer/ui/theme/renovation.css";
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="..." description="...">
|
||||||
|
<Nav slot="header" links={[{ href: "/", label: "Accueil" }]}>
|
||||||
|
<a slot="brand" href="/">Mon site</a>
|
||||||
|
</Nav>
|
||||||
|
<slot />
|
||||||
|
<Footer slot="footer">…</Footer>
|
||||||
|
</BaseLayout>
|
||||||
|
```
|
||||||
|
|
||||||
|
`BaseLayout` importe déjà `tokens.css` + `reset.css`. **Changer de thème = changer ce seul import.**
|
||||||
|
|
||||||
|
Primitives : `Button.astro`, `Card.astro`, `Prose.astro`, `Section.astro`, `Container.astro`, `Reveal.astro` (+ `reveal.js` en utilitaire nu). Props documentées en tête de chaque fichier.
|
||||||
|
|
||||||
|
Polices : le thème ne définit que les stacks — le site charge ses `@font-face` (self-host recommandé).
|
||||||
|
|
||||||
|
### Site Tailwind 4 (racine `astro-site-cerveau`)
|
||||||
|
|
||||||
|
La racine garde ses composants Vue mais s'aligne sur les tokens :
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* entrée CSS du site */
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "@transformer/ui/tokens.css";
|
||||||
|
@import "@transformer/ui/theme/base.css"; /* ou le thème racine */
|
||||||
|
@import "@transformer/ui/tailwind.css"; /* pont @theme → utilitaires */
|
||||||
|
```
|
||||||
|
|
||||||
|
Utilitaires générés : `bg-primary`, `text-text-muted`, `border-border`, `text-2xl` (échelle fluide), `p-md`, `gap-xl`… Cas en collision de nom (non mappés) : `rounded-(--radius-md)`, `shadow-(--shadow-md)`, `ease-(--ease-out)`, `font-(family-name:--font-heading)`.
|
||||||
|
|
||||||
|
## Structure du repo
|
||||||
|
|
||||||
|
```
|
||||||
|
tokens.css schéma canonique (rôles, échelles, motion) — clair + sombre
|
||||||
|
reset.css reset moderne + kill-switch reduced-motion
|
||||||
|
tailwind.css pont @theme pour Tailwind 4
|
||||||
|
theme/ base (à cloner) · renovation · aep · tmip (placeholder P2)
|
||||||
|
components/ BaseLayout · Nav · Footer · Button · Card · Prose
|
||||||
|
Section · Container · Reveal
|
||||||
|
js/reveal.js apparition au scroll (IntersectionObserver)
|
||||||
|
src/ démo kitchen sink uniquement (pas publiée dans le paquet)
|
||||||
|
PROTOCOLE.md règles mobile-first, motion charter, lois design, DoD
|
||||||
|
```
|
||||||
|
|
||||||
|
## Développement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev # démo sur localhost:4321/demo
|
||||||
|
npm run build # vérification
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajouter un thème : voir `PROTOCOLE.md` §5.
|
||||||
8
astro.config.mjs
Normal file
8
astro.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
|
||||||
|
// Config minimale : ce repo est une librairie ; Astro ne sert qu'à la démo.
|
||||||
|
export default defineConfig({
|
||||||
|
redirects: {
|
||||||
|
"/": "/demo",
|
||||||
|
},
|
||||||
|
});
|
||||||
94
components/BaseLayout.astro
Normal file
94
components/BaseLayout.astro
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* BaseLayout — coquille HTML commune à tous les sites.
|
||||||
|
*
|
||||||
|
* Props :
|
||||||
|
* - title (string, requis) : <title> + og:title
|
||||||
|
* - description (string) : meta description + og:description
|
||||||
|
* - lang (string, 'fr') : attribut lang du <html>
|
||||||
|
* - ogImage (string) : URL absolue de l'image OG
|
||||||
|
* - canonical (string) : URL canonique
|
||||||
|
*
|
||||||
|
* Slots : default (contenu du <main>), header, footer, head (metas en plus).
|
||||||
|
*
|
||||||
|
* Le THÈME n'est pas importé ici : le site importe son theme/*.css dans son
|
||||||
|
* propre layout, à côté de <BaseLayout> (cf. README).
|
||||||
|
* Gestion clair/sombre : data-theme posé avant le premier paint depuis
|
||||||
|
* localStorage('tf-theme') ; helper global window.tfSetTheme('dark'|'light'|'auto').
|
||||||
|
*/
|
||||||
|
import '../tokens.css';
|
||||||
|
import '../reset.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
lang?: string;
|
||||||
|
ogImage?: string;
|
||||||
|
canonical?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, lang = 'fr', ogImage, canonical } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang={lang}>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{title}</title>
|
||||||
|
{description && <meta name="description" content={description} />}
|
||||||
|
{canonical && <link rel="canonical" href={canonical} />}
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
{description && <meta property="og:description" content={description} />}
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
{ogImage && <meta property="og:image" content={ogImage} />}
|
||||||
|
<script is:inline>
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const t = localStorage.getItem("tf-theme");
|
||||||
|
if (t === "dark" || t === "light") document.documentElement.dataset.theme = t;
|
||||||
|
} catch {}
|
||||||
|
document.documentElement.dataset.js = "";
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<slot name="head" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a class="tf-skip-link" href="#main">Aller au contenu</a>
|
||||||
|
<slot name="header" />
|
||||||
|
<main id="main"><slot /></main>
|
||||||
|
<slot name="footer" />
|
||||||
|
<script is:inline>
|
||||||
|
window.tfSetTheme = (mode) => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
try {
|
||||||
|
if (mode === "auto") {
|
||||||
|
delete root.dataset.theme;
|
||||||
|
localStorage.removeItem("tf-theme");
|
||||||
|
} else {
|
||||||
|
root.dataset.theme = mode;
|
||||||
|
localStorage.setItem("tf-theme", mode);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tf-skip-link {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: var(--space-md);
|
||||||
|
inset-block-start: var(--space-md);
|
||||||
|
z-index: var(--z-toast);
|
||||||
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--on-primary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-decoration: none;
|
||||||
|
transform: translateY(-200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-skip-link:focus-visible {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
142
components/Button.astro
Normal file
142
components/Button.astro
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Button — bouton ou lien-bouton.
|
||||||
|
*
|
||||||
|
* Props :
|
||||||
|
* - variant ('primary' | 'secondary' | 'ghost' | 'link', 'primary')
|
||||||
|
* - size ('sm' | 'md' | 'lg', 'md') — sur écran tactile, sm remonte à 44px
|
||||||
|
* - href (string) : rend un <a> au lieu d'un <button>
|
||||||
|
* - type ('button' | 'submit' | 'reset', 'button') — ignoré si href
|
||||||
|
* - disabled (boolean, false) — sur <a> : aria-disabled + href retiré
|
||||||
|
* - class (string) : classes additionnelles
|
||||||
|
* + tout autre attribut est passé tel quel (aria-*, data-*, target…)
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
variant?: "primary" | "secondary" | "ghost" | "link";
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
href?: string;
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
|
disabled?: boolean;
|
||||||
|
class?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
variant = "primary",
|
||||||
|
size = "md",
|
||||||
|
href,
|
||||||
|
type = "button",
|
||||||
|
disabled = false,
|
||||||
|
class: className,
|
||||||
|
...rest
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const Tag = href ? "a" : "button";
|
||||||
|
const attrs = href
|
||||||
|
? disabled
|
||||||
|
? { "aria-disabled": "true", tabindex: "-1" }
|
||||||
|
: { href }
|
||||||
|
: { type, disabled: disabled || undefined };
|
||||||
|
---
|
||||||
|
|
||||||
|
<Tag
|
||||||
|
class:list={["tf-btn", `tf-btn--${variant}`, `tf-btn--${size}`, className]}
|
||||||
|
{...attrs}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Tag>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tf-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
min-height: 2.75rem; /* 44px — cible tactile */
|
||||||
|
padding-inline: var(--space-lg);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
line-height: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--dur-fast) var(--ease-out),
|
||||||
|
color var(--dur-fast) var(--ease-out),
|
||||||
|
border-color var(--dur-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tailles */
|
||||||
|
.tf-btn--sm {
|
||||||
|
min-height: 2.25rem;
|
||||||
|
padding-inline: var(--space-md);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-btn--lg {
|
||||||
|
min-height: 3rem;
|
||||||
|
padding-inline: var(--space-xl);
|
||||||
|
font-size: var(--fs-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sur pointeur tactile, aucune taille ne descend sous 44px */
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.tf-btn--sm {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variantes */
|
||||||
|
.tf-btn--primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--on-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-btn--primary:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
color: var(--on-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-btn--secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-btn--secondary:hover {
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-btn--ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-btn--ghost:hover {
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-btn--link {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--link);
|
||||||
|
padding-inline: var(--space-2xs);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-btn--link:hover {
|
||||||
|
color: var(--link-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* États désactivés */
|
||||||
|
.tf-btn:disabled,
|
||||||
|
.tf-btn[aria-disabled="true"] {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
components/Card.astro
Normal file
71
components/Card.astro
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Card — surface de contenu, cliquable ou non.
|
||||||
|
*
|
||||||
|
* Props :
|
||||||
|
* - href (string) : rend toute la carte cliquable (<a>) + hover élévation légère
|
||||||
|
* - class (string) : classes additionnelles
|
||||||
|
*
|
||||||
|
* Slots : media (image/visuel pleine largeur en tête), default (corps).
|
||||||
|
* Pas de border-left accent (loi Impeccable) : la hiérarchie passe par la
|
||||||
|
* surface, la bordure fine et l'élévation au hover.
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
href?: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { href, class: className } = Astro.props;
|
||||||
|
const Tag = href ? "a" : "article";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Tag class:list={["tf-card", { "tf-card--link": href }, className]} href={href}>
|
||||||
|
{
|
||||||
|
Astro.slots.has("media") && (
|
||||||
|
<div class="tf-card__media">
|
||||||
|
<slot name="media" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="tf-card__body"><slot /></div>
|
||||||
|
</Tag>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tf-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-card--link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform var(--dur-base) var(--ease-out),
|
||||||
|
box-shadow var(--dur-base) var(--ease-out),
|
||||||
|
border-color var(--dur-base) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-card--link:hover {
|
||||||
|
color: inherit;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-card__media :global(img) {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-card__body {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
components/Container.astro
Normal file
31
components/Container.astro
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Container — largeur max + gouttières cohérentes.
|
||||||
|
*
|
||||||
|
* Props :
|
||||||
|
* - size ('base' | 'narrow', 'base') : --container-max ou 45rem (lecture)
|
||||||
|
* - class (string) : classes additionnelles
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
size?: "base" | "narrow";
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { size = "base", class: className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={["tf-container", { "tf-container--narrow": size === "narrow" }, className]}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tf-container {
|
||||||
|
max-width: var(--container-max);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--container-pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-container--narrow {
|
||||||
|
max-width: 45rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
components/Footer.astro
Normal file
50
components/Footer.astro
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Footer — pied de page commun.
|
||||||
|
*
|
||||||
|
* Slots : default (colonnes / liens), legal (ligne basse : mentions, copyright).
|
||||||
|
*/
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="tf-footer">
|
||||||
|
<div class="tf-footer__inner">
|
||||||
|
<div class="tf-footer__content"><slot /></div>
|
||||||
|
<div class="tf-footer__legal"><slot name="legal" /></div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tf-footer {
|
||||||
|
margin-block-start: auto;
|
||||||
|
background: var(--bg-alt);
|
||||||
|
border-block-start: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-footer__inner {
|
||||||
|
max-width: var(--container-max);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--container-pad);
|
||||||
|
padding-block: var(--space-xl);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-footer__content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-lg) var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-footer__legal {
|
||||||
|
border-block-start: 1px solid var(--border);
|
||||||
|
padding-block-start: var(--space-md);
|
||||||
|
color: var(--text-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-footer__legal:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
292
components/Nav.astro
Normal file
292
components/Nav.astro
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Nav — LA nav de référence de la constellation. Définie une seule fois ici.
|
||||||
|
*
|
||||||
|
* Comportement :
|
||||||
|
* - sticky en haut, se condense après 16px de scroll (padding réduit)
|
||||||
|
* - mobile (< 768px) : hamburger accessible — aria-expanded, aria-controls,
|
||||||
|
* fermeture Échap, focus trap dans le panneau ouvert, retour focus au bouton
|
||||||
|
* - sans JS : les liens restent visibles (rangée wrappée), rien n'est cassé
|
||||||
|
* - lien actif : aria-current="page" calculé depuis Astro.url.pathname
|
||||||
|
*
|
||||||
|
* Props :
|
||||||
|
* - links (NavLink[], requis) : { href, label }[]
|
||||||
|
* - label (string, 'Navigation principale') : aria-label du <nav>
|
||||||
|
*
|
||||||
|
* Slots : brand (logo / nom du site, à gauche), actions (CTA / toggle thème,
|
||||||
|
* rendu dans le panneau).
|
||||||
|
*/
|
||||||
|
export interface NavLink {
|
||||||
|
href: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
links: NavLink[];
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { links, label = "Navigation principale" } = Astro.props;
|
||||||
|
const current = Astro.url.pathname.replace(/\/$/, "") || "/";
|
||||||
|
const isActive = (href: string) => {
|
||||||
|
const h = href.replace(/\/$/, "") || "/";
|
||||||
|
return h === "/" ? current === "/" : current === h || current.startsWith(h + "/");
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="tf-nav" data-tf-nav>
|
||||||
|
<nav aria-label={label}>
|
||||||
|
<div class="tf-nav__inner">
|
||||||
|
<div class="tf-nav__brand"><slot name="brand" /></div>
|
||||||
|
<button
|
||||||
|
class="tf-nav__toggle"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="tf-nav-panel"
|
||||||
|
data-tf-nav-toggle
|
||||||
|
>
|
||||||
|
<span class="tf-nav__toggle-box" aria-hidden="true"
|
||||||
|
><span class="tf-nav__toggle-bar"></span></span
|
||||||
|
>
|
||||||
|
<span>Menu</span>
|
||||||
|
</button>
|
||||||
|
<div class="tf-nav__panel" id="tf-nav-panel" data-tf-nav-panel>
|
||||||
|
<ul class="tf-nav__list">
|
||||||
|
{
|
||||||
|
links.map((l) => (
|
||||||
|
<li>
|
||||||
|
<a href={l.href} aria-current={isActive(l.href) ? "page" : undefined}>
|
||||||
|
{l.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<div class="tf-nav__actions"><slot name="actions" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tf-nav {
|
||||||
|
position: sticky;
|
||||||
|
inset-block-start: 0;
|
||||||
|
z-index: var(--z-nav);
|
||||||
|
background: var(--bg);
|
||||||
|
border-block-end: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__inner {
|
||||||
|
max-width: var(--container-max);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--container-pad);
|
||||||
|
padding-block: var(--space-md);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs) var(--space-lg);
|
||||||
|
transition: padding-block var(--dur-base) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav.is-scrolled .tf-nav__inner {
|
||||||
|
padding-block: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__brand {
|
||||||
|
margin-inline-end: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__brand :global(a) {
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hamburger — caché tant que JS n'a pas pris la main, et toujours dès md */
|
||||||
|
.tf-nav__toggle {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
min-height: 2.75rem;
|
||||||
|
min-width: 2.75rem;
|
||||||
|
padding: var(--space-2xs) var(--space-sm);
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__toggle-box {
|
||||||
|
display: block;
|
||||||
|
width: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__toggle-bar,
|
||||||
|
.tf-nav__toggle-box::before,
|
||||||
|
.tf-nav__toggle-box::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__toggle-bar {
|
||||||
|
margin-block: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__panel {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs) var(--space-md);
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2xs) var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__list a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
padding-inline: var(--space-sm);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--dur-fast) var(--ease-out),
|
||||||
|
background-color var(--dur-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__list a:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__list a[aria-current="page"] {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-nav__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile avec JS : hamburger visible, panneau replié en accordéon */
|
||||||
|
@media (max-width: 47.9375rem) {
|
||||||
|
:global(html[data-js]) .tf-nav__toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-js]) .tf-nav__panel {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
padding-block: var(--space-sm) var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-js]) .tf-nav.is-open .tf-nav__panel {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-js]) .tf-nav__list {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop : panneau inline, jamais de hamburger */
|
||||||
|
@media (min-width: 48rem) {
|
||||||
|
.tf-nav__panel {
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const nav = document.querySelector<HTMLElement>("[data-tf-nav]");
|
||||||
|
|
||||||
|
if (nav) {
|
||||||
|
const toggle = nav.querySelector<HTMLButtonElement>("[data-tf-nav-toggle]")!;
|
||||||
|
const panel = nav.querySelector<HTMLElement>("[data-tf-nav-panel]")!;
|
||||||
|
const mqDesktop = window.matchMedia("(min-width: 48rem)");
|
||||||
|
|
||||||
|
// Condensation au scroll (sticky-puis-compact)
|
||||||
|
let ticking = false;
|
||||||
|
const onScroll = () => {
|
||||||
|
if (ticking) return;
|
||||||
|
ticking = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
nav.classList.toggle("is-scrolled", window.scrollY > 16);
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
onScroll();
|
||||||
|
|
||||||
|
const isOpen = () => toggle.getAttribute("aria-expanded") === "true";
|
||||||
|
|
||||||
|
const focusables = () =>
|
||||||
|
Array.from(
|
||||||
|
panel.querySelectorAll<HTMLElement>("a[href], button:not([disabled])")
|
||||||
|
);
|
||||||
|
|
||||||
|
const close = (returnFocus = true) => {
|
||||||
|
toggle.setAttribute("aria-expanded", "false");
|
||||||
|
nav.classList.remove("is-open");
|
||||||
|
if (returnFocus) toggle.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
toggle.setAttribute("aria-expanded", "true");
|
||||||
|
nav.classList.add("is-open");
|
||||||
|
focusables()[0]?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
toggle.addEventListener("click", () => (isOpen() ? close() : open()));
|
||||||
|
|
||||||
|
nav.addEventListener("keydown", (e) => {
|
||||||
|
if (mqDesktop.matches || !isOpen()) return;
|
||||||
|
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus trap : Tab circule entre le bouton et le panneau ouvert
|
||||||
|
if (e.key === "Tab") {
|
||||||
|
const items = [toggle, ...focusables()];
|
||||||
|
const first = items[0];
|
||||||
|
const last = items[items.length - 1];
|
||||||
|
if (e.shiftKey && document.activeElement === first) {
|
||||||
|
e.preventDefault();
|
||||||
|
last.focus();
|
||||||
|
} else if (!e.shiftKey && document.activeElement === last) {
|
||||||
|
e.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.addEventListener("click", (e) => {
|
||||||
|
if ((e.target as HTMLElement).closest("a")) close(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
mqDesktop.addEventListener("change", () => close(false));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
129
components/Prose.astro
Normal file
129
components/Prose.astro
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Prose — enveloppe de contenu long (article, page éditoriale).
|
||||||
|
* Style tout le HTML slotté (titres, listes, blockquote, code, images, tables)
|
||||||
|
* depuis les tokens. Largeur de lecture confortable (65ch).
|
||||||
|
*
|
||||||
|
* Props :
|
||||||
|
* - class (string) : classes additionnelles
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { class: className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={["tf-prose", className]}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tf-prose {
|
||||||
|
max-width: 65ch;
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
line-height: var(--lh-loose);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose > :global(* + *) {
|
||||||
|
margin-block-start: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(h1) {
|
||||||
|
font-size: var(--fs-3xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(h2) {
|
||||||
|
font-size: var(--fs-2xl);
|
||||||
|
margin-block-start: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(h3) {
|
||||||
|
font-size: var(--fs-xl);
|
||||||
|
margin-block-start: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(h4) {
|
||||||
|
font-size: var(--fs-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(ul),
|
||||||
|
.tf-prose :global(ol) {
|
||||||
|
padding-inline-start: 1.5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(li::marker) {
|
||||||
|
color: var(--text-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(blockquote) {
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(blockquote cite) {
|
||||||
|
display: block;
|
||||||
|
margin-block-start: var(--space-xs);
|
||||||
|
font-style: normal;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--text-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(code) {
|
||||||
|
background: var(--surface-2);
|
||||||
|
padding: 0.15em 0.4em;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(pre) {
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(pre code) {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(img) {
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(hr) {
|
||||||
|
border: none;
|
||||||
|
border-block-start: 1px solid var(--border);
|
||||||
|
margin-block: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(table) {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(th),
|
||||||
|
.tf-prose :global(td) {
|
||||||
|
text-align: start;
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
border-block-end: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(th) {
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-prose :global(a) {
|
||||||
|
text-underline-offset: 0.2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
components/Reveal.astro
Normal file
49
components/Reveal.astro
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Reveal — apparition au scroll (fade + léger translate), sobre.
|
||||||
|
*
|
||||||
|
* Props :
|
||||||
|
* - delay (number, 0) : décalage en ms (pour échelonner une grille)
|
||||||
|
* - class (string) : classes additionnelles
|
||||||
|
*
|
||||||
|
* Motion piloté-utilisateur : déclenché par le scroll (IntersectionObserver).
|
||||||
|
* Sous prefers-reduced-motion: reduce → contenu visible immédiatement, aucun
|
||||||
|
* mouvement. Sans JS → rien n'est masqué (garde html[data-js]).
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
delay?: number;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { delay = 0, class: className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class:list={["tf-reveal", className]}
|
||||||
|
data-reveal
|
||||||
|
style={delay ? `--reveal-delay: ${delay}ms` : undefined}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
:global(html[data-js]) .tf-reveal {
|
||||||
|
opacity: 0;
|
||||||
|
translate: 0 12px;
|
||||||
|
transition: opacity var(--dur-slow) var(--ease-out),
|
||||||
|
translate var(--dur-slow) var(--ease-out);
|
||||||
|
transition-delay: var(--reveal-delay, 0ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-js]) .tf-reveal.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
translate: 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { initReveal } from "../js/reveal.js";
|
||||||
|
initReveal();
|
||||||
|
</script>
|
||||||
40
components/Section.astro
Normal file
40
components/Section.astro
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Section — rythme vertical cohérent entre blocs de page.
|
||||||
|
*
|
||||||
|
* Props :
|
||||||
|
* - alt (boolean, false) : fond alterné (--bg-alt) pour rythmer la page
|
||||||
|
* - size ('base' | 'lg', 'base') : padding vertical --space-2xl ou --space-3xl
|
||||||
|
* - id (string) : ancre
|
||||||
|
* - class (string) : classes additionnelles
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
alt?: boolean;
|
||||||
|
size?: "base" | "lg";
|
||||||
|
id?: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { alt = false, size = "base", id, class: className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class:list={["tf-section", { "tf-section--alt": alt, "tf-section--lg": size === "lg" }, className]}
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tf-section {
|
||||||
|
padding-block: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-section--lg {
|
||||||
|
padding-block: var(--space-3xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-section--alt {
|
||||||
|
background: var(--bg-alt);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
39
js/reveal.js
Normal file
39
js/reveal.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* reveal.js — apparition au scroll via IntersectionObserver.
|
||||||
|
*
|
||||||
|
* Budget de mouvement sobre : fade + translateY(12px), durée --dur-slow.
|
||||||
|
* Sous prefers-reduced-motion: reduce, tout est visible immédiatement.
|
||||||
|
* Progressif : sans JS, le CSS ne masque rien (garde html[data-js]).
|
||||||
|
*
|
||||||
|
* Usage : import { initReveal } from '@transformer/ui/reveal.js'; initReveal();
|
||||||
|
* (Reveal.astro l'appelle déjà pour ses instances.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
let observer;
|
||||||
|
|
||||||
|
export function initReveal(selector = "[data-reveal]") {
|
||||||
|
document.documentElement.dataset.js = "";
|
||||||
|
|
||||||
|
const targets = document.querySelectorAll(selector);
|
||||||
|
|
||||||
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||||
|
targets.forEach((el) => el.classList.add("is-visible"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
observer ??= new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add("is-visible");
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.15, rootMargin: "0px 0px -10% 0px" }
|
||||||
|
);
|
||||||
|
|
||||||
|
targets.forEach((el) => {
|
||||||
|
if (!el.classList.contains("is-visible")) observer.observe(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
5843
package-lock.json
generated
Normal file
5843
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "@transformer/ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Design system partagé de la constellation trans-former.fr — tokens, thèmes, primitives Astro",
|
||||||
|
"type": "module",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.trans-former.fr/jules/design-system.git"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./tokens.css": "./tokens.css",
|
||||||
|
"./reset.css": "./reset.css",
|
||||||
|
"./tailwind.css": "./tailwind.css",
|
||||||
|
"./theme/*.css": "./theme/*.css",
|
||||||
|
"./BaseLayout.astro": "./components/BaseLayout.astro",
|
||||||
|
"./Nav.astro": "./components/Nav.astro",
|
||||||
|
"./Footer.astro": "./components/Footer.astro",
|
||||||
|
"./Button.astro": "./components/Button.astro",
|
||||||
|
"./Card.astro": "./components/Card.astro",
|
||||||
|
"./Prose.astro": "./components/Prose.astro",
|
||||||
|
"./Section.astro": "./components/Section.astro",
|
||||||
|
"./Container.astro": "./components/Container.astro",
|
||||||
|
"./Reveal.astro": "./components/Reveal.astro",
|
||||||
|
"./reveal.js": "./js/reveal.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"tokens.css",
|
||||||
|
"reset.css",
|
||||||
|
"tailwind.css",
|
||||||
|
"theme",
|
||||||
|
"components",
|
||||||
|
"js",
|
||||||
|
"README.md",
|
||||||
|
"PROTOCOLE.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"astro": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
98
reset.css
Normal file
98
reset.css
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
@transformer/ui — reset.css
|
||||||
|
Reset moderne minimal. À importer APRÈS tokens.css.
|
||||||
|
Vit dans @layer tf-reset : le CSS d'un site (non-layeré) gagne toujours.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@layer tf-reset {
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
hanging-punctuation: first last;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100svh;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
line-height: var(--lh-base);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
line-height: var(--lh-tight);
|
||||||
|
text-wrap: balance;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, li, blockquote {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
img, picture, video, canvas, svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, button, textarea, select {
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--link-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
code, pre, kbd, samp {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus visible partout, jamais supprimé sans remplacement */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus:not(:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Défilement doux uniquement si l'utilisateur accepte le mouvement */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduced motion : coupe tout mouvement non essentiel, globalement */
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} /* @layer tf-reset */
|
||||||
316
src/demo/KitchenSink.astro
Normal file
316
src/demo/KitchenSink.astro
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* KitchenSink — page démo partagée par /demo, /demo/renovation, /demo/aep.
|
||||||
|
* Rend toutes les primitives + les tokens, en clair et sombre (toggle).
|
||||||
|
* Chaque page ne diffère que par le theme/*.css qu'elle importe.
|
||||||
|
*/
|
||||||
|
import BaseLayout from "../../components/BaseLayout.astro";
|
||||||
|
import Nav from "../../components/Nav.astro";
|
||||||
|
import Footer from "../../components/Footer.astro";
|
||||||
|
import Button from "../../components/Button.astro";
|
||||||
|
import Card from "../../components/Card.astro";
|
||||||
|
import Prose from "../../components/Prose.astro";
|
||||||
|
import Section from "../../components/Section.astro";
|
||||||
|
import Container from "../../components/Container.astro";
|
||||||
|
import Reveal from "../../components/Reveal.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { themeName } = Astro.props;
|
||||||
|
|
||||||
|
const navLinks = [
|
||||||
|
{ href: "/demo", label: "Neutre" },
|
||||||
|
{ href: "/demo/renovation", label: "Renovation" },
|
||||||
|
{ href: "/demo/aep", label: "AEP" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const colorRoles = [
|
||||||
|
"bg", "bg-alt", "surface", "surface-2",
|
||||||
|
"text", "text-muted", "text-subtle",
|
||||||
|
"border", "border-strong",
|
||||||
|
"primary", "primary-hover", "on-primary",
|
||||||
|
"accent", "on-accent", "link", "link-hover",
|
||||||
|
"focus-ring", "success", "warning", "danger",
|
||||||
|
];
|
||||||
|
|
||||||
|
const typeScale = ["xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl"];
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title={`Démo — thème ${themeName} — @transformer/ui`}
|
||||||
|
description="Kitchen sink du design system trans-former.fr : tokens, thèmes, primitives."
|
||||||
|
>
|
||||||
|
<Nav slot="header" links={navLinks}>
|
||||||
|
<a slot="brand" href="/demo">@transformer/ui</a>
|
||||||
|
<Fragment slot="actions">
|
||||||
|
<button class="ks-theme-btn" type="button" onclick="tfSetTheme('light')">Clair</button>
|
||||||
|
<button class="ks-theme-btn" type="button" onclick="tfSetTheme('dark')">Sombre</button>
|
||||||
|
<button class="ks-theme-btn" type="button" onclick="tfSetTheme('auto')">Auto</button>
|
||||||
|
</Fragment>
|
||||||
|
</Nav>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<p class="ks-kicker">Thème actif : {themeName}</p>
|
||||||
|
<h1 class="ks-h1">Le design system de la constellation trans-former.fr</h1>
|
||||||
|
<p class="ks-lead">
|
||||||
|
Tokens, thèmes et primitives partagés. Changer de thème = changer un seul
|
||||||
|
import CSS. Cette page rend tout le système d'un coup d'œil.
|
||||||
|
</p>
|
||||||
|
<div class="ks-row">
|
||||||
|
<Button href="#boutons">Voir les boutons</Button>
|
||||||
|
<Button variant="secondary" href="#prose">Voir la prose</Button>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section alt id="couleurs">
|
||||||
|
<Container>
|
||||||
|
<h2 class="ks-h2">Couleurs — rôles</h2>
|
||||||
|
<div class="ks-swatches">
|
||||||
|
{
|
||||||
|
colorRoles.map((role) => (
|
||||||
|
<div class="ks-swatch">
|
||||||
|
<span class="ks-swatch__chip" style={`background: var(--${role})`} />
|
||||||
|
<code>--{role}</code>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="typo">
|
||||||
|
<Container>
|
||||||
|
<h2 class="ks-h2">Typographie — échelle fluide</h2>
|
||||||
|
<div class="ks-type">
|
||||||
|
{
|
||||||
|
typeScale.map((s) => (
|
||||||
|
<p style={`font-size: var(--fs-${s})`}>
|
||||||
|
<code>--fs-{s}</code> — Transformer l'habitat, pas le remplacer.
|
||||||
|
</p>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section alt id="boutons">
|
||||||
|
<Container>
|
||||||
|
<h2 class="ks-h2">Boutons — variantes × tailles × états</h2>
|
||||||
|
<div class="ks-row">
|
||||||
|
<Button>Primary</Button>
|
||||||
|
<Button variant="secondary">Secondary</Button>
|
||||||
|
<Button variant="ghost">Ghost</Button>
|
||||||
|
<Button variant="link">Link</Button>
|
||||||
|
</div>
|
||||||
|
<div class="ks-row">
|
||||||
|
<Button size="sm">Small</Button>
|
||||||
|
<Button size="md">Medium</Button>
|
||||||
|
<Button size="lg">Large</Button>
|
||||||
|
</div>
|
||||||
|
<div class="ks-row">
|
||||||
|
<Button disabled>Désactivé</Button>
|
||||||
|
<Button variant="secondary" disabled>Désactivé</Button>
|
||||||
|
<Button href="#boutons" variant="secondary">Lien-bouton</Button>
|
||||||
|
<Button href="#boutons" disabled>Lien désactivé</Button>
|
||||||
|
</div>
|
||||||
|
<p class="ks-note">
|
||||||
|
Focus visible : naviguer au clavier (Tab) pour voir l'anneau
|
||||||
|
<code>--focus-ring</code>. Cibles tactiles ≥ 44px.
|
||||||
|
</p>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="cards">
|
||||||
|
<Container>
|
||||||
|
<h2 class="ks-h2">Cards</h2>
|
||||||
|
<div class="ks-grid">
|
||||||
|
<Card href="#cards">
|
||||||
|
<svg slot="media" viewBox="0 0 320 180" role="img" aria-label="Visuel de démonstration">
|
||||||
|
<rect width="320" height="180" fill="var(--surface-2)"></rect>
|
||||||
|
<circle cx="160" cy="90" r="48" fill="var(--primary)"></circle>
|
||||||
|
</svg>
|
||||||
|
<h3>Carte cliquable</h3>
|
||||||
|
<p>Toute la surface est le lien. Hover : élévation légère, pas de border accent.</p>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<h3>Carte statique</h3>
|
||||||
|
<p>Surface, bordure fine, rayon large. Le contenu vit dans le slot par défaut.</p>
|
||||||
|
<div><Button variant="link" href="#cards">Action locale</Button></div>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<h3>États système</h3>
|
||||||
|
<p>
|
||||||
|
<span class="ks-dot" style="background: var(--success)"></span> succès ·
|
||||||
|
<span class="ks-dot" style="background: var(--warning)"></span> avertissement ·
|
||||||
|
<span class="ks-dot" style="background: var(--danger)"></span> erreur
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section alt id="prose">
|
||||||
|
<Container>
|
||||||
|
<h2 class="ks-h2">Prose — contenu long</h2>
|
||||||
|
<Prose>
|
||||||
|
<h3>Rénover plutôt que construire</h3>
|
||||||
|
<p>
|
||||||
|
Le bâti existant est un gisement. Une rénovation performante mobilise
|
||||||
|
<a href="#prose">l'isolation</a>, la ventilation et le pilotage des
|
||||||
|
apports — dans cet ordre.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Isolation des parois opaques</li>
|
||||||
|
<li>Menuiseries et étanchéité à l'air</li>
|
||||||
|
<li>Ventilation double flux si pertinent</li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
La maison la plus écologique est celle qui existe déjà.
|
||||||
|
<cite>Adage de la rénovation</cite>
|
||||||
|
</blockquote>
|
||||||
|
<p>
|
||||||
|
Un extrait de code s'écrit <code>var(--primary)</code> en ligne, ou en bloc :
|
||||||
|
</p>
|
||||||
|
<pre><code>import '@transformer/ui/tokens.css';
|
||||||
|
import '@transformer/ui/theme/renovation.css';</code></pre>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Poste</th><th>Gain</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Isolation toiture</td><td>25–30 %</td></tr>
|
||||||
|
<tr><td>Menuiseries</td><td>10–15 %</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Prose>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="reveal">
|
||||||
|
<Container>
|
||||||
|
<h2 class="ks-h2">Reveal — apparition au scroll</h2>
|
||||||
|
<p class="ks-note">
|
||||||
|
Fade + translate 12px, désactivé sous <code>prefers-reduced-motion</code>.
|
||||||
|
Sans JS, rien n'est masqué.
|
||||||
|
</p>
|
||||||
|
<div class="ks-grid">
|
||||||
|
<Reveal><Card><h3>Bloc 1</h3><p>Apparaît en entrant dans le viewport.</p></Card></Reveal>
|
||||||
|
<Reveal delay={80}><Card><h3>Bloc 2</h3><p>Décalé de 80 ms.</p></Card></Reveal>
|
||||||
|
<Reveal delay={160}><Card><h3>Bloc 3</h3><p>Décalé de 160 ms.</p></Card></Reveal>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Footer slot="footer">
|
||||||
|
<div>
|
||||||
|
<strong>@transformer/ui</strong>
|
||||||
|
<p>Design system de la constellation trans-former.fr.</p>
|
||||||
|
</div>
|
||||||
|
<Fragment slot="legal">v0.1.0 — démo kitchen sink. Testée à 360 / 768 / 1280.</Fragment>
|
||||||
|
</Footer>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ks-kicker {
|
||||||
|
color: var(--text-subtle);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-h1 {
|
||||||
|
font-size: var(--fs-4xl);
|
||||||
|
margin-block: var(--space-sm) var(--space-md);
|
||||||
|
max-width: 22ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-lead {
|
||||||
|
font-size: var(--fs-lg);
|
||||||
|
color: var(--text-muted);
|
||||||
|
max-width: 55ch;
|
||||||
|
margin-block-end: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-h2 {
|
||||||
|
font-size: var(--fs-2xl);
|
||||||
|
margin-block-end: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
margin-block-end: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-note {
|
||||||
|
color: var(--text-subtle);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
margin-block-end: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-swatches {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-swatch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-swatch__chip {
|
||||||
|
inline-size: 2rem;
|
||||||
|
block-size: 2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-type {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 48rem) {
|
||||||
|
.ks-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-dot {
|
||||||
|
display: inline-block;
|
||||||
|
inline-size: 0.75rem;
|
||||||
|
block-size: 0.75rem;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-theme-btn {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
padding-inline: var(--space-sm);
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-theme-btn:hover {
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
6
src/pages/demo.astro
Normal file
6
src/pages/demo.astro
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
// Démo — thème NEUTRE : aucun theme/*.css importé, tokens.css seul.
|
||||||
|
import KitchenSink from "../demo/KitchenSink.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<KitchenSink themeName="neutre (tokens seuls)" />
|
||||||
7
src/pages/demo/aep.astro
Normal file
7
src/pages/demo/aep.astro
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
// Démo — thème AEP (sombre par défaut, accent rouge).
|
||||||
|
import KitchenSink from "../../demo/KitchenSink.astro";
|
||||||
|
import "../../../theme/aep.css";
|
||||||
|
---
|
||||||
|
|
||||||
|
<KitchenSink themeName="AEP (sombre par défaut)" />
|
||||||
9
src/pages/demo/renovation.astro
Normal file
9
src/pages/demo/renovation.astro
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
// Démo — thème RENOVATION. Un seul import CSS change tout le look.
|
||||||
|
// tokens.css est layeré (@layer tf-tokens) : le thème gagne toujours,
|
||||||
|
// quel que soit l'ordre de bundling.
|
||||||
|
import KitchenSink from "../../demo/KitchenSink.astro";
|
||||||
|
import "../../../theme/renovation.css";
|
||||||
|
---
|
||||||
|
|
||||||
|
<KitchenSink themeName="renovation (Brunswick green)" />
|
||||||
73
tailwind.css
Normal file
73
tailwind.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
@transformer/ui — tailwind.css
|
||||||
|
Pont Tailwind 4 pour les sites qui l'utilisent (racine astro-site-cerveau).
|
||||||
|
À importer APRÈS tailwindcss et les tokens :
|
||||||
|
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "@transformer/ui/tokens.css";
|
||||||
|
@import "@transformer/ui/theme/mon-site.css";
|
||||||
|
@import "@transformer/ui/tailwind.css";
|
||||||
|
|
||||||
|
Mappe les rôles vers les namespaces @theme SANS collision de nom (pas de
|
||||||
|
cycle var()). Les namespaces qui collisionnent avec nos tokens
|
||||||
|
(--font-*, --radius-*, --shadow-*, --ease-*) ne sont PAS mappés :
|
||||||
|
utiliser la syntaxe var arbitraire de Tailwind 4, qui lit directement
|
||||||
|
le token à l'exécution :
|
||||||
|
font-(family-name:--font-heading) · rounded-(--radius-md)
|
||||||
|
shadow-(--shadow-md) · ease-(--ease-out)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
/* Couleurs — utilitaires bg-primary, text-text-muted, border-border, etc. */
|
||||||
|
--color-bg: var(--bg);
|
||||||
|
--color-bg-alt: var(--bg-alt);
|
||||||
|
--color-surface: var(--surface);
|
||||||
|
--color-surface-2: var(--surface-2);
|
||||||
|
--color-text: var(--text);
|
||||||
|
--color-text-muted: var(--text-muted);
|
||||||
|
--color-text-subtle: var(--text-subtle);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-border-strong: var(--border-strong);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-hover: var(--primary-hover);
|
||||||
|
--color-on-primary: var(--on-primary);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-on-accent: var(--on-accent);
|
||||||
|
--color-link: var(--link);
|
||||||
|
--color-link-hover: var(--link-hover);
|
||||||
|
--color-focus-ring: var(--focus-ring);
|
||||||
|
--color-success: var(--success);
|
||||||
|
--color-warning: var(--warning);
|
||||||
|
--color-danger: var(--danger);
|
||||||
|
|
||||||
|
/* Tailles de texte — text-xs … text-4xl (échelle fluide du DS) */
|
||||||
|
--text-xs: var(--fs-xs);
|
||||||
|
--text-sm: var(--fs-sm);
|
||||||
|
--text-base: var(--fs-base);
|
||||||
|
--text-lg: var(--fs-lg);
|
||||||
|
--text-xl: var(--fs-xl);
|
||||||
|
--text-2xl: var(--fs-2xl);
|
||||||
|
--text-3xl: var(--fs-3xl);
|
||||||
|
--text-4xl: var(--fs-4xl);
|
||||||
|
|
||||||
|
/* Interlignes — leading-tight / leading-base / leading-loose */
|
||||||
|
--leading-tight: var(--lh-tight);
|
||||||
|
--leading-base: var(--lh-base);
|
||||||
|
--leading-loose: var(--lh-loose);
|
||||||
|
|
||||||
|
/* Graisses — font-regular / font-medium / font-semibold / font-bold */
|
||||||
|
--font-weight-regular: var(--fw-regular);
|
||||||
|
--font-weight-medium: var(--fw-medium);
|
||||||
|
--font-weight-semibold: var(--fw-semibold);
|
||||||
|
--font-weight-bold: var(--fw-bold);
|
||||||
|
|
||||||
|
/* Spacing nommé — p-2xs … gap-3xl (en plus de l'échelle numérique TW) */
|
||||||
|
--spacing-2xs: var(--space-2xs);
|
||||||
|
--spacing-xs: var(--space-xs);
|
||||||
|
--spacing-sm: var(--space-sm);
|
||||||
|
--spacing-md: var(--space-md);
|
||||||
|
--spacing-lg: var(--space-lg);
|
||||||
|
--spacing-xl: var(--space-xl);
|
||||||
|
--spacing-2xl: var(--space-2xl);
|
||||||
|
--spacing-3xl: var(--space-3xl);
|
||||||
|
}
|
||||||
80
theme/aep.css
Normal file
80
theme/aep.css
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
theme/aep.css — aep.trans-former.fr
|
||||||
|
SOMBRE PAR DÉFAUT : le :root est la palette sombre, quelle que soit la
|
||||||
|
préférence système. Seul un data-theme="light" explicite (toggle) bascule
|
||||||
|
vers la variante claire.
|
||||||
|
Accent : rouge #c0392b.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
--bg: #0d0d0d;
|
||||||
|
--bg-alt: #131313;
|
||||||
|
--surface: #181818;
|
||||||
|
--surface-2: #202020;
|
||||||
|
|
||||||
|
--text: #f2f2f2;
|
||||||
|
--text-muted: #b5b5b5;
|
||||||
|
--text-subtle: #8c8c8c;
|
||||||
|
|
||||||
|
--border: #262626;
|
||||||
|
--border-strong: #3f3f3f;
|
||||||
|
|
||||||
|
--primary: #c0392b;
|
||||||
|
--primary-hover: #d64535;
|
||||||
|
--on-primary: #ffffff;
|
||||||
|
|
||||||
|
--accent: #c0392b;
|
||||||
|
--on-accent: #ffffff;
|
||||||
|
|
||||||
|
--link: #e98b7f;
|
||||||
|
--link-hover: #f2a99f;
|
||||||
|
|
||||||
|
--focus-ring: #e98b7f;
|
||||||
|
|
||||||
|
--success: #34d399;
|
||||||
|
--warning: #fbbf24;
|
||||||
|
--danger: #f87171;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
|
||||||
|
--shadow-md: 0 4px 12px rgb(0 0 0 / 0.45);
|
||||||
|
--shadow-lg: 0 12px 32px rgb(0 0 0 / 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variante claire — uniquement sur toggle explicite */
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
--bg: #faf9f7;
|
||||||
|
--bg-alt: #f1efec;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-2: #eceae6;
|
||||||
|
|
||||||
|
--text: #171717;
|
||||||
|
--text-muted: #4a4a4a;
|
||||||
|
--text-subtle: #6d6d6d;
|
||||||
|
|
||||||
|
--border: #e2e0dc;
|
||||||
|
--border-strong: #ada9a2;
|
||||||
|
|
||||||
|
--primary: #a93226;
|
||||||
|
--primary-hover: #922b20;
|
||||||
|
--on-primary: #ffffff;
|
||||||
|
|
||||||
|
--accent: #a93226;
|
||||||
|
--on-accent: #ffffff;
|
||||||
|
|
||||||
|
--link: #a93226;
|
||||||
|
--link-hover: #922b20;
|
||||||
|
|
||||||
|
--focus-ring: #a93226;
|
||||||
|
|
||||||
|
--success: #15803d;
|
||||||
|
--warning: #a16207;
|
||||||
|
--danger: #b91c1c;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);
|
||||||
|
--shadow-md: 0 4px 12px rgb(0 0 0 / 0.08);
|
||||||
|
--shadow-lg: 0 12px 32px rgb(0 0 0 / 0.12);
|
||||||
|
}
|
||||||
94
theme/base.css
Normal file
94
theme/base.css
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
theme/base.css — thème neutre de départ.
|
||||||
|
À CLONER pour créer le thème d'un nouveau site :
|
||||||
|
cp theme/base.css theme/mon-site.css
|
||||||
|
Un thème ne surcharge QUE la palette et le couple de polices.
|
||||||
|
Jamais les échelles, le spacing, les rayons, le motion, le layout.
|
||||||
|
Structure obligatoire : bloc clair + bloc [data-theme="dark"]
|
||||||
|
+ duplication dans @media (prefers-color-scheme: dark).
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Polices du site (charger les @font-face côté site, fallbacks ici) */
|
||||||
|
--font-heading: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||||
|
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||||
|
|
||||||
|
/* Palette claire — remplacer par la palette du site */
|
||||||
|
--bg: #ffffff;
|
||||||
|
--bg-alt: #f5f5f3;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-2: #eeeeeb;
|
||||||
|
|
||||||
|
--text: #1b1b1f;
|
||||||
|
--text-muted: #4b4b52;
|
||||||
|
--text-subtle: #6e6e76;
|
||||||
|
|
||||||
|
--border: #e0e0dd;
|
||||||
|
--border-strong: #a8a8a2;
|
||||||
|
|
||||||
|
--primary: #1b1b1f;
|
||||||
|
--primary-hover: #333338;
|
||||||
|
--on-primary: #ffffff;
|
||||||
|
|
||||||
|
--accent: #1b1b1f;
|
||||||
|
--on-accent: #ffffff;
|
||||||
|
|
||||||
|
--link: #1d4ed8;
|
||||||
|
--link-hover: #1e40af;
|
||||||
|
|
||||||
|
--focus-ring: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--bg: #121214;
|
||||||
|
--bg-alt: #18181b;
|
||||||
|
--surface: #1e1e21;
|
||||||
|
--surface-2: #26262a;
|
||||||
|
|
||||||
|
--text: #ededf0;
|
||||||
|
--text-muted: #b4b4bb;
|
||||||
|
--text-subtle: #8b8b93;
|
||||||
|
|
||||||
|
--border: #2f2f34;
|
||||||
|
--border-strong: #4d4d55;
|
||||||
|
|
||||||
|
--primary: #ededf0;
|
||||||
|
--primary-hover: #ffffff;
|
||||||
|
--on-primary: #121214;
|
||||||
|
|
||||||
|
--accent: #ededf0;
|
||||||
|
--on-accent: #121214;
|
||||||
|
|
||||||
|
--link: #8ab4f8;
|
||||||
|
--link-hover: #aecbfa;
|
||||||
|
|
||||||
|
--focus-ring: #8ab4f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
--bg: #121214;
|
||||||
|
--bg-alt: #18181b;
|
||||||
|
--surface: #1e1e21;
|
||||||
|
--surface-2: #26262a;
|
||||||
|
|
||||||
|
--text: #ededf0;
|
||||||
|
--text-muted: #b4b4bb;
|
||||||
|
--text-subtle: #8b8b93;
|
||||||
|
|
||||||
|
--border: #2f2f34;
|
||||||
|
--border-strong: #4d4d55;
|
||||||
|
|
||||||
|
--primary: #ededf0;
|
||||||
|
--primary-hover: #ffffff;
|
||||||
|
--on-primary: #121214;
|
||||||
|
|
||||||
|
--accent: #ededf0;
|
||||||
|
--on-accent: #121214;
|
||||||
|
|
||||||
|
--link: #8ab4f8;
|
||||||
|
--link-hover: #aecbfa;
|
||||||
|
|
||||||
|
--focus-ring: #8ab4f8;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
theme/renovation.css
Normal file
103
theme/renovation.css
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
theme/renovation.css — renovation-energetique.trans-former.fr
|
||||||
|
Palette Brunswick green + crème + jaune secondaire.
|
||||||
|
Polices : Poppins (titres) / Open Sans (corps long).
|
||||||
|
Les @font-face sont chargés côté site (self-host ou fontsource) ;
|
||||||
|
ce fichier ne définit que les stacks.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-heading: "Poppins", system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
--font-body: "Open Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
|
||||||
|
/* Clair */
|
||||||
|
--bg: #ffffff;
|
||||||
|
--bg-alt: #f7f2e9; /* crème clair, dérivé de #E8D5B0 */
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-2: #f1e8d8; /* crème #E8D5B0 adouci pour rester AA */
|
||||||
|
|
||||||
|
--text: #1e2a25;
|
||||||
|
--text-muted: #47564e;
|
||||||
|
--text-subtle: #63736a;
|
||||||
|
|
||||||
|
--border: #e4ddd0;
|
||||||
|
--border-strong: #b9ac93;
|
||||||
|
|
||||||
|
--primary: #1b4436; /* Brunswick green */
|
||||||
|
--primary-hover: #163829;
|
||||||
|
--on-primary: #ffffff;
|
||||||
|
|
||||||
|
--accent: #f9d400; /* jaune secondaire */
|
||||||
|
--on-accent: #1b4436;
|
||||||
|
|
||||||
|
--link: #1b4436;
|
||||||
|
--link-hover: #163829;
|
||||||
|
|
||||||
|
--focus-ring: #35735d;
|
||||||
|
|
||||||
|
--success: #2e7d32;
|
||||||
|
--warning: #a16207;
|
||||||
|
--danger: #b3261e;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--bg: #101714;
|
||||||
|
--bg-alt: #141d18;
|
||||||
|
--surface: #182420;
|
||||||
|
--surface-2: #1f2e28;
|
||||||
|
|
||||||
|
--text: #ede7da; /* crème clair */
|
||||||
|
--text-muted: #bcc6bd;
|
||||||
|
--text-subtle: #93a097;
|
||||||
|
|
||||||
|
--border: #27352e;
|
||||||
|
--border-strong: #43564c;
|
||||||
|
|
||||||
|
--primary: #7fc4a4;
|
||||||
|
--primary-hover: #98d4b8;
|
||||||
|
--on-primary: #10241c;
|
||||||
|
|
||||||
|
--accent: #f9d400;
|
||||||
|
--on-accent: #1f1a00;
|
||||||
|
|
||||||
|
--link: #8fcbae;
|
||||||
|
--link-hover: #aadcc3;
|
||||||
|
|
||||||
|
--focus-ring: #7fc4a4;
|
||||||
|
|
||||||
|
--success: #6fbf9a;
|
||||||
|
--warning: #e8c15a;
|
||||||
|
--danger: #f08a80;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
--bg: #101714;
|
||||||
|
--bg-alt: #141d18;
|
||||||
|
--surface: #182420;
|
||||||
|
--surface-2: #1f2e28;
|
||||||
|
|
||||||
|
--text: #ede7da;
|
||||||
|
--text-muted: #bcc6bd;
|
||||||
|
--text-subtle: #93a097;
|
||||||
|
|
||||||
|
--border: #27352e;
|
||||||
|
--border-strong: #43564c;
|
||||||
|
|
||||||
|
--primary: #7fc4a4;
|
||||||
|
--primary-hover: #98d4b8;
|
||||||
|
--on-primary: #10241c;
|
||||||
|
|
||||||
|
--accent: #f9d400;
|
||||||
|
--on-accent: #1f1a00;
|
||||||
|
|
||||||
|
--link: #8fcbae;
|
||||||
|
--link-hover: #aadcc3;
|
||||||
|
|
||||||
|
--focus-ring: #7fc4a4;
|
||||||
|
|
||||||
|
--success: #6fbf9a;
|
||||||
|
--warning: #e8c15a;
|
||||||
|
--danger: #f08a80;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
theme/tmip.css
Normal file
38
theme/tmip.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
theme/tmip.css — tmip.trans-former.fr
|
||||||
|
PLACEHOLDER — palette à confirmer, sera remplie au prompt P2 (migration
|
||||||
|
TMIP). Ne pas importer en l'état : sans surcharge, le site tourne sur le
|
||||||
|
thème neutre de tokens.css.
|
||||||
|
|
||||||
|
À remplir en clonant la structure de theme/base.css :
|
||||||
|
- stacks --font-heading / --font-body
|
||||||
|
- bloc clair (:root)
|
||||||
|
- bloc sombre (:root[data-theme="dark"])
|
||||||
|
- duplication sombre dans @media (prefers-color-scheme: dark)
|
||||||
|
avec le garde :root:not([data-theme="light"])
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
:root {
|
||||||
|
--font-heading: ... ;
|
||||||
|
--font-body: ... ;
|
||||||
|
|
||||||
|
--bg: ... ;
|
||||||
|
--bg-alt: ... ;
|
||||||
|
--surface: ... ;
|
||||||
|
--surface-2: ... ;
|
||||||
|
--text: ... ;
|
||||||
|
--text-muted: ... ;
|
||||||
|
--text-subtle: ... ;
|
||||||
|
--border: ... ;
|
||||||
|
--border-strong: ... ;
|
||||||
|
--primary: ... ;
|
||||||
|
--primary-hover: ... ;
|
||||||
|
--on-primary: ... ;
|
||||||
|
--accent: ... ;
|
||||||
|
--on-accent: ... ;
|
||||||
|
--link: ... ;
|
||||||
|
--link-hover: ... ;
|
||||||
|
--focus-ring: ... ;
|
||||||
|
}
|
||||||
|
*/
|
||||||
192
tokens.css
Normal file
192
tokens.css
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
@transformer/ui — tokens.css
|
||||||
|
Schéma canonique de tokens. Nommage par RÔLE, jamais par couleur brute.
|
||||||
|
Les valeurs ci-dessous sont le thème NEUTRE par défaut ; chaque site les
|
||||||
|
surcharge via un fichier theme/*.css (palette + polices UNIQUEMENT).
|
||||||
|
|
||||||
|
Échelle fluide calibrée entre 360px (22.5rem) et 1280px (80rem).
|
||||||
|
Breakpoints (constantes, voir PROTOCOLE.md) :
|
||||||
|
base 360 · sm 480 · md 768 · lg 1024 · xl 1280
|
||||||
|
|
||||||
|
Tout le fichier vit dans @layer tf-tokens : un theme/*.css (non-layeré)
|
||||||
|
gagne TOUJOURS sur ces valeurs, quel que soit l'ordre de chargement.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@layer tf-tokens, tf-reset;
|
||||||
|
|
||||||
|
@layer tf-tokens {
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
|
||||||
|
/* --- Couleur : rôles (mode clair par défaut) --------------------------- */
|
||||||
|
--bg: #ffffff;
|
||||||
|
--bg-alt: #f5f5f3;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-2: #eeeeeb;
|
||||||
|
|
||||||
|
--text: #1b1b1f;
|
||||||
|
--text-muted: #4b4b52;
|
||||||
|
--text-subtle: #6e6e76;
|
||||||
|
|
||||||
|
--border: #e0e0dd;
|
||||||
|
--border-strong: #a8a8a2;
|
||||||
|
|
||||||
|
--primary: #1b1b1f;
|
||||||
|
--primary-hover: #333338;
|
||||||
|
--on-primary: #ffffff;
|
||||||
|
|
||||||
|
--accent: #1b1b1f;
|
||||||
|
--on-accent: #ffffff;
|
||||||
|
|
||||||
|
--link: #1d4ed8;
|
||||||
|
--link-hover: #1e40af;
|
||||||
|
|
||||||
|
--focus-ring: #2563eb;
|
||||||
|
|
||||||
|
--success: #15803d;
|
||||||
|
--warning: #a16207;
|
||||||
|
--danger: #b91c1c;
|
||||||
|
|
||||||
|
/* --- Ombres (discrètes ; surchargées en sombre) ------------------------ */
|
||||||
|
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);
|
||||||
|
--shadow-md: 0 4px 12px rgb(0 0 0 / 0.08);
|
||||||
|
--shadow-lg: 0 12px 32px rgb(0 0 0 / 0.12);
|
||||||
|
|
||||||
|
/* --- Typographie ------------------------------------------------------- */
|
||||||
|
--font-heading: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||||
|
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||||
|
--font-mono: ui-monospace, "Cascadia Code", "JetBrains Mono", Menlo, monospace;
|
||||||
|
|
||||||
|
/* Échelle fluide 360→1280 (min → max) */
|
||||||
|
--fs-xs: clamp(0.75rem, 0.728rem + 0.098vw, 0.8125rem); /* 12 → 13 */
|
||||||
|
--fs-sm: clamp(0.875rem, 0.851rem + 0.109vw, 0.9375rem); /* 14 → 15 */
|
||||||
|
--fs-base: clamp(1rem, 0.951rem + 0.217vw, 1.125rem); /* 16 → 18 */
|
||||||
|
--fs-lg: clamp(1.125rem, 1.076rem + 0.217vw, 1.25rem); /* 18 → 20 */
|
||||||
|
--fs-xl: clamp(1.25rem, 1.152rem + 0.435vw, 1.5rem); /* 20 → 24 */
|
||||||
|
--fs-2xl: clamp(1.5rem, 1.304rem + 0.87vw, 2rem); /* 24 → 32 */
|
||||||
|
--fs-3xl: clamp(1.875rem, 1.533rem + 1.522vw, 2.75rem); /* 30 → 44 */
|
||||||
|
--fs-4xl: clamp(2.25rem, 1.761rem + 2.174vw, 3.5rem); /* 36 → 56 */
|
||||||
|
|
||||||
|
--lh-tight: 1.15;
|
||||||
|
--lh-base: 1.6;
|
||||||
|
--lh-loose: 1.75;
|
||||||
|
|
||||||
|
--fw-regular: 400;
|
||||||
|
--fw-medium: 500;
|
||||||
|
--fw-semibold: 600;
|
||||||
|
--fw-bold: 700;
|
||||||
|
|
||||||
|
/* --- Spacing (base 4/8px, fluide sur les grands paliers) --------------- */
|
||||||
|
--space-2xs: 0.25rem; /* 4 */
|
||||||
|
--space-xs: 0.5rem; /* 8 */
|
||||||
|
--space-sm: 0.75rem; /* 12 */
|
||||||
|
--space-md: 1rem; /* 16 */
|
||||||
|
--space-lg: 1.5rem; /* 24 */
|
||||||
|
--space-xl: 2rem; /* 32 */
|
||||||
|
--space-2xl: clamp(2.5rem, 1.913rem + 2.609vw, 4rem); /* 40 → 64 */
|
||||||
|
--space-3xl: clamp(4rem, 3.217rem + 3.478vw, 6rem); /* 64 → 96 */
|
||||||
|
|
||||||
|
/* --- Rayons ------------------------------------------------------------ */
|
||||||
|
--radius-sm: 0.25rem;
|
||||||
|
--radius-md: 0.5rem;
|
||||||
|
--radius-lg: 1rem;
|
||||||
|
--radius-full: 999px;
|
||||||
|
|
||||||
|
/* --- Layout ------------------------------------------------------------ */
|
||||||
|
--container-max: 75rem; /* ~1200px */
|
||||||
|
--container-pad: clamp(1rem, 0.609rem + 1.739vw, 2rem); /* 16 → 32 */
|
||||||
|
|
||||||
|
/* --- Z-index ------------------------------------------------------------ */
|
||||||
|
--z-nav: 100;
|
||||||
|
--z-overlay: 200;
|
||||||
|
--z-modal: 300;
|
||||||
|
--z-toast: 400;
|
||||||
|
|
||||||
|
/* --- Motion ------------------------------------------------------------ */
|
||||||
|
--dur-fast: 150ms;
|
||||||
|
--dur-base: 250ms;
|
||||||
|
--dur-slow: 400ms;
|
||||||
|
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||||
|
--ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Mode sombre — deux portes d'entrée, le toggle explicite gagne toujours :
|
||||||
|
1. préférence système (tant que data-theme n'est pas forcé à "light")
|
||||||
|
2. attribut data-theme="dark" posé par le toggle du site
|
||||||
|
Un theme/*.css surcharge ces mêmes blocs avec sa palette.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--bg: #121214;
|
||||||
|
--bg-alt: #18181b;
|
||||||
|
--surface: #1e1e21;
|
||||||
|
--surface-2: #26262a;
|
||||||
|
|
||||||
|
--text: #ededf0;
|
||||||
|
--text-muted: #b4b4bb;
|
||||||
|
--text-subtle: #8b8b93;
|
||||||
|
|
||||||
|
--border: #2f2f34;
|
||||||
|
--border-strong: #4d4d55;
|
||||||
|
|
||||||
|
--primary: #ededf0;
|
||||||
|
--primary-hover: #ffffff;
|
||||||
|
--on-primary: #121214;
|
||||||
|
|
||||||
|
--accent: #ededf0;
|
||||||
|
--on-accent: #121214;
|
||||||
|
|
||||||
|
--link: #8ab4f8;
|
||||||
|
--link-hover: #aecbfa;
|
||||||
|
|
||||||
|
--focus-ring: #8ab4f8;
|
||||||
|
|
||||||
|
--success: #34d399;
|
||||||
|
--warning: #fbbf24;
|
||||||
|
--danger: #f87171;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
|
||||||
|
--shadow-md: 0 4px 12px rgb(0 0 0 / 0.45);
|
||||||
|
--shadow-lg: 0 12px 32px rgb(0 0 0 / 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
--bg: #121214;
|
||||||
|
--bg-alt: #18181b;
|
||||||
|
--surface: #1e1e21;
|
||||||
|
--surface-2: #26262a;
|
||||||
|
|
||||||
|
--text: #ededf0;
|
||||||
|
--text-muted: #b4b4bb;
|
||||||
|
--text-subtle: #8b8b93;
|
||||||
|
|
||||||
|
--border: #2f2f34;
|
||||||
|
--border-strong: #4d4d55;
|
||||||
|
|
||||||
|
--primary: #ededf0;
|
||||||
|
--primary-hover: #ffffff;
|
||||||
|
--on-primary: #121214;
|
||||||
|
|
||||||
|
--accent: #ededf0;
|
||||||
|
--on-accent: #121214;
|
||||||
|
|
||||||
|
--link: #8ab4f8;
|
||||||
|
--link-hover: #aecbfa;
|
||||||
|
|
||||||
|
--focus-ring: #8ab4f8;
|
||||||
|
|
||||||
|
--success: #34d399;
|
||||||
|
--warning: #fbbf24;
|
||||||
|
--danger: #f87171;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
|
||||||
|
--shadow-md: 0 4px 12px rgb(0 0 0 / 0.45);
|
||||||
|
--shadow-lg: 0 12px 32px rgb(0 0 0 / 0.55);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} /* @layer tf-tokens */
|
||||||
5
tsconfig.json
Normal file
5
tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/base",
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user