docs: fondation projet (CLAUDE.md, design system, spec + plan jalon 1)

Ignore les dépôts de référence imbriqués (linux-update-dashboard, nas-ops).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 04:41:30 +02:00
parent 032287e2ab
commit 1e1be7f627
15 changed files with 6279 additions and 0 deletions
+304
View File
@@ -0,0 +1,304 @@
# mon design system — Gruvbox seventies
> Design system rétro-futuriste pour applications de monitoring, ops, IoT, domotique.
> Orange brûlé, fond brun délavé en sombre / gris clair usé en clair.
> **Version 1.0** · deux thèmes (dark + light), 14+ composants React, palette GTK pour GNOME.
---
## 🚀 Démarrage rapide (web)
```html
<!DOCTYPE html>
<html data-theme="dark">
<head>
<!-- 1. Polices Google -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<!-- 2. Icônes Font Awesome 6 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- 3. Tokens (variables CSS) -->
<link rel="stylesheet" href="tokens/tokens.css">
<!-- 4. React + Babel -->
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<!-- 5. Composants UI -->
<script type="text/babel" src="components/ui-kit.jsx"></script>
<script type="text/babel">
// Tes composants ici
</script>
</body>
</html>
```
Pour voir tout fonctionner, ouvre `examples/exemple-minimal.html`.
---
## 📂 Contenu du package
```
export/
├── README.md ← Ce fichier
├── consigne_design_system.md ← Brief pour agents IA (Claude, ChatGPT…)
├── tokens/
│ ├── tokens.css ← Variables CSS web (dark + light)
│ ├── tokens.gnome.css ← GTK 4 / libadwaita (apps GNOME)
│ └── tokens.json ← Format générique (Tailwind, Figma…)
├── components/
│ └── ui-kit.jsx ← 14 composants React (Button, IconButton, Toggle, Tooltip,
│ StatusLed, BatteryGauge, RadialGauge, BigRadialGauge,
│ Popup, TreeNav, Sparkline, LineChart, Icon, …)
└── examples/
└── exemple-minimal.html ← Démo minimale autoportante
```
---
## 🎨 Ce qui est paramétrable
### 1. Thème global
```html
<html data-theme="dark"> <!-- ou "light" -->
```
Tu peux mettre `data-theme` sur **n'importe quel parent** pour basculer un sous-arbre uniquement (utile pour une preview en mode opposé dans un menu de réglages).
### 2. Toutes les couleurs (CSS variables)
Édite `tokens.css` ou surcharge dans ton propre CSS :
```css
:root[data-theme="dark"] {
--accent: #fe8019; /* Couleur principale (orange seventies) */
--accent-soft: #d65d0e;
--bg-1: #2a231d; /* Fond app */
--bg-3: #3c332a; /* Cartes */
--ink-1: #f2e5c7; /* Texte */
--ok: #4dbb26;
--warn: #fabd2f;
--err: #fb4934;
--blue: #3db0d1; /* Datavis additionnel */
--purple: #c882c8;
}
```
**4 statuts** (ok / warn / err / info) + **2 couleurs datavis** (blue / purple) + **6 niveaux de fond** + **4 niveaux d'encre** + **3 niveaux de bordure**.
### 3. Polices
Trois familles, toutes substituables :
| Variable | Usage | Défaut |
|-----------------|-------------------------------------|---------------------|
| `--font-ui` | Interface (titres, corps, boutons) | Inter |
| `--font-mono` | Données, code, valeurs numériques | JetBrains Mono |
| `--font-terminal` | Logs, terminal embarqué, vibe rétro | Share Tech Mono |
Pour changer, remplace simplement les `@import` Google Fonts et redéfinis les variables.
### 4. Ombres et relief
```css
--tile-3d /* Relief 3D marqué pour cartes */
--shadow-1, -2, -3 /* Niveaux d'élévation */
--shadow-press /* Inset pour état pressé */
--hover-glow /* Halo accent au survol */
```
### 5. Composants — props paramétrables
Chaque composant accepte des props pour personnaliser sans toucher au CSS. Exemples :
```jsx
<Button variant="primary|ghost|danger|default" size="sm|md|lg" icon="play">Texte</Button>
<IconButton icon="cog" label="Tooltip obligatoire" primary danger active />
<Toggle on={state} onChange={setState} label="Auto" icon="refresh" />
<BatteryGauge
value={64} max={100} unit="%"
label="CPU"
warnAt={70} errAt={85} // seuils de couleur
compact // mode 1 ligne
icon="cpu"
color="var(--blue)" // couleur fixe (sinon auto selon seuils)
/>
<RadialGauge value={87} label="SCORE" size={120} />
<BigRadialGauge value={87} label="santé système" />
<Popup open={open} onClose={fn} title="…" footer={}>
Contenu
</Popup>
<TreeNav groups={[
{ id, icon: 'server', label, count, open, children: [
{ id, label, status: 'ok|warn|err', meta }
]}
]} activeId={id} onSelect={fn} />
```
Voir la doc complète des props : `Component Reference.html` dans le projet original.
---
## 🐧 Utilisation dans une app GNOME (GTK 4 / libadwaita)
Charge `tokens/tokens.gnome.css` comme provider CSS au démarrage de l'app.
**Python (PyGObject)** :
```python
from gi.repository import Gtk, Gdk
css_provider = Gtk.CssProvider()
css_provider.load_from_path("tokens.gnome.css")
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(),
css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
```
**GJS** :
```javascript
const provider = new Gtk.CssProvider();
provider.load_from_path('tokens.gnome.css');
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(),
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
);
```
**Rust (gtk4-rs)** :
```rust
let provider = gtk::CssProvider::new();
provider.load_from_path("tokens.gnome.css");
gtk::style_context_add_provider_for_display(
&gdk::Display::default().unwrap(),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
```
Le fichier override directement les couleurs sémantiques de libadwaita (`@window_bg_color`, `@accent_color`, etc.) ET ajoute des styles spécifiques pour les widgets courants : `button.suggested-action`, `entry`, `switch`, `scale`, `progressbar`, `notebook`, `popover`
Classes CSS supplémentaires à appliquer via `add_css_class()` :
- `.tile` / `.card` — Tuile en relief 3D
- `.mono` — Texte monospace JetBrains Mono
- `.terminal` — Texte terminal Share Tech Mono
- `.status.ok` / `.status.warn` / `.status.error` / `.status.info` — Badge de statut
---
## 🔧 Intégration dans d'autres outils
### Tailwind CSS
Convertis `tokens.json` en `tailwind.config.js` :
```js
const tokens = require('./tokens/tokens.json');
module.exports = {
theme: {
extend: {
colors: {
accent: tokens.themes.dark.accent.primary.value,
ok: tokens.themes.dark.status.ok.value,
// …
},
fontFamily: {
sans: [tokens.typography.fonts.ui.family, ...tokens.typography.fonts.ui.fallback],
mono: [tokens.typography.fonts.mono.family],
},
},
},
};
```
### Figma / outils de design
`tokens.json` suit un schéma compatible avec la plupart des plugins de tokens (Figma Tokens, Style Dictionary). Importe-le directement.
### Variables Sass / SCSS
```scss
@use 'sass:map';
$tokens: (
accent: #fe8019,
bg-1: #2a231d,
ok: #4dbb26,
);
// …
```
---
## ⚙️ Personnalisation avancée
### Créer un thème dérivé
Duplique `tokens.css`, change le nom du sélecteur (`[data-theme="ocean"]` par exemple) et modifie les variables. Charge les deux fichiers — `data-theme` choisira automatiquement.
### Ajouter une couleur status custom
```css
:root[data-theme="dark"] {
--critical: #ff0080;
--critical-glow: rgba(255, 0, 128, 0.45);
}
```
Utilisable ensuite partout : `<StatusLed status="critical">` nécessite une PR dans `ui-kit.jsx` (carte `map` dans `StatusLed`), mais en raw CSS tu peux utiliser la variable directement.
### Désactiver les effets
Tous les effets de `transition` / `transform` / `box-shadow` sont concentrés dans les classes `.interactive`, `.bg-hover`, `.gauge-hover`. Surcharge-les en CSS si besoin :
```css
.interactive { transition: none !important; }
```
---
## ✅ Checklist d'intégration
- [ ] Polices Google Fonts chargées (Inter, JetBrains Mono, Share Tech Mono)
- [ ] Font Awesome 6 chargé
- [ ] `tokens.css` (web) **ou** `tokens.gnome.css` (GTK) chargé
- [ ] Attribut `data-theme="dark"` (ou "light") sur `<html>` ou un parent
- [ ] React 18 + Babel chargés (uniquement pour `ui-kit.jsx`)
- [ ] `ui-kit.jsx` chargé en `type="text/babel"`
---
## 📋 Statuts du système
| Couleur | Token | Hex (dark) | Hex (light) | Usage |
|---------|--------|------------|-------------|-----------------------------|
| Accent | `--accent` | `#fe8019` | `#af3a03` | Primaire, focus, sélection |
| OK | `--ok` | `#4dbb26` | `#3c911c` | Succès, état nominal |
| Warn | `--warn` | `#fabd2f` | `#b57614` | Attention, latence élevée |
| Err | `--err` | `#fb4934` | `#9d0006` | Erreur, alerte critique |
| Info | `--info` | `#83a598` | `#427b58` | Information neutre |
| Blue | `--blue` | `#3db0d1` | `#2d82a3` | Datavis catégorie 2 |
| Purple | `--purple` | `#c882c8` | `#8c468c` | Datavis catégorie 3 |
---
## 🤖 Pour les agents IA
Si tu utilises ce design system avec une IA (Claude, GPT, Copilot, etc.), partage-lui le fichier **`consigne_design_system.md`**. Il y trouvera toutes les règles d'utilisation, conventions, contre-exemples à éviter.
---
**Licence** · Usage libre dans tes projets. Pas de garantie.
+656
View File
@@ -0,0 +1,656 @@
/* ============================================================
ui-kit.jsx
Composants haute-fid Gruvbox Seventies.
Tout est purement décoratif/interactif côté composant.
Effets : transparence (glass), hover glow, click 3D, tooltips.
============================================================ */
const { useState, useRef, useEffect } = React;
/* ============================================================
Icônes — Font Awesome 6 Free.
Mapping nom logique → classe FA. Le CSS de FA est chargé en CDN
dans le <head>. Le composant garde la MÊME API qu'avant (name,
size, style) pour ne rien casser ailleurs.
============================================================ */
const ICON_MAP = {
cpu: 'microchip',
memory: 'memory',
disk: 'hard-drive',
network: 'network-wired',
clock: 'clock',
grid: 'table-cells',
list: 'list',
cog: 'gear',
alert: 'triangle-exclamation',
bell: 'bell',
server: 'server',
chart: 'chart-line',
bars: 'chart-simple',
terminal: 'terminal',
refresh: 'arrows-rotate',
play: 'play',
pause: 'pause',
power: 'power-off',
sun: 'sun',
moon: 'moon',
search: 'magnifying-glass',
close: 'xmark',
chevR: 'chevron-right',
chevL: 'chevron-left',
chevD: 'chevron-down',
chevU: 'chevron-up',
plus: 'plus',
filter: 'filter',
download: 'download',
folder: 'folder',
node: 'circle-nodes',
user: 'user',
};
const Icon = ({ name, size = 16, style }) => {
const fa = ICON_MAP[name] || 'circle-question';
return (
<i className={`fa-solid fa-${fa}`} aria-hidden="true" style={{
fontSize: size,
width: size,
height: size,
lineHeight: `${size}px`,
textAlign: 'center',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flex: '0 0 auto',
color: 'currentColor',
...style,
}} />
);
};
/* ============================================================
Tooltip — apparaît au hover après 300ms, position auto.
============================================================ */
function Tooltip({ children, label, side = 'top' }) {
const [show, setShow] = useState(false);
const t = useRef();
const onEnter = () => { t.current = setTimeout(() => setShow(true), 280); };
const onLeave = () => { clearTimeout(t.current); setShow(false); };
const sides = {
top: { bottom: 'calc(100% + 8px)', left: '50%', transform: 'translateX(-50%)' },
bottom: { top: 'calc(100% + 8px)', left: '50%', transform: 'translateX(-50%)' },
left: { right: 'calc(100% + 8px)', top: '50%', transform: 'translateY(-50%)' },
right: { left: 'calc(100% + 8px)', top: '50%', transform: 'translateY(-50%)' },
};
return (
<span style={{ position: 'relative', display: 'inline-flex' }}
onMouseEnter={onEnter} onMouseLeave={onLeave}>
{children}
{show && (
<span className="glass-strong" style={{
position: 'absolute', ...sides[side],
padding: '6px 10px',
borderRadius: 6,
fontSize: 12, lineHeight: 1.3,
color: 'var(--ink-1)',
whiteSpace: 'nowrap',
boxShadow: 'var(--shadow-2)',
zIndex: 1000,
pointerEvents: 'none',
fontFamily: 'JetBrains Mono, monospace',
letterSpacing: '0.02em',
}}>{label}</span>
)}
</span>
);
}
/* ============================================================
IconButton — bouton icône seul + tooltip obligatoire.
============================================================ */
function IconButton({ icon, label, onClick, active, danger, size = 34, primary }) {
const bg = active ? 'var(--accent-tint)'
: primary ? 'var(--accent)'
: 'var(--bg-3)';
const fg = active ? 'var(--accent)'
: primary ? 'var(--bg-1)'
: danger ? 'var(--err)'
: 'var(--ink-2)';
const bd = active ? 'var(--accent-soft)' : 'var(--border-2)';
return (
<Tooltip label={label}>
<button onClick={onClick} className="interactive" style={{
width: size, height: size,
background: bg,
color: fg,
border: `1px solid ${bd}`,
borderRadius: 8,
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: 0, cursor: 'pointer',
boxShadow: primary ? '0 2px 6px var(--accent-glow)' : 'var(--shadow-1)',
}}>
<Icon name={icon} size={Math.round(size * 0.5)} />
</button>
</Tooltip>
);
}
/* ============================================================
Toggle on/off — switch tactile avec glow accent quand ON
============================================================ */
function Toggle({ on, onChange, label, icon }) {
return (
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
{icon && <Icon name={icon} size={14} style={{ color: on ? 'var(--accent)' : 'var(--ink-3)' }} />}
{label && <span className="label" style={{ color: on ? 'var(--ink-1)' : 'var(--ink-3)' }}>{label}</span>}
<button onClick={() => onChange(!on)} className="interactive" style={{
width: 42, height: 22, borderRadius: 12,
background: on ? 'var(--accent)' : 'var(--bg-4)',
border: `1px solid ${on ? 'var(--accent-soft)' : 'var(--border-2)'}`,
boxShadow: on ? `0 0 10px var(--accent-glow), var(--shadow-1)` : 'var(--shadow-press)',
position: 'relative', cursor: 'pointer', padding: 0,
}}>
<span style={{
position: 'absolute', top: 1, left: on ? 21 : 1,
width: 18, height: 18, borderRadius: '50%',
background: on ? 'var(--bg-1)' : 'var(--ink-2)',
transition: 'left .18s cubic-bezier(.5,.2,.3,1.3), background .15s',
boxShadow: 'var(--shadow-1)',
}} />
</button>
</div>
);
}
/* ============================================================
Status LED — pastille pulsante (effet halo si critique)
============================================================ */
function StatusLed({ status = 'ok', size = 10, pulse }) {
const map = {
ok: { c: 'var(--ok)', g: 'var(--ok-glow)' },
warn: { c: 'var(--warn)', g: 'var(--warn-glow)' },
err: { c: 'var(--err)', g: 'var(--err-glow)' },
off: { c: 'var(--ink-4)', g: 'transparent' },
info: { c: 'var(--info)', g: 'var(--info-glow)' },
};
const { c, g } = map[status];
const id = `pulse-${status}-${size}`;
return (
<>
{pulse && (
<style>{`@keyframes ${id} { 0%{box-shadow:0 0 0 0 ${g}} 70%{box-shadow:0 0 0 6px transparent} 100%{box-shadow:0 0 0 0 transparent} }`}</style>
)}
<span style={{
display: 'inline-block',
width: size, height: size,
borderRadius: '50%',
background: c,
boxShadow: status === 'off' ? 'none' : `0 0 6px ${g}, inset 0 0 2px rgba(0,0,0,0.3)`,
animation: pulse ? `${id} 1.8s ease-out infinite` : 'none',
flex: '0 0 auto',
}} />
</>
);
}
/* ============================================================
BatteryGauge — jauge horizontale style batterie
- Pas de bandes (couleur unie + léger gloss interne)
- Pas de graduations verticales
- Hover : glow lumineux dans la couleur de la jauge
- Mode compact : label [bar] valeur sur une seule ligne
============================================================ */
function BatteryGauge({ value = 60, label, max = 100, unit = '%', warnAt = 70, errAt = 90, height = 22, compact = false, color: colorOverride, icon }) {
const pct = Math.max(0, Math.min(100, (value / max) * 100));
const color = colorOverride
|| (pct >= errAt ? 'var(--err)' : pct >= warnAt ? 'var(--warn)' : 'var(--ok)');
const glowVar = pct >= errAt ? 'var(--err-glow)'
: pct >= warnAt ? 'var(--warn-glow)'
: 'var(--ok-glow)';
// Variante compacte : label [bar] valeur sur une seule ligne
if (compact) {
return (
<div className="bg-hover" style={{
display: 'flex', alignItems: 'center', gap: 10, minWidth: 0,
'--bg-glow': glowVar,
}}>
{(icon || label) && (
<span style={{
flex: '0 0 auto', display: 'inline-flex', alignItems: 'center', gap: 6,
minWidth: 90,
}}>
{icon && <Icon name={icon} size={12} style={{ color: 'var(--ink-3)' }} />}
{label && <span className="label" style={{ fontSize: 11 }}>{label}</span>}
</span>
)}
<div className="bg-bar" style={{
flex: 1, height: 12, borderRadius: 3,
background: 'var(--bg-1)',
border: '1px solid var(--border-2)',
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.4)',
overflow: 'hidden', position: 'relative',
transition: 'border-color .2s',
}}>
<div className="bg-fill" style={{
position: 'absolute', top: 1, left: 1, bottom: 1,
width: `calc((100% - 2px) * ${pct / 100})`,
background: color,
borderRadius: 2,
transition: 'width .4s cubic-bezier(.3,.6,.3,1), box-shadow .2s',
}} />
</div>
<span className="mono" style={{
flex: '0 0 auto', fontSize: 13,
color: 'var(--ink-1)', minWidth: 52, textAlign: 'right',
}}>
{value}<span style={{ color: 'var(--ink-3)', marginLeft: 2 }}>{unit}</span>
</span>
</div>
);
}
return (
<div className="bg-hover" style={{
display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0,
'--bg-glow': glowVar,
}}>
{label && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<span className="label">{label}</span>
<span className="mono" style={{ fontSize: 13, color: 'var(--ink-1)' }}>
{value}<span style={{ color: 'var(--ink-3)', marginLeft: 2 }}>{unit}</span>
</span>
</div>
)}
<div className="bg-bar" style={{
position: 'relative',
height, borderRadius: 4,
background: 'var(--bg-1)',
border: '1px solid var(--border-2)',
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.4)',
overflow: 'hidden',
transition: 'border-color .2s',
}}>
<div className="bg-fill" style={{
position: 'absolute', top: 1, left: 1, bottom: 1,
width: `calc((100% - 2px) * ${pct / 100})`,
background: color,
borderRadius: 3,
transition: 'width .4s cubic-bezier(.3,.6,.3,1), box-shadow .2s',
}} />
{/* Gloss interne très léger (un seul highlight haut, pas de bande inférieure) */}
<div style={{
position: 'absolute', top: 1, left: 1, right: 1, height: '40%',
background: 'linear-gradient(180deg, rgba(255,255,255,0.18), transparent)',
borderRadius: '3px 3px 0 0',
pointerEvents: 'none',
}} />
</div>
</div>
);
}
/* ============================================================
RadialGauge — jauge ronde, version épurée
============================================================ */
function RadialGauge({ value = 64, label, size = 120, warnAt = 70, errAt = 90 }) {
const pct = Math.max(0, Math.min(100, value));
const color = pct >= errAt ? 'var(--err)' : pct >= warnAt ? 'var(--warn)' : 'var(--ok)';
const glow = pct >= errAt ? 'var(--err-glow)' : pct >= warnAt ? 'var(--warn-glow)' : 'var(--ok-glow)';
const r = size / 2 - 10;
const cx = size / 2;
const cy = size / 2 + 6;
const circ = Math.PI * r;
const offset = circ - (pct / 100) * circ;
return (
<div className="gauge-hover" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
<svg width={size} height={size * 0.72} viewBox={`0 0 ${size} ${size * 0.8}`}>
<defs>
<filter id={`glow-${label}`} x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2.5" />
</filter>
</defs>
{/* arc background */}
<path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
fill="none" stroke="var(--bg-4)" strokeWidth="6" strokeLinecap="round" />
{/* arc value glow */}
<path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
fill="none" stroke={color} strokeWidth="8" strokeLinecap="round"
strokeDasharray={circ} strokeDashoffset={offset}
filter={`url(#glow-${label})`} opacity="0.7" />
{/* arc value crisp */}
<path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
fill="none" stroke={color} strokeWidth="5" strokeLinecap="round"
strokeDasharray={circ} strokeDashoffset={offset}
style={{ transition: 'stroke-dashoffset .5s cubic-bezier(.3,.6,.3,1)' }} />
</svg>
<div style={{ marginTop: -10, textAlign: 'center' }}>
<div className="mono" style={{ fontSize: size * 0.22, fontWeight: 600, color: 'var(--ink-1)', lineHeight: 1 }}>
{value}<span style={{ fontSize: '0.55em', color: 'var(--ink-3)', marginLeft: 2 }}>%</span>
</div>
{label && <div className="label" style={{ marginTop: 2 }}>{label}</div>}
</div>
</div>
);
}
/* ============================================================
BigRadialGauge — la grande jauge cockpit "santé système"
============================================================ */
function BigRadialGauge({ value = 87, label = 'score santé · stable' }) {
const size = 320;
const r = 130;
const cx = size / 2;
const cy = size / 2 + 30;
const circ = Math.PI * r;
const offset = circ - (value / 100) * circ;
const color = value >= 80 ? 'var(--ok)' : value >= 50 ? 'var(--warn)' : 'var(--err)';
return (
<div className="gauge-hover" style={{ position: 'relative', width: size, height: size * 0.78 }}>
<svg width={size} height={size * 0.85}>
<defs>
<filter id="biggauge-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" />
</filter>
<linearGradient id="biggauge-grad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stopColor={color} stopOpacity="0.7"/>
<stop offset="1" stopColor={color}/>
</linearGradient>
</defs>
{/* tics */}
{Array.from({ length: 21 }).map((_, i) => {
const a = Math.PI - (i / 20) * Math.PI;
const major = i % 5 === 0;
const inner = major ? r + 8 : r + 11;
const outer = major ? r + 20 : r + 15;
return <line key={i}
x1={cx + Math.cos(a) * inner} y1={cy - Math.sin(a) * inner}
x2={cx + Math.cos(a) * outer} y2={cy - Math.sin(a) * outer}
stroke={major ? 'var(--ink-3)' : 'var(--ink-4)'} strokeWidth={major ? 1.5 : 0.8}
/>;
})}
{[0, 50, 100].map(v => {
const a = Math.PI - (v / 100) * Math.PI;
const x = cx + Math.cos(a) * (r + 32);
const y = cy - Math.sin(a) * (r + 32) + 4;
return <text key={v} x={x} y={y} textAnchor="middle"
fontFamily="JetBrains Mono" fontSize="11"
fill="var(--ink-3)">{v}</text>;
})}
{/* arc bg */}
<path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
fill="none" stroke="var(--bg-4)" strokeWidth="10" strokeLinecap="round" />
{/* arc value glow */}
<path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
fill="none" stroke={color} strokeWidth="14" strokeLinecap="round"
strokeDasharray={circ} strokeDashoffset={offset}
filter="url(#biggauge-glow)" opacity="0.55" />
{/* arc value */}
<path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
fill="none" stroke="url(#biggauge-grad)" strokeWidth="9" strokeLinecap="round"
strokeDasharray={circ} strokeDashoffset={offset}
style={{ transition: 'stroke-dashoffset .8s cubic-bezier(.3,.6,.3,1)' }} />
{/* needle */}
<line x1={cx} y1={cy}
x2={cx + Math.cos(Math.PI - (value / 100) * Math.PI) * (r - 14)}
y2={cy - Math.sin(Math.PI - (value / 100) * Math.PI) * (r - 14)}
stroke="var(--accent)" strokeWidth="3" strokeLinecap="round"
style={{ filter: 'drop-shadow(0 0 4px var(--accent-glow))' }} />
<circle cx={cx} cy={cy} r="9" fill="var(--bg-3)" stroke="var(--border-3)" strokeWidth="1.5"/>
<circle cx={cx} cy={cy} r="3" fill="var(--accent)" />
</svg>
<div style={{ position: 'absolute', bottom: 12, left: 0, right: 0, textAlign: 'center' }}>
<div className="mono" style={{
fontSize: 64, fontWeight: 700, lineHeight: 1,
color: 'var(--ink-1)',
textShadow: `0 0 20px ${color}33`,
}}>{value}</div>
<div className="label" style={{ marginTop: 6 }}>{label}</div>
</div>
</div>
);
}
/* ============================================================
Popup — modale glassmorphism centrée + bouton fermer
============================================================ */
function Popup({ open, onClose, title, children, footer, width = 460 }) {
if (!open) return null;
return (
<div style={{
position: 'absolute', inset: 0, zIndex: 100,
background: 'rgba(0,0,0,0.45)',
backdropFilter: 'blur(4px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
animation: 'fadein .2s ease-out',
}} onClick={onClose}>
<style>{`
@keyframes fadein { from { opacity: 0 } to { opacity: 1 } }
@keyframes popin { from { opacity: 0; transform: translateY(8px) scale(.98) } to { opacity: 1; transform: translateY(0) scale(1) } }
`}</style>
<div className="glass-strong" onClick={e => e.stopPropagation()} style={{
width, maxWidth: '90%',
borderRadius: 12,
boxShadow: 'var(--shadow-3)',
animation: 'popin .25s cubic-bezier(.3,.7,.3,1.2)',
overflow: 'hidden',
}}>
<div style={{
padding: '14px 16px',
borderBottom: '1px solid var(--border-1)',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
background: 'var(--bg-3)',
}}>
<div style={{ fontWeight: 600, fontSize: 15, color: 'var(--ink-1)' }}>{title}</div>
<IconButton icon="close" label="Fermer" onClick={onClose} size={28} />
</div>
<div style={{ padding: 18 }}>{children}</div>
{footer && (
<div style={{
padding: '12px 16px',
borderTop: '1px solid var(--border-1)',
background: 'var(--bg-2)',
display: 'flex', justifyContent: 'flex-end', gap: 8,
}}>{footer}</div>
)}
</div>
</div>
);
}
/* ============================================================
Button — bouton classique avec variantes
============================================================ */
function Button({ children, icon, onClick, variant = 'default', size = 'md' }) {
const sizes = {
sm: { padding: '5px 10px', fontSize: 12, h: 28 },
md: { padding: '7px 14px', fontSize: 13, h: 34 },
lg: { padding: '10px 18px', fontSize: 14, h: 40 },
}[size];
const variants = {
default: { bg: 'var(--bg-3)', fg: 'var(--ink-1)', bd: 'var(--border-2)' },
primary: { bg: 'var(--accent)', fg: 'var(--bg-1)', bd: 'var(--accent-soft)' },
ghost: { bg: 'transparent', fg: 'var(--ink-2)', bd: 'var(--border-2)' },
danger: { bg: 'var(--bg-3)', fg: 'var(--err)', bd: 'var(--err)' },
}[variant];
return (
<button onClick={onClick} className="interactive" style={{
height: sizes.h,
padding: sizes.padding,
background: variants.bg,
color: variants.fg,
border: `1px solid ${variants.bd}`,
borderRadius: 8,
display: 'inline-flex', alignItems: 'center', gap: 8,
fontFamily: 'inherit', fontSize: sizes.fontSize, fontWeight: 500,
cursor: 'pointer',
boxShadow: variant === 'primary' ? '0 2px 8px var(--accent-glow)' : 'var(--shadow-1)',
}}>
{icon && <Icon name={icon} size={14} />}
{children}
</button>
);
}
/* ============================================================
TreeNav — arbre dépliable avec icône en tête (style B)
============================================================ */
function TreeNav({ groups, activeId, onSelect }) {
const [open, setOpen] = useState(() =>
Object.fromEntries(groups.map(g => [g.id, g.open !== false]))
);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{groups.map(g => (
<div key={g.id}>
<div className="interactive" onClick={() => setOpen({ ...open, [g.id]: !open[g.id] })}
style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '7px 8px', borderRadius: 6,
color: 'var(--ink-2)',
background: 'transparent',
border: '1px solid transparent',
cursor: 'pointer',
}}>
<Icon name="chevR" size={12} style={{
transform: open[g.id] ? 'rotate(90deg)' : 'rotate(0)',
transition: 'transform .15s',
color: 'var(--ink-3)',
}} />
<Icon name={g.icon || 'folder'} size={15} style={{ color: 'var(--accent)' }} />
<span style={{ flex: 1, fontSize: 13, fontWeight: 500 }}>{g.label}</span>
{g.count != null && (
<span className="mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>
{g.count}
</span>
)}
</div>
{open[g.id] && (
<div style={{ marginLeft: 18, marginTop: 2, display: 'flex', flexDirection: 'column', gap: 1, paddingLeft: 8, borderLeft: '1px dashed var(--border-1)' }}>
{g.children.map(c => {
const active = c.id === activeId;
return (
<div key={c.id} className="interactive" onClick={() => onSelect && onSelect(c.id)}
style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '6px 10px', borderRadius: 6,
background: active ? 'var(--accent-tint)' : 'transparent',
color: active ? 'var(--ink-1)' : 'var(--ink-2)',
borderLeft: active ? '2px solid var(--accent)' : '2px solid transparent',
marginLeft: active ? 0 : 2,
fontSize: 12.5,
}}>
<StatusLed status={c.status} size={8} pulse={c.status === 'err'} />
<span className="mono" style={{ fontSize: 11.5, flex: 1 }}>{c.label}</span>
{c.meta && <span className="mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>{c.meta}</span>}
</div>
);
})}
</div>
)}
</div>
))}
</div>
);
}
/* ============================================================
Sparkline pour les KPI
============================================================ */
function Sparkline({ points = [], color = 'var(--accent)', h = 28 }) {
const w = 100;
const max = Math.max(...points);
const min = Math.min(...points);
const range = max - min || 1;
const step = w / (points.length - 1);
const path = points.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${(i * step).toFixed(1)} ${(h - 2 - ((p - min) / range) * (h - 4)).toFixed(1)}`
).join(' ');
const area = path + ` L ${w} ${h} L 0 ${h} Z`;
return (
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ width: '100%', height: h }}>
<path d={area} fill={color} opacity="0.12" />
<path d={path} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" />
</svg>
);
}
/* ============================================================
LineChart — grand graph multi-séries
============================================================ */
function LineChart({ series, h = 200, labels }) {
const w = 600;
const padding = { l: 36, r: 12, t: 12, b: 24 };
const innerW = w - padding.l - padding.r;
const innerH = h - padding.t - padding.b;
const all = series.flatMap(s => s.points);
const max = Math.max(...all) * 1.1;
const min = 0;
const range = max - min;
const ptsCount = series[0].points.length;
const step = innerW / (ptsCount - 1);
return (
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ width: '100%', height: h }}>
{/* grid horizontal */}
{[0, 0.25, 0.5, 0.75, 1].map(p => {
const y = padding.t + innerH * p;
const v = Math.round(max - range * p);
return (
<g key={p}>
<line x1={padding.l} x2={w - padding.r} y1={y} y2={y}
stroke="var(--border-1)" strokeWidth="1" strokeDasharray="3 5" />
<text x={padding.l - 6} y={y + 3} textAnchor="end"
fontFamily="JetBrains Mono" fontSize="9" fill="var(--ink-3)">{v}</text>
</g>
);
})}
{/* labels x */}
{labels && labels.map((lb, i) => (
i % Math.ceil(labels.length / 8) === 0 && (
<text key={i} x={padding.l + i * step} y={h - 6} textAnchor="middle"
fontFamily="JetBrains Mono" fontSize="9" fill="var(--ink-3)">{lb}</text>
)
))}
{/* séries */}
{series.map((s, si) => {
const path = s.points.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${(padding.l + i * step).toFixed(1)} ${(padding.t + innerH - ((p - min) / range) * innerH).toFixed(1)}`
).join(' ');
const area = path + ` L ${padding.l + (ptsCount - 1) * step} ${padding.t + innerH} L ${padding.l} ${padding.t + innerH} Z`;
return (
<g key={si}>
<path d={area} fill={s.color} opacity="0.12" />
<path d={path} fill="none" stroke={s.color} strokeWidth="1.8"
strokeLinejoin="round" strokeLinecap="round" />
</g>
);
})}
</svg>
);
}
/* Expose */
Object.assign(window, {
Icon, Tooltip, IconButton, Toggle, StatusLed,
BatteryGauge, RadialGauge, BigRadialGauge,
Popup, Button, TreeNav, Sparkline, LineChart,
});
/* Effets hover sur les jauges (sans effet au clic) */
(function injectGaugeHoverStyles() {
if (document.getElementById('gauge-hover-styles')) return;
const s = document.createElement('style');
s.id = 'gauge-hover-styles';
s.textContent = `
.bg-hover:hover .bg-bar {
border-color: color-mix(in oklch, var(--accent) 60%, var(--border-3));
}
.bg-hover:hover .bg-fill {
box-shadow: 0 0 14px var(--bg-glow, var(--accent-glow));
filter: brightness(1.15);
}
.gauge-hover { transition: filter .2s; }
.gauge-hover:hover { filter: drop-shadow(0 0 8px var(--accent-glow)) brightness(1.08); }
`;
document.head.appendChild(s);
})();
+363
View File
@@ -0,0 +1,363 @@
# Consignes — mon design system (Gruvbox seventies)
> **Tu es un agent IA chargé de produire ou modifier du code utilisant ce design system.**
> Lis ce fichier en entier avant d'écrire la moindre ligne. Suis les règles à la lettre.
---
## 🎯 Identité du système
- **Nom** : mon design system — Gruvbox seventies
- **Vibe** : rétro-industriel, console de monitoring, SCADA, terminal années 70
- **Palette** : orange brûlé Gruvbox + fond brun délavé (pas noir intense) ou gris clair usé (pas blanc pur)
- **Cas d'usage cibles** : tableaux de bord, monitoring, IoT, domotique, ops, scanners réseau
- **Public** : utilisateurs techniques (admin sys, devs, makers) — densité d'info élevée acceptée
---
## 📁 Fichiers à connaître
| Fichier | Contient |
|---------------------------------|-------------------------------------------------------|
| `tokens/tokens.css` | Variables CSS web (`:root[data-theme="dark|light"]`) |
| `tokens/tokens.gnome.css` | Tokens GTK 4 / libadwaita (`@define-color`) |
| `tokens/tokens.json` | Tokens en JSON pour outils externes |
| `components/ui-kit.jsx` | 14 composants React (Button, Icon, Popup…) |
| `examples/exemple-minimal.html` | Démo de référence |
---
## ⚠️ Règles absolues — ne JAMAIS enfreindre
1. **Toujours utiliser les variables CSS**, jamais des hex en dur dans le code utilisateur.
`color: var(--accent)`
`color: #fe8019`
2. **Toujours déclarer `data-theme`** sur un parent (`<html>` ou un wrapper).
Sans ça, les variables ne sont pas définies et l'UI casse silencieusement.
3. **Composants existants** — ne jamais en réinventer. Vérifier d'abord la liste ci-dessous.
4. **Icônes** — utiliser le composant `<Icon name="…">` avec les noms mappés. JAMAIS d'emoji, JAMAIS de SVG inline custom pour un cas où une icône Font Awesome existe.
5. **Pas d'effet hover** sur les boutons / tuiles / composants généraux (sauf jauges et tuiles Heimdall qui en ont un). Seulement **pression 3D au clic** via `.interactive`.
6. **Toujours des tooltips** sur les boutons icônes seuls (`<IconButton>` exige `label`).
7. **Pas de bordure arrondie excessive**. Tuiles : `border-radius: 10-12px`. Boutons : `8px`. Pastilles : `999px`.
8. **Polices** — respecter strictement les 3 familles :
- **Inter** → UI (titres, corps, boutons, labels d'interface généraux)
- **JetBrains Mono** → données numériques, valeurs, code, IDs, IPs
- **Share Tech Mono** → logs, terminal embarqué, ambiance rétro
Toute autre police = bug.
9. **Tonalité** : labels en `text-transform: uppercase` + `letter-spacing: 0.08em` (classe `.label` déjà fournie).
10. **Densité** : pas de padding inutile. Ce DS est dense par nature. Tuiles : padding 14-18px. Boutons : 6-10px vertical.
---
## 🎨 Tokens disponibles
### Couleurs (toutes définies en `dark` ET `light`)
#### Fonds (du plus profond au plus haut)
```
--bg-0 très rare, niveau le plus bas
--bg-1 fond application principal
--bg-2 panneaux (sidebar, headerbar)
--bg-3 cartes, tuiles ← LE PLUS UTILISÉ
--bg-4 hover, état actif
--bg-5 press, sélection forte
```
#### Texte (du plus contrasté au moins)
```
--ink-1 texte principal
--ink-2 texte secondaire
--ink-3 labels, hints
--ink-4 désactivé
```
#### Accent
```
--accent couleur primaire (orange Gruvbox seventies)
--accent-soft variante foncée (bordures, hover)
--accent-glow halo (rgba)
--accent-tint teinte transparente (fonds discrets)
```
#### Statuts
```
--ok #4dbb26 (vert flashy)
--warn #fabd2f (jaune)
--err #fb4934 (rouge)
--info #83a598 (vert-bleu pastel)
```
#### Datavis additionnel
```
--blue #3db0d1
--purple #c882c8
```
#### Bordures
```
--border-1, --border-2, --border-3 du plus subtil au plus marqué
```
#### Ombres / relief
```
--shadow-1, -2, -3 élévations standards
--shadow-press état pressé (inset)
--tile-3d relief 3D marqué pour cartes ← À utiliser sur les tuiles importantes
```
### Polices
```
--font-ui 'Inter', system-ui, sans-serif
--font-mono 'JetBrains Mono', monospace
--font-terminal 'Share Tech Mono', monospace
```
---
## 🧩 Composants — quand utiliser quoi
| Besoin | Composant | Exemple |
|--------|-----------|---------|
| Bouton texte avec ou sans icône | `<Button variant="primary|ghost|danger|default">` | Action principale, secondaire |
| Bouton icône seul | `<IconButton icon="…" label="…">` | Toolbars, headers (le `label` devient tooltip) |
| On/off | `<Toggle on={…} onChange={…} label icon>` | Activer/désactiver une option |
| État système | `<StatusLed status="ok|warn|err|info|off" pulse>` | LED pulsante pour critique |
| Jauge ronde standard | `<RadialGauge value={…} label size>` | KPI compact, cockpit |
| Jauge ronde héro | `<BigRadialGauge value={…} label>` | Métrique principale unique |
| Jauge barre standard | `<BatteryGauge value label>` | Stack vertical de ressources |
| Jauge barre **inline** | `<BatteryGauge compact value label icon>` | Listes denses, label + barre + valeur sur 1 ligne |
| Modale | `<Popup open onClose title footer>` | Confirmation, config détaillée |
| Tree dépliable | `<TreeNav groups activeId onSelect>` | Sidebar hiérarchique (clusters/nodes) |
| Mini graphe | `<Sparkline points color>` | Dans une tuile KPI |
| Graphe ligne | `<LineChart series labels h>` | Évolution temporelle multi-séries |
| Tooltip | `<Tooltip label side><…/></Tooltip>` | Toute icône isolée |
| Icône | `<Icon name="…" size>` | JAMAIS d'emoji, JAMAIS de SVG custom |
### Icônes disponibles (noms logiques → Font Awesome)
`cpu`, `memory`, `disk`, `network`, `clock`, `grid`, `list`, `cog`, `alert`, `bell`, `server`, `chart`, `bars`, `terminal`, `refresh`, `play`, `pause`, `power`, `sun`, `moon`, `search`, `close`, `chevR`, `chevL`, `chevD`, `chevU`, `plus`, `filter`, `download`, `folder`, `node`, `user`.
Pour un nouveau besoin → utiliser une icône Font Awesome (préfixe `fa-solid fa-…`) en ajoutant l'alias dans `ICON_MAP` au sein de `ui-kit.jsx`.
---
## 🏗️ Patterns d'agencement standards
### Layout dashboard 3 colonnes
```
┌─ Header (tabs workspace + search + actions + statut connexion) ─┐
├──────┬────────────────────────────────┬──────────────────────────┤
│ Tree │ Center cockpit (KPIs + jauges) │ Logs/Terminal repliable │
│ nav │ │ │
├──────┴────────────────────────────────┴──────────────────────────┤
│ Status bar (mode · workspace · stats · horloge) │
└──────────────────────────────────────────────────────────────────┘
```
### Tuile KPI standard
```jsx
<div className="glass" style={{ padding: 12, borderRadius: 10, ...}}>
<Icon name="cpu" /> <span className="label">CPU</span>
<span className="mono">{value}<span className="label">%</span></span>
<Sparkline points={trend} color="var(--accent)" />
</div>
```
### Status bar inférieure
- Première cellule = mode courant en fond accent (style tmux)
- Cellules séparées par `border-right: 1px solid var(--border-1)`
- Police `Share Tech Mono` 11-12px
- Horloge à droite
---
## 🚫 Anti-patterns à éviter
### NE PAS faire :
**Mettre des emoji** pour un état :
```jsx
<span> Système OK</span> // NON
<><StatusLed status="ok" /> Système OK</> // OUI
```
**Inventer de nouvelles couleurs hors palette** :
```jsx
style={{ color: '#ff00aa' }} // NON — utilise les tokens
```
**Police arbitraire** :
```jsx
fontFamily: 'Roboto' // NON
fontFamily: 'var(--font-ui)' // OUI
```
**Bordures arrondies à 24px+** sur des cartes (vibe trop SaaS pastel).
**Tooltip absent sur une icône isolée** :
```jsx
<button><Icon name="cog" /></button> // NON
<IconButton icon="cog" label="Configurer" onClick={fn} /> // OUI
```
**`window.alert` / `confirm`** — toujours utiliser `<Popup>`.
**Texte secondaire en `--ink-1`** — choisir la bonne couche d'encre selon la hiérarchie.
**Sur-utiliser le glow / shadow** — réservé aux accents importants.
**Mélanger les casses de label** — labels = uppercase mono, titres = sentence case.
---
## ✅ Patterns recommandés
### Hiérarchie de fond
- App / page → `--bg-1`
- Sidebar / headerbar → `--bg-2`
- Tuiles / cartes principales → `--bg-3` ou `.glass`
- Input fields / containers profonds → `--bg-1` avec inset shadow
### Effet glass standard
```jsx
className="glass" // backdrop-filter + bg semi-transparent + tile-3d shadow
```
ou pour plus marqué :
```jsx
className="glass-strong"
```
### Validation visuelle d'un état critique
```jsx
<StatusLed status="err" pulse /> // pastille pulsante
<Button variant="danger" icon="power"></Button>
// + bordure rouge sur le conteneur :
style={{ border: '1px solid var(--err)', boxShadow: 'inset 0 1px 0 rgba(251,73,52,0.2), 0 0 18px rgba(251,73,52,0.15)' }}
```
### Sticky footer d'actions (form)
```jsx
<div className="glass-strong" style={{
padding: '12px 20px',
display: 'flex', gap: 12, alignItems: 'center',
borderTop: '1px solid var(--border-2)',
}}>
<StatusLed status={dirty ? 'warn' : 'ok'} pulse={dirty} />
<span className="terminal">{dirty ? 'modifications non sauvegardées' : 'à jour'}</span>
<span style={{ flex: 1 }}></span>
<Button variant="ghost">Annuler</Button>
<Button variant="primary" icon="download">Enregistrer</Button>
</div>
```
---
## 🌗 Gestion des deux thèmes
**Règle d'or** : tout ce qui s'affiche doit être lisible et cohérent dans les deux thèmes.
Avant de livrer un écran, **mentalement (ou réellement) bascule `data-theme`** et vérifie :
- Les couleurs personnalisées (en dur) cassent forcément → utilise les tokens
- Les opacités blanches (`rgba(255,255,255,…)`) en dark passent mal en light → préfère les variables `--border-*`
- Les ombres très profondes en dark sont invisibles en light → utilise `--shadow-*` qui s'adapte
Pour basculer dynamiquement :
```jsx
document.documentElement.dataset.theme = 'light';
```
---
## 🪟 Cas particulier : applications GNOME
Pour GTK 4 / libadwaita :
1. Charger `tokens/tokens.gnome.css` via `GtkCssProvider`
2. Le fichier **override les couleurs sémantiques libadwaita** (`@accent_color`, `@window_bg_color`, etc.) — les widgets standards se ré-habillent automatiquement
3. Ajouter `add_css_class("tile")` pour le relief 3D, `("mono")` pour monospace, `("terminal")` pour Share Tech Mono
4. Pour les boutons accent : utiliser la classe libadwaita standard `suggested-action` (déjà restylée)
5. Pour danger : classe `destructive-action`
Polices : penser à installer ou bundler Inter / JetBrains Mono / Share Tech Mono dans le `.flatpak` / `.deb` (sinon GTK fallback sur Cantarell / DejaVu).
---
## 🎯 Quand l'utilisateur demande quelque chose…
### "Ajoute un bouton de déconnexion"
`<IconButton icon="power" label="Se déconnecter" danger />` ou
`<Button variant="danger" icon="power">Déconnexion</Button>`
### "Affiche le statut du serveur"
→ Combinaison `<StatusLed status="ok|warn|err" pulse />` + label texte. Le pulse uniquement si c'est critique/nouveau.
### "Mets une jauge CPU"
`<BatteryGauge compact value={cpu} label="cpu" icon="cpu" warnAt={70} errAt={85} />` (inline)
ou `<RadialGauge value={cpu} label="CPU" />` (visuel)
### "Crée une modale de confirmation"
`<Popup>` avec `footer={<><Button variant="ghost">Annuler</Button><Button variant="primary">Confirmer</Button></>}`
### "Liste hiérarchique des serveurs"
`<TreeNav>` avec `groups: [{ id, icon: 'server', label, count, open, children: [{ id, label, status, meta }] }]`
### "Affiche les logs"
→ Conteneur avec `font-family: var(--font-terminal)` + lignes colorées par niveau (ERROR → var(--err), WARN → var(--warn), INFO → var(--ink-2)).
### "Ajoute une option dark/light dans les réglages"
`<RadioGroup options={[{value:'dark', icon:'moon'}, {value:'light', icon:'sun'}, {value:'auto', icon:'clock'}]}>` + effet de bord :
```jsx
React.useEffect(() => { document.documentElement.dataset.theme = theme; }, [theme]);
```
---
## 📐 Tailles standards à respecter
| Élément | Taille / Padding |
|---------------|------------------------------------------|
| Boutons sm | h: 28px · pad: 5px 10px · font: 12px |
| Boutons md | h: 34px · pad: 7px 14px · font: 13px |
| Boutons lg | h: 40px · pad: 10px 18px · font: 14px |
| IconButton | 34px (default) · 26px (compact) |
| Inputs | pad: 9px 12px · font: 13px |
| Toggle | 42 × 22px |
| StatusLed | 8-14px diamètre |
| Header app | 48-56px hauteur |
| Sidebar | 200-260px largeur |
| Volet logs | 320-360px largeur |
| Status bar | 24-28px hauteur |
| Radius tuile | 10-12px |
| Radius button | 8px |
| Espacement | 8 / 12 / 14 / 18 / 24px (rythme bas) |
---
## 💡 Trucs pour ne pas se tromper
1. **Avant de créer un composant, cherche d'abord** dans `ui-kit.jsx`. 90% du temps il existe déjà.
2. **Avant d'inventer une couleur**, regarde les tokens. Tu as 6 fonds, 4 encres, 4 statuts, 2 datavis = largement assez.
3. **Si tu hésites sur une taille de police** : labels = 11px mono uppercase, body = 13-14px, kpi = 18-28px mono bold.
4. **Quand tu ajoutes une tuile**, mets `className="glass"` (ou `glass-strong` pour les modales) — tout le styling est inclus.
5. **Pour un état critique**, combine plusieurs signaux : couleur + pulse LED + icône + position visuelle. Pas juste une couleur.
6. **Quand l'utilisateur demande "un peu d'effet"** : pas de hover (sauf jauges), oui à la pression 3D, oui aux animations d'entrée 200-400ms `cubic-bezier(.3,.7,.3,1.2)`.
---
## 🔚 En cas de doute
- Pas sûr d'une couleur ? → tokens
- Pas sûr d'un composant ? → `ui-kit.jsx`
- Pas sûr d'un layout ? → `examples/exemple-minimal.html`
- Pas sûr d'une convention ? → ce fichier
Toujours préférer la cohérence avec l'existant à l'innovation.
Quand tu doutes, **demande-moi** plutôt que de deviner.
+115
View File
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<title>Exemple minimal — mon design system</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 1. Polices -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<!-- 2. Icônes -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- 3. Tokens du design system -->
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
body { padding: 32px; }
.row { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; flex-wrap: wrap; }
h2 { font-size: 20px; margin: 32px 0 12px; color: var(--ink-1); }
h2:first-child { margin-top: 0; }
p { color: var(--ink-3); margin: 0 0 8px; }
</style>
<!-- 4. React + composants -->
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="../components/ui-kit.jsx"></script>
<script type="text/babel">
function App() {
const [theme, setTheme] = React.useState('dark');
const [popupOpen, setPopupOpen] = React.useState(false);
const [auto, setAuto] = React.useState(true);
React.useEffect(() => { document.documentElement.dataset.theme = theme; }, [theme]);
return (
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 24 }}>
<Icon name="grid" size={28} style={{ color: 'var(--accent)' }} />
<h1 style={{ margin: 0, fontSize: 28 }}>Exemple minimal</h1>
<span style={{ flex: 1 }}></span>
<IconButton icon={theme === 'dark' ? 'sun' : 'moon'}
label={theme === 'dark' ? 'Mode clair' : 'Mode sombre'}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} />
</div>
<h2>Boutons</h2>
<div className="row">
<Button>défaut</Button>
<Button variant="primary" icon="play">primaire</Button>
<Button variant="ghost" icon="filter">ghost</Button>
<Button variant="danger" icon="power">danger</Button>
</div>
<h2>Boutons icônes (avec tooltip)</h2>
<div className="row">
<IconButton icon="refresh" label="Rafraîchir" />
<IconButton icon="cog" label="Configurer" primary />
<IconButton icon="bell" label="Notifications" />
<IconButton icon="power" label="Arrêter" danger />
</div>
<h2>Statuts</h2>
<div className="row">
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}><StatusLed status="ok" /> ok</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}><StatusLed status="warn" pulse /> warn</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}><StatusLed status="err" pulse /> err</span>
<Toggle on={auto} onChange={setAuto} label="Auto-refresh" icon="refresh" />
</div>
<h2>Jauges</h2>
<div className="row" style={{ alignItems: 'flex-end' }}>
<RadialGauge value={28} label="DISQUE" />
<RadialGauge value={64} label="CPU" warnAt={70} errAt={85} />
<RadialGauge value={92} label="RÉSEAU" warnAt={70} errAt={85} />
</div>
<div style={{ maxWidth: 520, marginTop: 12, display: 'flex', flexDirection: 'column', gap: 8 }}>
<BatteryGauge compact value={88} label="mémoire" icon="memory" />
<BatteryGauge compact value={64} label="cpu" icon="cpu" warnAt={70} errAt={85} />
<BatteryGauge compact value={28} label="disque" icon="disk" />
<BatteryGauge compact value={92} label="réseau" icon="network" warnAt={70} errAt={85} />
</div>
<h2>Popup</h2>
<Button variant="primary" icon="cog" onClick={() => setPopupOpen(true)}>Ouvrir la popup</Button>
<Popup open={popupOpen} onClose={() => setPopupOpen(false)}
title="Confirmer l'action"
footer={<>
<Button variant="ghost" onClick={() => setPopupOpen(false)}>Annuler</Button>
<Button variant="primary" onClick={() => setPopupOpen(false)}>OK</Button>
</>}>
<div style={{ fontSize: 14, color: 'var(--ink-2)', lineHeight: 1.5 }}>
Une popup glassmorphism centrée. Clic à l'extérieur ou Échap pour fermer.
</div>
</Popup>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+204
View File
@@ -0,0 +1,204 @@
/* ============================================================
ui-tokens.css
Design tokens Gruvbox Seventies — dark (par défaut) + light.
Sombre délavé (pas noir intense) / gris clair usé (pas blanc pur).
============================================================ */
:root,
[data-theme="dark"] {
/* Couches de fond — sombre délavé, brun-gris chaud */
--bg-0: #221c17; /* niveau le plus profond (rare) */
--bg-1: #2a231d; /* fond app */
--bg-2: #322a23; /* panneaux */
--bg-3: #3c332a; /* cartes */
--bg-4: #4a4035; /* hover */
--bg-5: #5a4f43; /* press / actif */
/* Surfaces translucides */
--surf-glass: rgba(50, 42, 35, 0.72);
--surf-glass-strong: rgba(50, 42, 35, 0.92);
--surf-glass-soft: rgba(50, 42, 35, 0.42);
/* Bordures */
--border-1: rgba(168, 153, 132, 0.18);
--border-2: rgba(168, 153, 132, 0.32);
--border-3: rgba(168, 153, 132, 0.55);
/* Texte */
--ink-1: #f2e5c7; /* cream principal */
--ink-2: #d5c4a1; /* secondaire */
--ink-3: #a89984; /* labels / hints */
--ink-4: #7c6f64; /* désactivé */
/* Accent orange seventies */
--accent: #fe8019;
--accent-soft: #d65d0e;
--accent-glow: rgba(254, 128, 25, 0.35);
--accent-tint: rgba(254, 128, 25, 0.12);
/* Statuts */
--ok: #4dbb26;
--ok-glow: rgba(77, 187, 38, 0.45);
--warn: #fabd2f;
--warn-glow: rgba(250, 189, 47, 0.45);
--err: #fb4934;
--err-glow: rgba(251, 73, 52, 0.4);
--info: #83a598;
--info-glow: rgba(131, 165, 152, 0.4);
/* Couleurs additionnelles (datavis, badges, catégories) */
--blue: #3db0d1;
--blue-glow: rgba(61, 176, 209, 0.45);
--purple: #c882c8;
--purple-glow: rgba(200, 130, 200, 0.45);
/* Ombres */
--shadow-1: 0 1px 2px rgba(0,0,0,0.4);
--shadow-2: 0 4px 12px rgba(0,0,0,0.45);
--shadow-3: 0 12px 32px rgba(0,0,0,0.55);
--shadow-press: inset 0 2px 4px rgba(0,0,0,0.5);
/* Relief 3D pour tuiles : highlight haut + ombre bas + ombre portée */
--tile-3d:
inset 0 1px 0 rgba(255, 230, 180, 0.12),
inset 0 -1px 0 rgba(0, 0, 0, 0.45),
0 1px 0 rgba(0, 0, 0, 0.35),
0 2px 4px rgba(0, 0, 0, 0.4),
0 8px 18px rgba(0, 0, 0, 0.5);
--tile-3d-strong:
inset 0 1px 0 rgba(255, 230, 180, 0.18),
inset 0 -2px 0 rgba(0, 0, 0, 0.55),
0 1px 0 rgba(0, 0, 0, 0.4),
0 4px 8px rgba(0, 0, 0, 0.5),
0 14px 28px rgba(0, 0, 0, 0.55);
/* Polices */
--font-ui: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--font-terminal: 'Share Tech Mono', 'VT323', 'Courier New', monospace;
}
[data-theme="light"] {
/* Gris clair usé, légèrement chaud (pas blanc pur) */
--bg-0: #b8b2a3;
--bg-1: #d5d0c5;
--bg-2: #dcd7cc;
--bg-3: #e3ded3;
--bg-4: #ccc6b8;
--bg-5: #bdb6a7;
--surf-glass: rgba(220, 215, 204, 0.72);
--surf-glass-strong: rgba(220, 215, 204, 0.94);
--surf-glass-soft: rgba(220, 215, 204, 0.42);
--border-1: rgba(60, 56, 54, 0.15);
--border-2: rgba(60, 56, 54, 0.28);
--border-3: rgba(60, 56, 54, 0.5);
--ink-1: #28241f;
--ink-2: #3c3836;
--ink-3: #5a544c;
--ink-4: #8a8278;
--accent: #af3a03;
--accent-soft: #d65d0e;
--accent-glow: rgba(175, 58, 3, 0.28);
--accent-tint: rgba(175, 58, 3, 0.08);
--ok: #3c911c;
--ok-glow: rgba(60, 145, 28, 0.32);
--warn: #b57614;
--warn-glow: rgba(181, 118, 20, 0.35);
--err: #9d0006;
--err-glow: rgba(157, 0, 6, 0.3);
--info: #427b58;
--info-glow: rgba(66, 123, 88, 0.3);
/* Couleurs additionnelles (datavis, badges, catégories) */
--blue: #2d82a3;
--blue-glow: rgba(45, 130, 163, 0.32);
--purple: #8c468c;
--purple-glow: rgba(140, 70, 140, 0.32);
--shadow-1: 0 1px 2px rgba(40,30,20,0.12);
--shadow-2: 0 4px 12px rgba(40,30,20,0.15);
--shadow-3: 0 12px 32px rgba(40,30,20,0.2);
--shadow-press: inset 0 2px 4px rgba(40,30,20,0.18);
/* Relief light : highlight haut blanc cassé + ombre marquée */
--tile-3d:
inset 0 1px 0 rgba(255, 255, 255, 0.55),
inset 0 -1px 0 rgba(60, 50, 40, 0.18),
0 1px 0 rgba(60, 50, 40, 0.1),
0 2px 4px rgba(60, 50, 40, 0.12),
0 8px 18px rgba(60, 50, 40, 0.18);
--tile-3d-strong:
inset 0 1px 0 rgba(255, 255, 255, 0.7),
inset 0 -2px 0 rgba(60, 50, 40, 0.22),
0 1px 0 rgba(60, 50, 40, 0.15),
0 4px 8px rgba(60, 50, 40, 0.18),
0 14px 28px rgba(60, 50, 40, 0.22);
}
/* ============================================================
Reset minimal + base typo
============================================================ */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: var(--font-ui);
font-size: 14px;
color: var(--ink-1);
background: var(--bg-1);
-webkit-font-smoothing: antialiased;
}
.mono { font-family: var(--font-mono); }
.terminal { font-family: var(--font-terminal); letter-spacing: 0.02em; }
.label {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-3);
}
/* ============================================================
Surfaces — relief 3D marqué, AUCUN effet hover
============================================================ */
.glass {
background: var(--surf-glass);
backdrop-filter: blur(12px) saturate(140%);
-webkit-backdrop-filter: blur(12px) saturate(140%);
border: 1px solid var(--border-2);
box-shadow: var(--tile-3d);
}
.glass-strong {
background: var(--surf-glass-strong);
backdrop-filter: blur(16px) saturate(150%);
-webkit-backdrop-filter: blur(16px) saturate(150%);
border: 1px solid var(--border-3);
box-shadow: var(--tile-3d-strong);
}
/* Élément cliquable : pas de hover, mais réelle pression 3D au clic */
.interactive {
cursor: pointer;
transition: transform .04s ease-out, box-shadow .04s, background .04s;
transform: translateY(0);
}
.interactive:active {
transform: translateY(1px);
box-shadow: var(--shadow-press) !important;
filter: brightness(0.92);
}
/* Scrollbar custom */
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
background: var(--border-2);
border-radius: 4px;
}
*::-webkit-scrollbar-thumb:hover { background: var(--accent-soft); }
+378
View File
@@ -0,0 +1,378 @@
/* ============================================================
tokens.gnome.css — Tokens pour applications GNOME (GTK 4 / libadwaita)
Gruvbox seventies · v1.0
============================================================
Usage dans une app GTK 4 / libadwaita :
#include <gtk/gtk.h>
GtkCssProvider *provider = gtk_css_provider_new();
gtk_css_provider_load_from_path(provider, "tokens.gnome.css");
gtk_style_context_add_provider_for_display(
gdk_display_get_default(), GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
Python (PyGObject) :
css_provider = Gtk.CssProvider()
css_provider.load_from_path("tokens.gnome.css")
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(), css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
GJS :
const provider = new Gtk.CssProvider();
provider.load_from_path('tokens.gnome.css');
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(), provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
============================================================ */
/* ============================================================
THÈME SOMBRE (défaut)
============================================================ */
/* Couches de fond (du plus profond au plus haut) */
@define-color bg_0 #221c17;
@define-color bg_1 #2a231d;
@define-color bg_2 #322a23;
@define-color bg_3 #3c332a;
@define-color bg_4 #4a4035;
@define-color bg_5 #5a4f43;
/* Encres / texte */
@define-color ink_1 #f2e5c7;
@define-color ink_2 #d5c4a1;
@define-color ink_3 #a89984;
@define-color ink_4 #7c6f64;
/* Accent orange seventies */
@define-color accent_color #fe8019;
@define-color accent_soft #d65d0e;
@define-color accent_fg_color #221c17;
/* Statuts */
@define-color success_color #4dbb26;
@define-color warning_color #fabd2f;
@define-color error_color #fb4934;
@define-color info_color #83a598;
@define-color blue_color #3db0d1;
@define-color purple_color #c882c8;
/* Bordures */
@define-color border_1 alpha(#a89984, 0.18);
@define-color border_2 alpha(#a89984, 0.32);
@define-color border_3 alpha(#a89984, 0.55);
/* Couleurs sémantiques GNOME / libadwaita (overrides) */
@define-color window_bg_color @bg_1;
@define-color window_fg_color @ink_1;
@define-color view_bg_color @bg_2;
@define-color view_fg_color @ink_1;
@define-color headerbar_bg_color @bg_2;
@define-color headerbar_fg_color @ink_1;
@define-color headerbar_border_color @border_2;
@define-color headerbar_backdrop_color @bg_1;
@define-color sidebar_bg_color @bg_2;
@define-color sidebar_fg_color @ink_1;
@define-color sidebar_backdrop_color @bg_1;
@define-color popover_bg_color @bg_3;
@define-color popover_fg_color @ink_1;
@define-color card_bg_color @bg_3;
@define-color card_fg_color @ink_1;
@define-color shade_color alpha(black, 0.4);
@define-color scrollbar_outline_color alpha(@ink_3, 0.3);
/* ============================================================
COMPOSANTS GTK — habillage Gruvbox seventies
============================================================ */
/* Fond global */
window {
background-color: @window_bg_color;
color: @window_fg_color;
font-family: 'Inter', 'Cantarell', sans-serif;
font-size: 14px;
}
/* HeaderBar (barre de titre) */
headerbar {
background: @bg_2;
color: @ink_1;
border-bottom: 1px solid @border_2;
box-shadow: inset 0 1px 0 alpha(white, 0.04);
min-height: 48px;
}
headerbar .title {
font-weight: 700;
font-size: 15px;
}
headerbar .subtitle {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: @ink_3;
}
/* Boutons — relief 3D et accent */
button {
background: @bg_3;
color: @ink_1;
border: 1px solid @border_2;
border-radius: 8px;
padding: 6px 12px;
font-weight: 500;
box-shadow:
inset 0 1px 0 alpha(white, 0.06),
inset 0 -1px 0 alpha(black, 0.3),
0 1px 2px alpha(black, 0.4);
transition: all 60ms ease;
}
button:active {
background: @bg_4;
box-shadow: inset 0 2px 4px alpha(black, 0.5);
transform: translateY(1px);
}
button:disabled {
color: @ink_4;
opacity: 0.6;
}
/* Bouton "suggested-action" = primary (accent orange) */
button.suggested-action {
background: @accent_color;
color: @accent_fg_color;
border-color: @accent_soft;
box-shadow:
inset 0 1px 0 alpha(white, 0.2),
0 2px 6px alpha(@accent_color, 0.35);
}
button.suggested-action:active {
background: @accent_soft;
}
/* Bouton "destructive-action" = danger */
button.destructive-action {
background: @bg_3;
color: @error_color;
border-color: @error_color;
}
/* Bouton plat (toolbar) */
button.flat {
background: transparent;
border-color: transparent;
box-shadow: none;
}
button.flat:hover {
background: @bg_3;
}
/* Champs de saisie */
entry,
text {
background: @bg_1;
color: @ink_1;
border: 1px solid @border_2;
border-radius: 8px;
padding: 8px 12px;
box-shadow: inset 0 1px 2px alpha(black, 0.3);
}
entry:focus,
text:focus {
border-color: @accent_color;
outline: 2px solid alpha(@accent_color, 0.18);
outline-offset: -1px;
}
/* Listes / treeview */
list,
treeview {
background: @bg_2;
color: @ink_1;
}
list > row {
padding: 8px 12px;
border-bottom: 1px solid @border_1;
}
list > row:selected,
treeview:selected {
background: alpha(@accent_color, 0.12);
color: @ink_1;
border-left: 3px solid @accent_color;
}
/* Switch (toggle) */
switch {
background: @bg_4;
border: 1px solid @border_2;
border-radius: 12px;
box-shadow: inset 0 1px 2px alpha(black, 0.4);
min-height: 22px;
min-width: 42px;
}
switch:checked {
background: @accent_color;
border-color: @accent_soft;
box-shadow: 0 0 10px alpha(@accent_color, 0.35);
}
switch slider {
background: @ink_2;
border-radius: 50%;
min-width: 18px;
min-height: 18px;
}
switch:checked slider {
background: @accent_fg_color;
}
/* Scale (slider) */
scale trough {
background: @bg_1;
border-radius: 4px;
min-height: 6px;
}
scale highlight {
background: @accent_color;
border-radius: 4px;
}
scale slider {
background: @ink_1;
border: 2px solid @accent_color;
border-radius: 50%;
min-width: 16px;
min-height: 16px;
box-shadow: 0 1px 4px alpha(black, 0.5);
}
/* Progress bar (jauge horizontale type batterie) */
progressbar trough {
background: @bg_1;
border: 1px solid @border_2;
border-radius: 4px;
box-shadow: inset 0 1px 2px alpha(black, 0.4);
min-height: 12px;
}
progressbar progress {
background: @success_color;
border-radius: 3px;
box-shadow: 0 0 8px alpha(@success_color, 0.45);
}
/* Niveaux de progression sémantiques (à appliquer via add_css_class) */
progressbar.warning progress { background: @warning_color; }
progressbar.error progress { background: @error_color; }
progressbar.info progress { background: @info_color; }
/* Notebook / onglets */
notebook header {
background: @bg_2;
border-bottom: 1px solid @border_2;
}
notebook tab {
padding: 8px 16px;
color: @ink_3;
border-top: 2px solid transparent;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
}
notebook tab:checked {
color: @ink_1;
border-top-color: @accent_color;
background: @bg_3;
}
/* Popover */
popover contents {
background: @bg_3;
color: @ink_1;
border: 1px solid @border_2;
border-radius: 10px;
padding: 6px;
box-shadow: 0 12px 32px alpha(black, 0.55);
}
/* Menubutton / dropdown */
menubutton button {
padding: 4px 8px;
}
/* Status pill (badge) — à appliquer sur GtkLabel.status */
label.status {
padding: 2px 8px;
border-radius: 999px;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
}
label.status.ok { background: alpha(@success_color, 0.18); color: @success_color; }
label.status.warn { background: alpha(@warning_color, 0.18); color: @warning_color; }
label.status.error { background: alpha(@error_color, 0.18); color: @error_color; }
label.status.info { background: alpha(@info_color, 0.18); color: @info_color; }
/* Texte monospace / terminal */
label.mono,
.mono {
font-family: 'JetBrains Mono', monospace;
}
label.terminal,
.terminal {
font-family: 'Share Tech Mono', 'VT323', monospace;
letter-spacing: 0.02em;
}
/* Carte tuile (à appliquer via add_css_class("tile")) */
.tile,
.card {
background: @bg_3;
color: @ink_1;
border: 1px solid @border_2;
border-radius: 12px;
padding: 14px;
box-shadow:
inset 0 1px 0 alpha(white, 0.06),
inset 0 -1px 0 alpha(black, 0.4),
0 2px 4px alpha(black, 0.4),
0 6px 14px alpha(black, 0.45);
}
/* Scrollbar */
scrollbar slider {
background: @border_2;
border-radius: 4px;
min-width: 6px;
min-height: 6px;
}
scrollbar slider:hover {
background: @accent_soft;
}
/* ============================================================
THÈME CLAIR — à charger en alternative
Pour appliquer le thème clair, charger ce fichier puis
`tokens.gnome.light.css` (à dupliquer en remplaçant
les @define-color des fonds et encres) OU appliquer
un settings GTK light :
g_object_set(gtk_settings, "gtk-application-prefer-dark-theme",
FALSE, NULL);
Et fournir un fichier dérivé avec les valeurs ci-dessous :
============================================================ */
/*
bg_0: #b8b2a3
bg_1: #d5d0c5
bg_2: #dcd7cc
bg_3: #e3ded3
bg_4: #ccc6b8
bg_5: #bdb6a7
ink_1: #28241f
ink_2: #3c3836
ink_3: #5a544c
ink_4: #8a8278
accent_color: #af3a03
success_color: #3c911c
warning_color: #b57614
error_color: #9d0006
info_color: #427b58
blue_color: #2d82a3
purple_color: #8c468c
*/
+136
View File
@@ -0,0 +1,136 @@
{
"$schema": "design-tokens-v1",
"name": "mon design system — gruvbox seventies",
"version": "1.0.0",
"description": "Design system Gruvbox seventies. Orange brûlé, fond brun délavé en sombre / gris clair usé en clair. Deux thèmes dark/light parfaitement à parité.",
"themes": {
"dark": {
"bg": {
"0": { "value": "#221c17", "description": "Niveau le plus profond, rare" },
"1": { "value": "#2a231d", "description": "Fond application principal" },
"2": { "value": "#322a23", "description": "Panneaux (sidebar, headerbar)" },
"3": { "value": "#3c332a", "description": "Cartes, tuiles" },
"4": { "value": "#4a4035", "description": "Hover, état actif" },
"5": { "value": "#5a4f43", "description": "Press, sélection forte" }
},
"ink": {
"1": { "value": "#f2e5c7", "description": "Texte principal (cream)" },
"2": { "value": "#d5c4a1", "description": "Texte secondaire" },
"3": { "value": "#a89984", "description": "Labels, hints" },
"4": { "value": "#7c6f64", "description": "Désactivé" }
},
"accent": {
"primary": { "value": "#fe8019", "description": "Orange Gruvbox seventies" },
"soft": { "value": "#d65d0e", "description": "Orange foncé (hover, bordures)" },
"glow": { "value": "rgba(254, 128, 25, 0.35)" },
"tint": { "value": "rgba(254, 128, 25, 0.12)" }
},
"status": {
"ok": { "value": "#4dbb26" },
"warn": { "value": "#fabd2f" },
"err": { "value": "#fb4934" },
"info": { "value": "#83a598" }
},
"extra": {
"blue": { "value": "#3db0d1" },
"purple": { "value": "#c882c8" }
},
"border": {
"1": { "value": "rgba(168, 153, 132, 0.18)" },
"2": { "value": "rgba(168, 153, 132, 0.32)" },
"3": { "value": "rgba(168, 153, 132, 0.55)" }
}
},
"light": {
"bg": {
"0": { "value": "#b8b2a3", "description": "Niveau le plus profond" },
"1": { "value": "#d5d0c5", "description": "Fond application principal" },
"2": { "value": "#dcd7cc", "description": "Panneaux" },
"3": { "value": "#e3ded3", "description": "Cartes, tuiles" },
"4": { "value": "#ccc6b8", "description": "Hover" },
"5": { "value": "#bdb6a7", "description": "Press" }
},
"ink": {
"1": { "value": "#28241f", "description": "Texte principal" },
"2": { "value": "#3c3836", "description": "Texte secondaire" },
"3": { "value": "#5a544c", "description": "Labels, hints" },
"4": { "value": "#8a8278", "description": "Désactivé" }
},
"accent": {
"primary": { "value": "#af3a03", "description": "Orange brûlé (variante contrastée)" },
"soft": { "value": "#d65d0e" },
"glow": { "value": "rgba(175, 58, 3, 0.28)" },
"tint": { "value": "rgba(175, 58, 3, 0.08)" }
},
"status": {
"ok": { "value": "#3c911c" },
"warn": { "value": "#b57614" },
"err": { "value": "#9d0006" },
"info": { "value": "#427b58" }
},
"extra": {
"blue": { "value": "#2d82a3" },
"purple": { "value": "#8c468c" }
},
"border": {
"1": { "value": "rgba(60, 56, 54, 0.15)" },
"2": { "value": "rgba(60, 56, 54, 0.28)" },
"3": { "value": "rgba(60, 56, 54, 0.5)" }
}
}
},
"typography": {
"fonts": {
"ui": { "family": "Inter", "weights": [400, 500, 600, 700], "fallback": ["Cantarell", "system-ui", "sans-serif"] },
"mono": { "family": "JetBrains Mono", "weights": [400, 500, 600, 700], "fallback": ["ui-monospace", "monospace"] },
"terminal": { "family": "Share Tech Mono", "weights": [400], "fallback": ["VT323", "Courier New", "monospace"] }
},
"scale": {
"label": { "size": 11, "weight": 500, "transform": "uppercase", "tracking": "0.08em", "family": "mono" },
"caption": { "size": 12, "weight": 400, "family": "ui" },
"body": { "size": 14, "weight": 400, "family": "ui" },
"body-emph": { "size": 14, "weight": 600, "family": "ui" },
"title": { "size": 18, "weight": 700, "family": "ui" },
"h2": { "size": 22, "weight": 700, "family": "ui" },
"h1": { "size": 28, "weight": 700, "family": "ui" },
"display": { "size": 44, "weight": 700, "family": "ui" },
"kpi": { "size": 28, "weight": 700, "family": "mono" }
}
},
"radius": {
"xs": 3,
"sm": 4,
"md": 6,
"lg": 8,
"xl": 10,
"2xl": 12,
"pill": 999
},
"spacing": {
"1": 4,
"2": 6,
"3": 8,
"4": 10,
"5": 12,
"6": 14,
"7": 16,
"8": 18,
"9": 20,
"10": 24,
"12": 32,
"14": 40,
"16": 56
},
"shadows": {
"1": "0 1px 2px rgba(0,0,0,0.4)",
"2": "0 4px 12px rgba(0,0,0,0.45)",
"3": "0 12px 32px rgba(0,0,0,0.55)",
"press": "inset 0 2px 4px rgba(0,0,0,0.5)",
"tile3d": "inset 0 1px 0 rgba(255,230,180,0.12), inset 0 -1px 0 rgba(0,0,0,0.45), 0 1px 0 rgba(0,0,0,0.35), 0 2px 4px rgba(0,0,0,0.4), 0 8px 18px rgba(0,0,0,0.5)"
},
"motion": {
"fast": "60ms ease",
"normal": "180ms cubic-bezier(.3,.7,.3,1.2)",
"slow": "400ms cubic-bezier(.3,.6,.3,1)"
}
}