Files

1016 lines
43 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<title>Exemple complet — mon design system</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="../tokens/tokens.css">
<style>
body {
background:
radial-gradient(ellipse at top, var(--bg-2) 0%, var(--bg-1) 60%);
min-height: 100vh;
}
/* Topbar */
.topbar {
position: sticky; top: 0; z-index: 50;
padding: 14px 24px;
background: var(--surf-glass-strong);
backdrop-filter: blur(14px) saturate(150%);
border-bottom: 1px solid var(--border-2);
display: flex; align-items: center; gap: 14px;
box-shadow: var(--shadow-2);
}
.topbar .logo {
width: 32px; height: 32px; border-radius: 8px;
background: var(--accent);
color: var(--bg-1);
display: flex; align-items: center; justify-content: center;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.3), 0 2px 6px var(--accent-glow);
}
.topbar h1 {
margin: 0; font-size: 17px; font-weight: 700;
}
.topbar h1 small {
font-family: var(--font-mono);
font-size: 10px; letter-spacing: 0.1em;
color: var(--ink-3); font-weight: 400;
margin-left: 8px; text-transform: uppercase;
}
/* Layout : sidebar + content */
.shell { display: flex; min-height: calc(100vh - 60px); }
.side-nav {
width: 220px; flex: 0 0 auto;
padding: 20px 8px;
border-right: 1px solid var(--border-2);
background: var(--bg-2);
position: sticky; top: 60px; align-self: flex-start;
max-height: calc(100vh - 60px);
overflow-y: auto;
}
.side-nav h3 {
margin: 14px 12px 4px;
font-family: var(--font-mono);
font-size: 10px; letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-3);
}
.side-nav a {
display: block;
padding: 6px 12px;
color: var(--ink-2);
text-decoration: none;
border-radius: 6px;
font-size: 13px;
border-left: 3px solid transparent;
margin-bottom: 1px;
}
.side-nav a:hover {
background: var(--bg-3);
color: var(--ink-1);
}
main { flex: 1; min-width: 0; padding: 32px 40px 80px; }
section { margin-bottom: 56px; max-width: 1100px; scroll-margin-top: 80px; }
section > h2 {
font-size: 24px; margin: 0 0 4px;
display: flex; align-items: center; gap: 12px;
}
section > .desc {
color: var(--ink-3); font-size: 14px; margin: 0 0 20px;
max-width: 720px; line-height: 1.5;
}
/* Cards demo */
.demo {
padding: 22px;
background: var(--bg-2);
border: 1px solid var(--border-2);
border-radius: 10px;
box-shadow: var(--tile-3d);
margin-bottom: 14px;
}
.demo h3 {
font-family: var(--font-mono); font-size: 10px;
letter-spacing: 0.08em; text-transform: uppercase;
color: var(--ink-3); margin: 0 0 14px;
}
.demo .row {
display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
}
.demo .col {
display: flex; flex-direction: column; gap: 12px;
}
.demo .grid-3 {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px;
}
.demo .grid-4 {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px;
}
/* Color swatches */
.swatch-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 8px;
}
.swatch {
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border-2);
box-shadow: var(--shadow-1);
}
.swatch .sw-color { height: 50px; }
.swatch .sw-meta {
padding: 6px 9px;
background: var(--bg-3);
}
.swatch .sw-var {
font-family: var(--font-mono); font-size: 10px; color: var(--ink-1);
}
.swatch .sw-hex {
font-family: var(--font-mono); font-size: 9.5px; color: var(--ink-3);
}
/* Typography sample */
.typo-sample {
display: grid;
grid-template-columns: 120px 1fr;
gap: 18px;
align-items: center;
padding: 12px 0;
border-bottom: 1px dashed var(--border-1);
}
.typo-sample:last-child { border-bottom: 0; }
.typo-sample .name {
font-family: var(--font-mono);
font-size: 10px; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--ink-3);
}
/* Icon grid */
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
gap: 8px;
}
.icon-cell {
display: flex; flex-direction: column; align-items: center;
gap: 6px; padding: 12px 6px;
background: var(--bg-3);
border: 1px solid var(--border-2);
border-radius: 6px;
}
.icon-cell .name {
font-family: var(--font-mono);
font-size: 9.5px;
color: var(--ink-3);
}
/* Native HTML form elements stylés avec tokens */
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.field-label {
font-family: var(--font-mono); font-size: 10px;
letter-spacing: 0.08em; text-transform: uppercase;
color: var(--ink-3);
}
input[type="text"], input[type="email"], input[type="password"],
input[type="time"], input[type="number"], textarea, select {
padding: 9px 12px;
background: var(--bg-1);
border: 1px solid var(--border-2);
border-radius: 8px;
color: var(--ink-1);
font-family: var(--font-ui); font-size: 13px;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3);
outline: none;
}
input:focus, textarea:focus, select:focus {
border-color: var(--accent);
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3), 0 0 0 2px var(--accent-tint);
}
textarea { resize: vertical; font-family: var(--font-mono); font-size: 12px; }
input[type="range"] { accent-color: var(--accent); }
/* Status pill (utilisable en HTML pur) */
.pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 3px 10px;
border-radius: 999px;
font-family: var(--font-mono);
font-size: 10px; font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.pill-ok { background: rgba(77,187,38,0.18); color: var(--ok); border: 1px solid var(--ok); }
.pill-warn { background: rgba(250,189,47,0.18); color: var(--warn); border: 1px solid var(--warn); }
.pill-err { background: rgba(251,73,52,0.18); color: var(--err); border: 1px solid var(--err); }
.pill-info { background: rgba(131,165,152,0.2); color: var(--info); border: 1px solid var(--info); }
/* Logs / terminal block */
.term-block {
background: #15110c;
border: 1px solid var(--border-2);
border-radius: 8px;
padding: 14px 16px;
font-family: var(--font-terminal);
font-size: 13px; line-height: 1.55;
letter-spacing: 0.02em;
color: var(--ink-2);
position: relative;
overflow: hidden;
}
.term-block::before {
content: '';
position: absolute; inset: 0;
background: repeating-linear-gradient(0deg, transparent 0 2px, rgba(0,0,0,0.18) 2px 3px);
pointer-events: none;
}
.term-block .prompt { color: var(--accent); font-weight: 700; }
.term-block .ok { color: var(--ok); }
.term-block .warn { color: var(--warn); }
.term-block .err { color: var(--err); }
.term-block .info { color: var(--info); }
.term-block .dim { color: var(--ink-4); }
</style>
<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">
const { useState, useEffect } = React;
/* ============================================================
TOPBAR + thème switcher
============================================================ */
function Topbar({ theme, setTheme }) {
return (
<header className="topbar">
<div className="logo"><Icon name="grid" size={16} /></div>
<h1>Exemple complet <small>tous les composants · v1.0</small></h1>
<span style={{ flex: 1 }}></span>
<a href="exemple-minimal.html" style={{
fontFamily: 'var(--font-mono)', fontSize: 12,
color: 'var(--ink-3)', textDecoration: 'none',
display: 'inline-flex', alignItems: 'center', gap: 6,
}}>
<Icon name="chevL" size={12} /> exemple minimal
</a>
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '5px 10px',
background: 'var(--bg-3)',
border: '1px solid var(--border-2)',
borderRadius: 8,
}}>
<Icon name={theme === 'dark' ? 'moon' : 'sun'} size={12} />
<span className="label" style={{ fontSize: 10 }}>{theme}</span>
<Toggle on={theme === 'light'} onChange={(v) => setTheme(v ? 'light' : 'dark')} />
</div>
</header>
);
}
/* ============================================================
SIDE NAV
============================================================ */
const SECTIONS = [
{ id: 'colors', label: 'Couleurs' },
{ id: 'typo', label: 'Typographie' },
{ id: 'shadows', label: 'Ombres & relief' },
{ id: 'icons', label: 'Icônes' },
{ id: 'buttons', label: 'Boutons' },
{ id: 'iconbtns', label: 'Boutons icônes' },
{ id: 'toggles', label: 'Toggles' },
{ id: 'leds', label: 'StatusLed' },
{ id: 'tooltips', label: 'Tooltips' },
{ id: 'gauges', label: 'Jauges' },
{ id: 'charts', label: 'Graphes' },
{ id: 'tree', label: 'TreeNav' },
{ id: 'popup', label: 'Popup' },
{ id: 'forms', label: 'Formulaires' },
{ id: 'pills', label: 'Badges / Pills' },
{ id: 'terminal', label: 'Logs & Terminal' },
];
function Nav() {
return (
<nav className="side-nav">
<h3>Tokens</h3>
{SECTIONS.slice(0, 3).map((s) => <a key={s.id} href={'#' + s.id}>{s.label}</a>)}
<h3>Atomes</h3>
{SECTIONS.slice(3, 9).map((s) => <a key={s.id} href={'#' + s.id}>{s.label}</a>)}
<h3>Datavis</h3>
{SECTIONS.slice(9, 11).map((s) => <a key={s.id} href={'#' + s.id}>{s.label}</a>)}
<h3>Molécules</h3>
{SECTIONS.slice(11).map((s) => <a key={s.id} href={'#' + s.id}>{s.label}</a>)}
</nav>
);
}
/* ============================================================
COULEURS
============================================================ */
function Swatches({ list }) {
return (
<div className="swatch-grid">
{list.map(([k, v]) => (
<div key={k} className="swatch">
<div className="sw-color" style={{ background: v }}></div>
<div className="sw-meta">
<div className="sw-var">{k}</div>
<div className="sw-hex">{v}</div>
</div>
</div>
))}
</div>
);
}
const SW_BG_DARK = [['--bg-0','#221c17'],['--bg-1','#2a231d'],['--bg-2','#322a23'],['--bg-3','#3c332a'],['--bg-4','#4a4035'],['--bg-5','#5a4f43']];
const SW_INK_DARK = [['--ink-1','#f2e5c7'],['--ink-2','#d5c4a1'],['--ink-3','#a89984'],['--ink-4','#7c6f64']];
const SW_BG_LIGHT = [['--bg-0','#b8b2a3'],['--bg-1','#d5d0c5'],['--bg-2','#dcd7cc'],['--bg-3','#e3ded3'],['--bg-4','#ccc6b8'],['--bg-5','#bdb6a7']];
const SW_INK_LIGHT = [['--ink-1','#28241f'],['--ink-2','#3c3836'],['--ink-3','#5a544c'],['--ink-4','#8a8278']];
const SW_ACCENT_DARK = [['--accent','#fe8019'],['--accent-soft','#d65d0e']];
const SW_ACCENT_LIGHT = [['--accent','#af3a03'],['--accent-soft','#d65d0e']];
const SW_STATUS_DARK = [['--ok','#4dbb26'],['--warn','#fabd2f'],['--err','#fb4934'],['--info','#83a598']];
const SW_STATUS_LIGHT = [['--ok','#3c911c'],['--warn','#b57614'],['--err','#9d0006'],['--info','#427b58']];
const SW_EXTRA_DARK = [['--blue','#3db0d1'],['--purple','#c882c8']];
const SW_EXTRA_LIGHT = [['--blue','#2d82a3'],['--purple','#8c468c']];
function ColorsSection({ theme }) {
const sets = theme === 'dark'
? { bg: SW_BG_DARK, ink: SW_INK_DARK, accent: SW_ACCENT_DARK, status: SW_STATUS_DARK, extra: SW_EXTRA_DARK }
: { bg: SW_BG_LIGHT, ink: SW_INK_LIGHT, accent: SW_ACCENT_LIGHT, status: SW_STATUS_LIGHT, extra: SW_EXTRA_LIGHT };
return (
<section id="colors">
<h2><Icon name="grid" size={22} style={{ color: 'var(--accent)' }} /> Couleurs</h2>
<p className="desc">19 tokens couleur par thème. Bascule le thème en haut à droite pour comparer dark / light.</p>
<div className="demo">
<h3>Fonds (du plus profond au plus haut)</h3>
<Swatches list={sets.bg} />
</div>
<div className="demo">
<h3>Encres / texte</h3>
<Swatches list={sets.ink} />
</div>
<div className="demo">
<h3>Accent</h3>
<Swatches list={sets.accent} />
</div>
<div className="demo">
<h3>Statuts</h3>
<Swatches list={sets.status} />
</div>
<div className="demo">
<h3>Datavis additionnel</h3>
<Swatches list={sets.extra} />
</div>
</section>
);
}
/* ============================================================
TYPO
============================================================ */
function TypoSection() {
return (
<section id="typo">
<h2><Icon name="list" size={22} style={{ color: 'var(--accent)' }} /> Typographie</h2>
<p className="desc">3 polices. Une seule règle : Inter pour l'UI, JetBrains Mono pour les données, Share Tech Mono pour les logs/terminal.</p>
<div className="demo">
<h3>Inter — UI</h3>
<div className="typo-sample"><span className="name">display</span><div style={{ fontSize: 44, fontWeight: 700, lineHeight: 1 }}>Santé système</div></div>
<div className="typo-sample"><span className="name">h1 / 28px</span><div style={{ fontSize: 28, fontWeight: 700 }}>Tableau de bord</div></div>
<div className="typo-sample"><span className="name">h2 / 22px</span><div style={{ fontSize: 22, fontWeight: 700 }}>Sous-titre</div></div>
<div className="typo-sample"><span className="name">title / 18px</span><div style={{ fontSize: 18, fontWeight: 700 }}>Titre de carte</div></div>
<div className="typo-sample"><span className="name">body / 14px</span><div style={{ fontSize: 14 }}>Paragraphe corps de texte, lisible 14px, line-height 1.5</div></div>
<div className="typo-sample"><span className="name">caption / 12px</span><div style={{ fontSize: 12, color: 'var(--ink-3)' }}>Texte secondaire, hints, métadonnées</div></div>
</div>
<div className="demo">
<h3>JetBrains Mono — données</h3>
<div className="typo-sample"><span className="name">kpi / 32px</span><div className="mono" style={{ fontSize: 32, fontWeight: 700 }}>87<span style={{ color: 'var(--ink-3)', fontSize: 18 }}>%</span></div></div>
<div className="typo-sample"><span className="name">value / 18px</span><div className="mono" style={{ fontSize: 18, fontWeight: 600 }}>10.0.1.236</div></div>
<div className="typo-sample"><span className="name">data / 13px</span><div className="mono" style={{ fontSize: 13 }}>14:02:11 · node-03 · 0.4ms</div></div>
<div className="typo-sample"><span className="name">label / 11px</span><div className="label">label uppercase</div></div>
</div>
<div className="demo">
<h3>Share Tech Mono — logs / terminal</h3>
<div className="typo-sample"><span className="name">prompt</span><div className="terminal" style={{ fontSize: 16 }}><span style={{ color: 'var(--accent)', fontWeight: 700 }}>root@srv</span>:~$ <span style={{ color: 'var(--ink-3)' }}>tail -f /var/log/ops.log</span></div></div>
<div className="typo-sample"><span className="name">log line</span><div className="terminal" style={{ fontSize: 14 }}><span style={{ color: 'var(--ink-4)' }}>14:02:11</span> <span style={{ color: 'var(--ok)' }}>INFO</span> scan terminé · 1022 IPs</div></div>
</div>
</section>
);
}
/* ============================================================
OMBRES
============================================================ */
function ShadowsSection() {
const items = [
{ name: '--shadow-1', style: { boxShadow: 'var(--shadow-1)' }, desc: 'Élévation discrète' },
{ name: '--shadow-2', style: { boxShadow: 'var(--shadow-2)' }, desc: 'Élévation standard' },
{ name: '--shadow-3', style: { boxShadow: 'var(--shadow-3)' }, desc: 'Élévation modale' },
{ name: '--shadow-press', style: { boxShadow: 'var(--shadow-press)', background: 'var(--bg-1)' }, desc: 'État pressé (inset)' },
{ name: '--tile-3d', style: { boxShadow: 'var(--tile-3d)' }, desc: 'Relief 3D marqué' },
{ name: '--tile-3d-strong', style: { boxShadow: 'var(--tile-3d-strong)' }, desc: 'Relief 3D fort' },
];
return (
<section id="shadows">
<h2><Icon name="bars" size={22} style={{ color: 'var(--accent)' }} /> Ombres & relief</h2>
<p className="desc">6 niveaux d'élévation pour hiérarchiser les surfaces.</p>
<div className="demo">
<div className="grid-3">
{items.map((s) => (
<div key={s.name} style={{
padding: 18,
background: 'var(--bg-3)',
borderRadius: 10,
border: '1px solid var(--border-2)',
minHeight: 90,
display: 'flex', flexDirection: 'column', justifyContent: 'center',
...s.style,
}}>
<div className="mono" style={{ fontSize: 11, color: 'var(--accent)', fontWeight: 600 }}>{s.name}</div>
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 4 }}>{s.desc}</div>
</div>
))}
</div>
</div>
</section>
);
}
/* ============================================================
ICÔNES
============================================================ */
const ALL_ICONS = ['cpu','memory','disk','network','clock','grid','list','cog','alert','bell','server','chart','bars','terminal','refresh','play','pause','power','sun','moon','search','close','plus','filter','download','folder','node','user','chevR','chevL','chevD','chevU'];
function IconsSection() {
return (
<section id="icons">
<h2><Icon name="grid" size={22} style={{ color: 'var(--accent)' }} /> Icônes</h2>
<p className="desc">32 icônes Font Awesome 6 mappées sous des noms logiques. Toujours via &lt;Icon name="…" /&gt;.</p>
<div className="demo">
<div className="icon-grid">
{ALL_ICONS.map((n) => (
<div key={n} className="icon-cell">
<Icon name={n} size={20} style={{ color: 'var(--ink-1)' }} />
<span className="name">{n}</span>
</div>
))}
</div>
</div>
</section>
);
}
/* ============================================================
BOUTONS
============================================================ */
function ButtonsSection() {
return (
<section id="buttons">
<h2><Icon name="play" size={22} style={{ color: 'var(--accent)' }} /> Boutons</h2>
<p className="desc">4 variantes (default, primary, ghost, danger) · 3 tailles (sm, md, lg) · icône optionnelle.</p>
<div className="demo">
<h3>Variantes</h3>
<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>
</div>
<div className="demo">
<h3>Tailles</h3>
<div className="row">
<Button size="sm" variant="primary" icon="play">petit (sm)</Button>
<Button size="md" variant="primary" icon="play">moyen (md)</Button>
<Button size="lg" variant="primary" icon="play">grand (lg)</Button>
</div>
</div>
<div className="demo">
<h3>Avec / sans icône</h3>
<div className="row">
<Button variant="primary">sans icône</Button>
<Button variant="primary" icon="cog">avec icône à gauche</Button>
<Button variant="ghost" icon="download">exporter</Button>
<Button variant="ghost" icon="refresh">rafraîchir</Button>
</div>
</div>
</section>
);
}
/* ============================================================
ICONBUTTONS
============================================================ */
function IconBtnsSection() {
return (
<section id="iconbtns">
<h2><Icon name="cog" size={22} style={{ color: 'var(--accent)' }} /> Boutons icônes</h2>
<p className="desc">Bouton icône seul, tooltip obligatoire (apparaît au survol après ~280 ms). Variantes : default, primary, active, danger.</p>
<div className="demo">
<h3>Variantes (survoler pour voir les tooltips)</h3>
<div className="row">
<IconButton icon="refresh" label="Rafraîchir" />
<IconButton icon="cog" label="Configurer" primary />
<IconButton icon="play" label="Démarrer" active />
<IconButton icon="power" label="Arrêter le service" danger />
<IconButton icon="bell" label="3 notifications" />
<IconButton icon="search" label="Rechercher" />
<IconButton icon="filter" label="Filtrer" />
<IconButton icon="download" label="Exporter" />
</div>
</div>
<div className="demo">
<h3>Tailles</h3>
<div className="row">
<IconButton icon="cog" label="petit (26)" size={26} />
<IconButton icon="cog" label="moyen (34, défaut)" />
<IconButton icon="cog" label="grand (44)" size={44} />
</div>
</div>
</section>
);
}
/* ============================================================
TOGGLES
============================================================ */
function TogglesSection() {
const [a, setA] = useState(true);
const [b, setB] = useState(false);
const [c, setC] = useState(true);
return (
<section id="toggles">
<h2><Icon name="power" size={22} style={{ color: 'var(--accent)' }} /> Toggles</h2>
<p className="desc">Switch on/off avec glow accent quand actif. Label et icône optionnels.</p>
<div className="demo">
<div className="col">
<Toggle on={a} onChange={setA} label="auto-refresh" icon="refresh" />
<Toggle on={b} onChange={setB} label="mode debug" icon="terminal" />
<Toggle on={c} onChange={setC} label="notifications" icon="bell" />
<Toggle on={true} onChange={() => {}} />
<Toggle on={false} onChange={() => {}} />
</div>
</div>
</section>
);
}
/* ============================================================
STATUS LEDs
============================================================ */
function LedsSection() {
return (
<section id="leds">
<h2><Icon name="alert" size={22} style={{ color: 'var(--accent)' }} /> Status LEDs</h2>
<p className="desc">Pastille colorée + halo. 5 statuts × option pulse.</p>
<div className="demo">
<h3>Statuts standard</h3>
<div className="row" style={{ gap: 22 }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}><StatusLed status="ok" size={14} /> ok</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}><StatusLed status="warn" size={14} /> warn</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}><StatusLed status="err" size={14} /> err</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}><StatusLed status="info" size={14} /> info</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}><StatusLed status="off" size={14} /> off</span>
</div>
</div>
<div className="demo">
<h3>Avec pulse (pour signaler un changement / état critique)</h3>
<div className="row" style={{ gap: 22 }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}><StatusLed status="ok" pulse size={14} /> ok pulse</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}><StatusLed status="warn" pulse size={14} /> warn pulse</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}><StatusLed status="err" pulse size={14} /> err pulse</span>
</div>
</div>
<div className="demo">
<h3>Tailles</h3>
<div className="row" style={{ gap: 16, alignItems: 'center' }}>
<StatusLed status="ok" size={6} />
<StatusLed status="ok" size={9} />
<StatusLed status="ok" size={14} />
<StatusLed status="ok" size={20} />
<StatusLed status="ok" size={28} />
</div>
</div>
</section>
);
}
/* ============================================================
TOOLTIPS
============================================================ */
function TooltipsSection() {
return (
<section id="tooltips">
<h2><Icon name="alert" size={22} style={{ color: 'var(--accent)' }} /> Tooltips</h2>
<p className="desc">Bulle d'info au survol, 4 positions disponibles.</p>
<div className="demo">
<div className="row">
<Tooltip label="top (défaut)"><Button>haut</Button></Tooltip>
<Tooltip label="position bottom" side="bottom"><Button>bas</Button></Tooltip>
<Tooltip label="position left" side="left"><Button>gauche</Button></Tooltip>
<Tooltip label="position right" side="right"><Button>droite</Button></Tooltip>
<Tooltip label="bulle plus longue avec quelques mots"><Button variant="ghost">label long</Button></Tooltip>
</div>
</div>
</section>
);
}
/* ============================================================
JAUGES
============================================================ */
function GaugesSection() {
return (
<section id="gauges">
<h2><Icon name="disk" size={22} style={{ color: 'var(--accent)' }} /> Jauges</h2>
<p className="desc">Radiales (rondes) ou Batterie (horizontales). Glow lumineux au survol. Couleur auto selon seuils warnAt/errAt, ou couleur fixe.</p>
<div className="demo">
<h3>Radiales — 3 tailles, 3 niveaux</h3>
<div className="row" style={{ alignItems: 'flex-end', justifyContent: 'space-around' }}>
<RadialGauge value={28} label="OK" size={100} />
<RadialGauge value={72} label="WARN" size={120} />
<RadialGauge value={93} label="ERR" size={140} />
</div>
</div>
<div className="demo">
<h3>BigRadialGauge — métrique héro cockpit</h3>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<BigRadialGauge value={87} label="score santé · stable" />
</div>
</div>
<div className="demo">
<h3>Battery — stack vertical (standard)</h3>
<div className="col">
<BatteryGauge value={28} label="DISQUE" />
<BatteryGauge value={64} label="CPU" warnAt={70} errAt={85} />
<BatteryGauge value={88} label="RÉSEAU" warnAt={70} errAt={85} />
<BatteryGauge value={42} label="MÉMOIRE" height={14} />
</div>
</div>
<div className="demo">
<h3>Battery compact — inline 1 ligne (idéal listes denses)</h3>
<div className="col" style={{ 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} />
<BatteryGauge compact value={1.4} max={4} unit="Go" label="ram libre" icon="memory" color="var(--blue)" />
<BatteryGauge compact value={68} label="charge" icon="cpu" color="var(--purple)" />
</div>
</div>
</section>
);
}
/* ============================================================
GRAPHES
============================================================ */
function ChartsSection() {
return (
<section id="charts">
<h2><Icon name="chart" size={22} style={{ color: 'var(--accent)' }} /> Graphes</h2>
<p className="desc">Sparkline pour KPI compacts. LineChart pour évolution multi-séries.</p>
<div className="demo">
<h3>Sparkline (dans des KPI tiles)</h3>
<div className="grid-4">
{[
{ label: 'CPU', val: 64, unit: '%', color: 'var(--accent)', pts: [40,45,38,50,55,60,58,62,65,64,66,64], icon: 'cpu' },
{ label: 'Mémoire', val: 42, unit: '%', color: 'var(--info)', pts: [30,32,35,38,40,41,42,42,42,42,42,42], icon: 'memory' },
{ label: 'Réseau', val: 8.4, unit: 'Mb', color: 'var(--ok)', pts: [2,4,3,6,5,8,7,9,8,8.4,7.8,8.4], icon: 'network' },
{ label: 'Latence', val: 12, unit: 'ms', color: 'var(--purple)', pts: [10,11,9,12,11,13,12,14,12,11,12,12], icon: 'clock' },
].map((k, i) => (
<div key={i} style={{
padding: 12, borderRadius: 10,
background: 'var(--bg-3)',
border: '1px solid var(--border-2)',
boxShadow: 'var(--tile-3d)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<Icon name={k.icon} size={12} style={{ color: k.color }} />
<span className="label">{k.label}</span>
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 4 }}>
<span className="mono" style={{ fontSize: 24, fontWeight: 700, color: 'var(--ink-1)' }}>{k.val}</span>
<span className="label">{k.unit}</span>
</div>
<Sparkline points={k.pts} color={k.color} h={26} />
</div>
))}
</div>
</div>
<div className="demo">
<h3>LineChart multi-séries</h3>
<LineChart h={200} labels={Array.from({length:24},(_,i)=>String(i).padStart(2,'0')+'h')} series={[
{ color: 'var(--accent)', points: [12,18,14,22,28,35,30,42,38,45,52,48,55,60,52,58,45,50,38,44,36,40,32,38] },
{ color: 'var(--info)', points: [8,10,12,14,16,20,18,24,22,28,30,32,30,28,26,24,22,20,18,16,14,12,10,12] },
{ color: 'var(--blue)', points: [4,5,6,8,10,12,13,15,14,16,18,20,18,15,14,13,12,10,9,8,7,6,5,4] },
]} />
<div className="row" style={{ marginTop: 8 }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12 }}><StatusLed status="ok" size={8} style={{ background: 'var(--accent)' }} /> entrant</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12 }}><StatusLed status="ok" size={8} style={{ background: 'var(--info)' }} /> sortant</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12 }}><StatusLed status="ok" size={8} style={{ background: 'var(--blue)' }} /> erreurs</span>
</div>
</div>
</section>
);
}
/* ============================================================
TREENAV
============================================================ */
function TreeNavSection() {
const [active, setActive] = useState('a2');
return (
<section id="tree">
<h2><Icon name="folder" size={22} style={{ color: 'var(--accent)' }} /> TreeNav</h2>
<p className="desc">Navigation arborescente dépliable avec icône en tête, statut LED par enfant.</p>
<div className="demo" style={{ maxWidth: 380 }}>
<TreeNav activeId={active} onSelect={setActive} groups={[
{ id: 'a', icon: 'server', label: 'cluster-prod', count: 3, open: true, children: [
{ id: 'a1', label: 'node-01', status: 'ok', meta: '0.4ms' },
{ id: 'a2', label: 'node-02', status: 'warn', meta: '14ms' },
{ id: 'a3', label: 'node-03', status: 'err', meta: 'down' },
]},
{ id: 'b', icon: 'node', label: 'cluster-edge', count: 2, open: true, children: [
{ id: 'b1', label: 'edge-fr', status: 'ok' },
{ id: 'b2', label: 'edge-de', status: 'err', meta: 'down' },
]},
{ id: 'c', icon: 'disk', label: 'storage', count: 4, open: false, children: [
{ id: 'c1', label: 'sda', status: 'ok' },
]},
]} />
<p style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 12 }}>
Sélection : <span className="mono" style={{ color: 'var(--accent)' }}>{active}</span>
</p>
</div>
</section>
);
}
/* ============================================================
POPUP
============================================================ */
function PopupSection() {
const [open1, setOpen1] = useState(false);
const [open2, setOpen2] = useState(false);
const [open3, setOpen3] = useState(false);
return (
<section id="popup">
<h2><Icon name="close" size={22} style={{ color: 'var(--accent)' }} /> Popup</h2>
<p className="desc">Modale glassmorphism centrée, header avec ✕, footer optionnel. Clic à l'extérieur ou Échap pour fermer.</p>
<div className="demo">
<div className="row">
<Button variant="primary" icon="cog" onClick={() => setOpen1(true)}>Popup simple</Button>
<Button variant="primary" icon="alert" onClick={() => setOpen2(true)}>Confirmation</Button>
<Button variant="ghost" icon="filter" onClick={() => setOpen3(true)}>Avec formulaire</Button>
</div>
</div>
<Popup open={open1} onClose={() => setOpen1(false)} title="Popup simple"
footer={<Button variant="primary" onClick={() => setOpen1(false)}>OK</Button>}>
<div style={{ fontSize: 14, color: 'var(--ink-2)', lineHeight: 1.5 }}>
Une popup glassmorphism centrée avec un titre et un bouton OK.
</div>
</Popup>
<Popup open={open2} onClose={() => setOpen2(false)} title="Confirmer le redémarrage"
footer={<>
<Button variant="ghost" onClick={() => setOpen2(false)}>Annuler</Button>
<Button variant="danger" icon="power" onClick={() => setOpen2(false)}>Redémarrer</Button>
</>}>
<div style={{ fontSize: 14, color: 'var(--ink-2)', lineHeight: 1.5 }}>
Redémarrer <span className="mono" style={{ color: 'var(--accent)' }}>worker_01</span> ?
Le service sera indisponible 8s environ.
</div>
</Popup>
<Popup open={open3} onClose={() => setOpen3(false)} title="Filtres"
footer={<>
<Button variant="ghost" onClick={() => setOpen3(false)}>Annuler</Button>
<Button variant="primary" icon="filter" onClick={() => setOpen3(false)}>Appliquer</Button>
</>}>
<PopupFormDemo />
</Popup>
</section>
);
}
function PopupFormDemo() {
const [thr, setThr] = useState(60);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div className="field">
<span className="field-label">recherche</span>
<input type="text" placeholder="filtrer par nom…" />
</div>
<Toggle on={true} onChange={() => {}} label="activé uniquement" icon="play" />
<div className="field">
<span className="field-label">seuil ({thr}%)</span>
<input type="range" min="0" max="100" value={thr} onChange={(e) => setThr(+e.target.value)} />
</div>
</div>
);
}
/* ============================================================
FORMULAIRES (HTML natif + tokens)
============================================================ */
function FormsSection() {
const [v, setV] = useState(50);
return (
<section id="forms">
<h2><Icon name="list" size={22} style={{ color: 'var(--accent)' }} /> Formulaires</h2>
<p className="desc">Champs HTML natifs habillés par les tokens. Focus accent, fond inset profond.</p>
<div className="demo">
<h3>Champs texte</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
<div className="field">
<span className="field-label">nom utilisateur</span>
<input type="text" defaultValue="root" />
</div>
<div className="field">
<span className="field-label">email</span>
<input type="email" placeholder="admin@exemple.local" />
</div>
<div className="field">
<span className="field-label">mot de passe</span>
<input type="password" defaultValue="••••••••" />
</div>
<div className="field">
<span className="field-label">heure de début</span>
<input type="time" defaultValue="22:00" />
</div>
</div>
</div>
<div className="demo">
<h3>Select</h3>
<div className="field">
<span className="field-label">source de données</span>
<select defaultValue="prom">
<option value="prom">prometheus.local:9090</option>
<option value="node">node-exporter:9100</option>
<option value="file">fichier local /var/log/</option>
</select>
</div>
</div>
<div className="demo">
<h3>Range (slider)</h3>
<div className="field">
<span className="field-label">intervalle de rafraîchissement ({v}s)</span>
<input type="range" min="5" max="120" value={v} onChange={(e) => setV(+e.target.value)} />
</div>
</div>
<div className="demo">
<h3>Textarea (avec police monospace)</h3>
<div className="field">
<span className="field-label">CSS personnalisé</span>
<textarea rows={4} defaultValue=":root { --accent: #fe8019; }"></textarea>
</div>
</div>
</section>
);
}
/* ============================================================
PILLS / BADGES
============================================================ */
function PillsSection() {
return (
<section id="pills">
<h2><Icon name="bell" size={22} style={{ color: 'var(--accent)' }} /> Badges / Pills</h2>
<p className="desc">Pastilles arrondies pour étiqueter un statut. Disponibles en HTML pur via les classes <code className="mono" style={{ color: 'var(--accent)' }}>.pill .pill-ok / -warn / -err / -info</code>.</p>
<div className="demo">
<div className="row">
<span className="pill pill-ok">ok</span>
<span className="pill pill-warn">warn</span>
<span className="pill pill-err">err</span>
<span className="pill pill-info">info</span>
</div>
</div>
<div className="demo">
<h3>Combinaisons usuelles</h3>
<div className="row">
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<span className="mono">nginx</span>
<span className="pill pill-ok">actif</span>
</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<span className="mono">redis</span>
<span className="pill pill-warn">latent</span>
</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<span className="mono">worker_01</span>
<span className="pill pill-err">arrêté</span>
</span>
</div>
</div>
</section>
);
}
/* ============================================================
TERMINAL / LOGS
============================================================ */
function TerminalSection() {
return (
<section id="terminal">
<h2><Icon name="terminal" size={22} style={{ color: 'var(--accent)' }} /> Logs & Terminal</h2>
<p className="desc">Bloc terminal avec police Share Tech Mono, effet CRT, couleurs par niveau de sévérité.</p>
<div className="demo">
<h3>Stream de logs typé</h3>
<div className="term-block">
<div><span className="dim">14:02:04</span> <span className="info">INFO </span> nouveau hôte 10.0.1.236</div>
<div><span className="dim">14:02:09</span> <span className="warn">WARN </span> latence élevée sur 10.0.1.42</div>
<div><span className="dim">14:02:11</span> <span className="info">INFO </span> scan terminé · 1022 IPs</div>
<div><span className="dim">14:01:58</span> <span className="err">ERROR</span> worker_01 arrêté inopinément</div>
<div><span className="dim">14:01:42</span> <span className="ok">OK </span> backup horaire OK (812 Mo)</div>
</div>
</div>
<div className="demo">
<h3>Prompt terminal</h3>
<div className="term-block">
<div><span className="prompt">root@pve-edge-01</span><span className="dim">:~$</span> ps aux</div>
<div className="dim">PID USER CPU% MEM% COMMAND</div>
<div>2341 postgres 14.8 22.1 postgres</div>
<div><span className="warn">3120 node 28.5 18.7 node app.js</span></div>
<div><span className="err">4012 worker 0.0 0.0 worker_01 [Z]</span></div>
<div><span className="prompt">root@pve-edge-01</span><span className="dim">:~$</span> <span style={{
display: 'inline-block', width: 8, height: 14,
background: 'var(--accent)', verticalAlign: 'middle',
animation: 'blink 1s steps(2) infinite',
}}></span></div>
<style>{`@keyframes blink { 50% { opacity: 0 } }`}</style>
</div>
</div>
</section>
);
}
/* ============================================================
APP COMPLÈTE
============================================================ */
function App() {
const [theme, setTheme] = useState('dark');
useEffect(() => { document.documentElement.dataset.theme = theme; }, [theme]);
return (
<>
<Topbar theme={theme} setTheme={setTheme} />
<div className="shell">
<Nav />
<main>
<ColorsSection theme={theme} />
<TypoSection />
<ShadowsSection />
<IconsSection />
<ButtonsSection />
<IconBtnsSection />
<TogglesSection />
<LedsSection />
<TooltipsSection />
<GaugesSection />
<ChartsSection />
<TreeNavSection />
<PopupSection />
<FormsSection />
<PillsSection />
<TerminalSection />
</main>
</div>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>