f9f805cd8b
Spec complète dans docs/superpowers/specs/2026-05-28-inventaire-hdd-design.md : architecture 2 conteneurs Docker (FastAPI + nginx), script Python stdlib only, SQLite avec serial comme clé de vérité, API ingest + dashboard + agents IA. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1016 lines
43 KiB
HTML
1016 lines
43 KiB
HTML
<!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 <Icon name="…" />.</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>
|