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