Téléverser les fichiers vers "/"
first
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
+1015
File diff suppressed because it is too large
Load Diff
+656
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user