chore(design): ajout du package design system smartphone

Contient les tokens, composants et exemples adaptés au mobile,
à utiliser comme référence lors du développement des vues smartphone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 08:53:36 +02:00
co-authored by Claude Sonnet 4.6
parent 4c616fa5d3
commit 4518ed8311
25 changed files with 10222 additions and 0 deletions
@@ -0,0 +1,952 @@
<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<title>Exemple mobile — 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>
html, body {
width: 100%; min-height: 100%;
background:
radial-gradient(ellipse at top, var(--bg-2) 0%, var(--bg-1) 60%);
color: var(--ink-1);
overflow-x: hidden;
}
/* Topbar de la page */
.page-top {
position: sticky; top: 0; z-index: 80;
padding: 14px 20px;
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);
}
.page-top h1 { margin: 0; font-size: 17px; font-weight: 700; }
.page-top 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 : 2 colonnes — phone à gauche, doc à droite */
.layout {
display: grid;
grid-template-columns: 420px 1fr;
gap: 32px;
padding: 32px;
max-width: 1400px;
margin: 0 auto;
align-items: start;
}
@media (max-width: 1000px) {
.layout { grid-template-columns: 1fr; padding: 20px; }
.phone-col { position: relative !important; top: auto !important; margin: 0 auto; }
}
/* Sticky phone */
.phone-col {
position: sticky; top: 80px;
display: flex; flex-direction: column; align-items: center; gap: 14px;
}
/* Mockup smartphone */
.phone {
width: 390px; height: 780px;
background: #0c0907;
border-radius: 48px;
padding: 12px;
box-shadow:
0 0 0 2px #2a2520,
0 0 0 8px #1a1612,
0 20px 50px rgba(0,0,0,0.5);
position: relative;
}
.phone-screen {
width: 100%; height: 100%;
border-radius: 36px;
overflow: hidden;
background: var(--bg-1);
position: relative;
display: flex; flex-direction: column;
}
.phone-notch {
position: absolute; top: 12px; left: 50%;
transform: translateX(-50%);
width: 120px; height: 30px;
background: #0c0907;
border-radius: 18px;
z-index: 100;
pointer-events: none;
}
/* Phone controls */
.phone-controls {
display: flex; gap: 8px; align-items: center;
padding: 10px 14px;
background: var(--bg-3);
border: 1px solid var(--border-2);
border-radius: 999px;
box-shadow: var(--shadow-2);
}
.phone-controls .seg {
display: flex; background: var(--bg-1);
border-radius: 999px;
padding: 3px;
gap: 2px;
}
.phone-controls .seg button {
padding: 4px 10px;
background: transparent;
border: none; border-radius: 999px;
color: var(--ink-3);
cursor: pointer;
font-family: var(--font-mono); font-size: 10px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.phone-controls .seg button.active {
background: var(--accent);
color: var(--bg-1);
box-shadow: 0 2px 6px var(--accent-glow);
}
/* Side doc */
.doc {
min-width: 0;
}
.doc section {
margin-bottom: 36px;
scroll-margin-top: 80px;
}
.doc h2 {
font-size: 22px;
margin: 0 0 4px;
display: flex; align-items: center; gap: 10px;
}
.doc h2 .name {
font-family: var(--font-mono);
color: var(--accent);
font-size: 18px;
}
.doc .desc {
color: var(--ink-3); font-size: 14px;
margin: 4px 0 16px; line-height: 1.55;
}
.doc .card {
padding: 18px;
background: var(--bg-2);
border: 1px solid var(--border-2);
border-radius: 12px;
box-shadow: var(--tile-3d);
margin-bottom: 12px;
}
.doc .pill-name {
display: inline-flex; align-items: center; gap: 6px;
padding: 3px 10px;
background: var(--accent-tint);
border: 1px solid var(--accent);
border-radius: 999px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent);
font-weight: 600;
}
.doc .row-use {
display: grid;
grid-template-columns: 130px 1fr;
gap: 12px;
padding: 8px 0;
border-bottom: 1px dashed var(--border-1);
font-size: 13px;
}
.doc .row-use:last-child { border-bottom: 0; }
.doc .row-use .k {
font-family: var(--font-mono); font-size: 11px;
color: var(--ink-3);
}
.doc .row-use .v { color: var(--ink-2); }
.nav-jump {
position: sticky; top: 80px;
padding: 14px 0;
display: flex; flex-direction: column; gap: 4px;
font-family: var(--font-mono); font-size: 12px;
}
.nav-jump a {
padding: 6px 12px;
color: var(--ink-3);
text-decoration: none;
border-radius: 6px;
border-left: 3px solid transparent;
}
.nav-jump a:hover {
background: var(--bg-3); color: var(--ink-1);
border-left-color: var(--accent);
}
/* Légende — utilisé un peu partout */
.legend {
display: flex; align-items: center; gap: 6px;
font-family: var(--font-mono); font-size: 11px;
color: var(--ink-3);
}
</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="ui-kit.jsx"></script>
<script type="text/babel" src="mobile-kit.jsx"></script>
<script type="text/babel" src="mobile-sheets.jsx"></script>
<script type="text/babel" src="mobile-gestures.jsx"></script>
<script type="text/babel">
const { useState, useEffect } = React;
/* ============================================================
ECRANS DU SMARTPHONE — chacun illustre un cas d'usage
============================================================ */
/* Écran ACCUEIL : ActionCards en grille + FAB */
function PhoneHome({ goto, showToast }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative' }}>
<StatusBar />
<NavBar large title="Accueil" subtitle="jeudi 21 mai · tout est OK" right={
<IconButton icon="bell" label="Notifications" size={34} />
} />
<div style={{ flex: 1, overflowY: 'auto', padding: 16, paddingBottom: 80 }}>
<SearchBar value="" onChange={() => {}} />
<div style={{
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10,
marginTop: 14,
}}>
<ActionCard icon="cpu" iconColor="var(--accent)" title="Système" subtitle="CPU 64%" value="OK" />
<ActionCard icon="network" iconColor="var(--blue)" title="Réseau" subtitle="8.4 Mb/s" value="OK" />
<ActionCard icon="disk" iconColor="var(--ok)" title="Stockage" subtitle="2 disques" value="28%" />
<ActionCard icon="bell" iconColor="var(--warn)" title="Alertes" subtitle="2 nouvelles" badge="2" />
</div>
<div style={{ marginTop: 18 }}>
<div className="label" style={{ marginBottom: 8 }}>Services</div>
<div style={{
background: 'var(--bg-3)',
border: '1px solid var(--border-2)',
borderRadius: 12,
overflow: 'hidden',
boxShadow: 'var(--tile-3d)',
}}>
{[
{ name: 'nginx', status: 'ok', meta: 'actif' },
{ name: 'postgres', status: 'ok', meta: 'actif' },
{ name: 'redis', status: 'warn', meta: 'latent' },
{ name: 'worker_01', status: 'err', meta: 'arrêté' },
].map((s, i, a) => (
<div key={i} style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '12px 14px',
borderBottom: i < a.length - 1 ? '1px solid var(--border-1)' : 'none',
}}>
<StatusLed status={s.status} pulse={s.status !== 'ok'} />
<span className="mono" style={{ flex: 1, fontSize: 14, color: 'var(--ink-1)' }}>{s.name}</span>
<span className="mono" style={{ fontSize: 11, color: s.status === 'err' ? 'var(--err)' : s.status === 'warn' ? 'var(--warn)' : 'var(--ok)' }}>{s.meta}</span>
</div>
))}
</div>
</div>
</div>
<FAB icon="plus" label="Ajouter" onClick={() => showToast('Action FAB')} />
</div>
);
}
/* Écran DASHBOARD : KPIs + jauges */
function PhoneDashboard() {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<NavBar title="Dashboard" />
<div style={{ flex: 1, overflowY: 'auto', padding: 14, paddingBottom: 80 }}>
{/* KPIs compacts */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 14 }}>
<BatteryGauge compact value={64} label="CPU" icon="cpu" warnAt={70} errAt={85} />
<BatteryGauge compact value={42} label="Mémoire" icon="memory" />
<BatteryGauge compact value={28} label="Disque" icon="disk" />
<BatteryGauge compact value={92} label="Réseau" icon="network" warnAt={70} errAt={85} />
</div>
{/* Grande jauge */}
<div style={{
padding: 14, background: 'var(--bg-3)',
border: '1px solid var(--border-2)',
borderRadius: 12, boxShadow: 'var(--tile-3d)',
display: 'flex', flexDirection: 'column', alignItems: 'center',
marginBottom: 14,
}}>
<div className="label" style={{ alignSelf: 'flex-start', marginBottom: 8 }}>Score santé</div>
<BigRadialGauge value={87} label="stable" />
</div>
{/* Graphique */}
<div style={{
padding: 14, background: 'var(--bg-3)',
border: '1px solid var(--border-2)',
borderRadius: 12, boxShadow: 'var(--tile-3d)',
}}>
<div className="label" style={{ marginBottom: 8 }}>Trafic · 24h</div>
<LineChart h={140} labels={[]} 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] },
]} />
</div>
</div>
</div>
);
}
/* Écran RÉGLAGES : ListRow style iOS */
function PhoneSettings({ openSheet, openAlert }) {
const [auto, setAuto] = useState(true);
const [notif, setNotif] = useState(false);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<NavBar large title="Réglages" />
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 0 80px' }}>
<ListSection title="Compte">
<ListRow icon="user" iconColor="var(--blue)" label="Marc" value="admin" onClick={() => {}} />
<ListRow icon="server" iconColor="var(--accent)" label="Instance" value="prod" onClick={() => {}} />
</ListSection>
<ListSection title="Notifications" hint="Choisis quand l'app doit te déranger.">
<ListRow icon="refresh" iconColor="var(--ok)" label="Auto-refresh"
right={<Toggle on={auto} onChange={setAuto} />} />
<ListRow icon="bell" iconColor="var(--purple)" label="Notifications push"
right={<Toggle on={notif} onChange={setNotif} />} />
</ListSection>
<ListSection title="Apparence">
<ListRow icon="moon" iconColor="var(--info)" label="Thème" value="Sombre" onClick={() => openSheet()} />
<ListRow icon="cog" iconColor="var(--ink-3)" label="Densité" value="Confort" onClick={() => {}} />
</ListSection>
<ListSection>
<ListRow icon="download" iconColor="var(--ok)" label="Exporter mes données" onClick={() => {}} />
<ListRow icon="power" iconColor="var(--err)" label="Supprimer mon compte" danger onClick={openAlert} />
</ListSection>
</div>
</div>
);
}
/* Écran GESTES : terrain de test pour chaque geste */
function PhoneGestures({ activeGesture, setActiveGesture }) {
const filter = activeGesture === 'all' ? [] : [activeGesture];
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<NavBar large title="Gestes" subtitle="teste chaque interaction tactile" />
<div style={{ padding: '0 14px 12px' }}>
<SegmentedControl
value={activeGesture}
onChange={setActiveGesture}
options={[
{ value: 'all', label: 'tous' },
{ value: 'tap', label: 'tap', icon: 'play' },
{ value: 'swipe', label: 'swipe', icon: 'chevR' },
{ value: 'pan', label: 'drag', icon: 'grid' },
]} />
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '0 14px 80px' }}>
<GestureZone label="zone tactile · essaie ici" accept={filter} />
<div className="legend" style={{ marginTop: 8, marginBottom: 6 }}> tap · double-tap · long-press · swipe · pan · pinch</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
{GESTURE_CATALOG.map((g) => (
<div key={g.name} style={{
padding: 10,
background: 'var(--bg-3)',
border: '1px solid var(--border-2)',
borderRadius: 8,
boxShadow: 'var(--shadow-1)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<Icon name={g.icon} size={12} style={{ color: 'var(--accent)' }} />
<span className="mono" style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink-1)' }}>{g.name}</span>
</div>
<div style={{ fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.3 }}>{g.desc}</div>
</div>
))}
</div>
</div>
</div>
);
}
/* ============================================================
APP COMPLÈTE DU PHONE — navigation par TabBar
============================================================ */
function PhoneApp({ theme }) {
const [tab, setTab] = useState('home');
const [sheet, setSheet] = useState(false);
const [alert, setAlert] = useState(false);
const [action, setAction] = useState(false);
const [toast, setToast] = useState(null);
const [activeGesture, setActiveGesture] = useState('all');
const [themeChoice, setThemeChoice] = useState('dark');
const showToast = (msg) => setToast(msg);
return (
<div data-theme={theme} style={{
width: '100%', height: '100%',
display: 'flex', flexDirection: 'column',
background: 'var(--bg-1)',
color: 'var(--ink-1)',
position: 'relative',
overflow: 'hidden',
}}>
<div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
{tab === 'home' && <PhoneHome showToast={showToast} />}
{tab === 'dash' && <PhoneDashboard />}
{tab === 'gestures' && <PhoneGestures activeGesture={activeGesture} setActiveGesture={setActiveGesture} />}
{tab === 'settings' && <PhoneSettings openSheet={() => setSheet(true)} openAlert={() => setAlert(true)} />}
</div>
<TabBar
active={tab}
onSelect={setTab}
items={[
{ id: 'home', icon: 'grid', label: 'accueil' },
{ id: 'dash', icon: 'chart', label: 'dashboard' },
{ id: 'gestures', icon: 'play', label: 'gestes' },
{ id: 'settings', icon: 'cog', label: 'réglages' },
]}
/>
{/* BottomSheet : choix du thème (depuis Réglages > Thème) */}
<BottomSheet open={sheet} onClose={() => setSheet(false)} title="Choisir le thème">
<SegmentedControl
value={themeChoice}
onChange={setThemeChoice}
options={[
{ value: 'dark', label: 'Sombre', icon: 'moon' },
{ value: 'light', label: 'Clair', icon: 'sun' },
{ value: 'auto', label: 'Auto', icon: 'clock' },
]} />
<div style={{ fontSize: 13, color: 'var(--ink-3)', marginTop: 14, lineHeight: 1.5 }}>
Le thème "Auto" suit automatiquement les réglages de ton téléphone (jour/nuit).
</div>
</BottomSheet>
{/* AlertDialog : confirmation destructive */}
<AlertDialog
open={alert} onClose={() => setAlert(false)}
icon="alert" iconColor="var(--err)"
title="Supprimer le compte ?"
message="Cette action est irréversible. Toutes tes données seront perdues."
actions={[
{ label: 'Annuler' },
{ label: 'Supprimer', danger: true, primary: true, onClick: () => showToast('Compte supprimé') },
]} />
{/* ActionSheet : ouverte depuis le FAB de l'accueil */}
<ActionSheet
open={action} onClose={() => setAction(false)}
title="Que veux-tu faire ?"
actions={[
{ label: 'Lancer un scan', icon: 'refresh', onClick: () => showToast('Scan lancé') },
{ label: 'Nouveau dashboard', icon: 'plus', onClick: () => showToast('Dashboard créé') },
{ label: 'Importer données', icon: 'download' },
{ label: 'Supprimer tout', icon: 'power', danger: true },
]} />
{/* Toast */}
<Toast open={toast !== null} onClose={() => setToast(null)} message={toast} variant="ok" />
</div>
);
}
/* ============================================================
PAGE DOC à droite — catalogue avec noms en clair
============================================================ */
function Doc({ currentScreen }) {
return (
<div className="doc">
{/* INTRO */}
<section>
<h2><Icon name="memory" size={22} style={{ color: 'var(--accent)' }} /> Variante mobile</h2>
<p className="desc">
Adaptation smartphone de mon design system (Gruvbox seventies).
<strong> Chaque composant a un nom explicite</strong> que tu peux utiliser pour
le demander à ton agent IA ou à un développeur. Hit targets 44px,
animations fluides, dark + light, optimisé iOS / Android.
</p>
<div className="card">
<div className="row-use"><span className="k">Largeur réf.</span><span className="v">390 px (iPhone 14, Galaxy S22)</span></div>
<div className="row-use"><span className="k">Hit target min.</span><span className="v">44 × 44 px (recommandation Apple/Google)</span></div>
<div className="row-use"><span className="k">Navigation</span><span className="v">TabBar en bas (3-5 sections)</span></div>
<div className="row-use"><span className="k">Action principale</span><span className="v">FAB bottom-right (Material) ou bouton plein largeur (iOS)</span></div>
<div className="row-use"><span className="k">Modales</span><span className="v">BottomSheet (priorité) · ActionSheet · AlertDialog</span></div>
</div>
</section>
{/* COMPOSANTS PHARES */}
<section id="components">
<h2><Icon name="grid" size={22} style={{ color: 'var(--accent)' }} /> Composants nommés</h2>
<p className="desc">Vois-les en vrai dans le téléphone à gauche. Le nom est ce que tu emploies dans le code.</p>
<NamedComp name="StatusBar" desc="Barre de statut iOS-like en haut de l'écran (heure, signal, batterie). Purement décorative." location="Tous les écrans" />
<NamedComp name="NavBar" desc="Barre de titre. Variante large pour écran d'accueil, ou compacte avec bouton retour pour écran enfant." location="Tous les écrans" />
<NamedComp name="TabBar" desc="Barre d'onglets en bas, 3-5 sections principales de l'app. C'est ta navigation primaire." location="Toujours visible" />
<NamedComp name="ActionCard" desc="Grande tuile tactile avec icône colorée + titre + valeur. Idéale en grille 2 colonnes pour un dashboard d'accueil." location="Accueil" />
<NamedComp name="ListSection / ListRow" desc="Liste de réglages style iOS. ListRow = une ligne (icône + label + valeur + chevron). Toute ligne fait ≥ 52px." location="Réglages" />
<NamedComp name="PrimaryButton" desc="Gros bouton 52px plein largeur. Variante primary, ghost, danger. Pour l'action principale d'un écran." location="Réglages > formulaires" />
<NamedComp name="SegmentedControl" desc="Sélecteur segmenté pour 2-4 options exclusives (jamais plus, sinon utilise un Select)." location="Gestes (filtre) · BottomSheet (choix thème)" />
<NamedComp name="SearchBar" desc="Champ de recherche avec icône loupe et bouton effacer. Padding tactile généreux." location="Accueil" />
<NamedComp name="FAB" desc="Floating Action Button. Toujours en bas à droite. Une seule action principale par écran. Style Android Material." location="Accueil" />
</section>
{/* FENÊTRES / DIALOGUES */}
<section id="windows">
<h2><Icon name="list" size={22} style={{ color: 'var(--accent)' }} /> Types de fenêtres</h2>
<p className="desc">Sur mobile, on évite les modales centrées. Voici les 4 types à utiliser à la place, chacun avec son cas.</p>
<WindowType
name="BottomSheet"
when="Action contextuelle, formulaire court, choix dans une liste."
why="Accessible au pouce, geste swipe down pour fermer, sensation native."
gesture="SwipeDown ↓ pour fermer · drag du handle en haut"
example="Sur ce smartphone : Réglages > Thème → ouvre une BottomSheet"
/>
<WindowType
name="ActionSheet"
when="Choix parmi 2-6 actions sur un élément (équiv. menu contextuel desktop)."
why="Style iOS natif, l'utilisateur sait que c'est une liste d'options."
gesture="Tap sur une option · Tap hors zone ou bouton Annuler pour fermer"
example="Tape le FAB orange sur l'accueil"
/>
<WindowType
name="AlertDialog"
when="Message critique, demande de confirmation ferme (suppression, déconnexion)."
why="Centré, bloque l'attention. À utiliser avec parcimonie."
gesture="Tap sur Annuler / Confirmer (pas de swipe pour fermer — c'est volontairement bloquant)"
example="Réglages > Supprimer mon compte"
/>
<WindowType
name="Toast"
when="Feedback éphémère après une action (succès, erreur)."
why="Non bloquant, disparaît seul après 2.5s."
gesture="Aucun — disparaît automatiquement"
example="Toute action ci-dessus déclenche un Toast en haut"
/>
</section>
{/* GESTES */}
<section id="gestures">
<h2><Icon name="play" size={22} style={{ color: 'var(--accent)' }} /> Gestes tactiles</h2>
<p className="desc">
Onglet <strong>Gestes</strong> en bas du smartphone zone interactive pour tester
chaque geste. Le nom du geste s'affiche en temps réel.
</p>
<div style={{
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10,
marginBottom: 16,
}}>
{GESTURE_CATALOG.map((g) => (
<div key={g.name} className="card" style={{ padding: 14, margin: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<Icon name={g.icon} size={14} style={{ color: 'var(--accent)' }} />
<span className="mono" style={{ fontSize: 13, fontWeight: 700, color: 'var(--accent)' }}>{g.name}</span>
</div>
<GestureAnim name={g.name} />
<div style={{ fontSize: 13, color: 'var(--ink-2)', marginTop: 8 }}>{g.desc}</div>
<div style={{ fontSize: 11, color: 'var(--ink-3)', fontStyle: 'italic', marginTop: 2 }}>
Usage : {g.usage}
</div>
</div>
))}
</div>
<div className="card">
<h3 style={{
margin: '0 0 8px', fontFamily: 'var(--font-mono)',
fontSize: 11, letterSpacing: '0.08em', textTransform: 'uppercase',
color: 'var(--ink-3)',
}}>Utilitaire</h3>
<div className="row-use">
<span className="k">useGesture()</span>
<span className="v">Hook React qui transforme un élément en zone tactile. Pose les handlers <code className="mono" style={{ color:'var(--accent)' }}>onTap / onSwipeLeft / onLongPress / onPinch</code> etc.</span>
</div>
<div className="row-use">
<span className="k">GestureZone</span>
<span className="v">Composant prêt-à-l'emploi qui affiche le geste détecté + un journal des 5 derniers. Utilisé dans l'onglet Gestes.</span>
</div>
</div>
</section>
{/* INSTALLATION */}
<section id="install">
<h2><Icon name="download" size={22} style={{ color: 'var(--accent)' }} /> Comment utiliser</h2>
<p className="desc">
Ajoute ces lignes en plus de <code className="mono" style={{ color:'var(--accent)' }}>ui-kit.jsx</code> :
</p>
<div className="card" style={{ background:'#15110c', padding: 16 }}>
<pre className="mono" style={{
margin: 0, fontSize: 12, lineHeight: 1.6, color: 'var(--ink-2)',
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
}}>
{`<scr` + `ipt type="text/babel" src="components/ui-kit.jsx"></scr` + `ipt>
<scr` + `ipt type="text/babel" src="components/mobile-kit.jsx"></scr` + `ipt>
<scr` + `ipt type="text/babel" src="components/mobile-sheets.jsx"></scr` + `ipt>
<scr` + `ipt type="text/babel" src="components/mobile-gestures.jsx"></scr` + `ipt>`}
</pre>
</div>
<p className="desc" style={{ marginTop: 16 }}>
Tu retrouves ensuite dans <code className="mono" style={{ color:'var(--accent)' }}>window</code> tous les composants exposés :
<strong> StatusBar, NavBar, TabBar, ListRow, ListSection, ActionCard, PrimaryButton, SegmentedControl, SearchBar,
BottomSheet, ActionSheet, AlertDialog, Toast, FAB, PullToRefresh, useGesture, GestureZone</strong>.
</p>
</section>
</div>
);
}
function NamedComp({ name, desc, location }) {
return (
<div className="card">
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8, flexWrap: 'wrap' }}>
<span className="pill-name">&lt;{name}/&gt;</span>
{location && <span className="legend">📍 {location}</span>}
</div>
<div style={{ fontSize: 13.5, color: 'var(--ink-2)', lineHeight: 1.5 }}>{desc}</div>
<div style={{
marginTop: 12, padding: 12,
background: 'var(--bg-1)',
border: '1px dashed var(--border-2)',
borderRadius: 8,
display: 'flex', alignItems: 'center', justifyContent: 'center',
minHeight: 72,
}}>
<ComponentPreview name={name} />
</div>
</div>
);
}
/* ============================================================
ComponentPreview — mini-rendu live de chaque composant nommé
============================================================ */
function ComponentPreview({ name }) {
// Réduit la taille via un wrapper compact
const wrap = (children, w = '100%') => (
<div style={{ width: w, maxWidth: 320 }}>{children}</div>
);
if (name === 'StatusBar') return wrap(<div style={{ background: 'var(--bg-2)', border: '1px solid var(--border-2)', borderRadius: 8, overflow: 'hidden' }}><StatusBar /></div>);
if (name === 'NavBar') return wrap(<div style={{ background: 'var(--bg-2)', border: '1px solid var(--border-2)', borderRadius: 8, overflow: 'hidden' }}><NavBar title="Mon écran" /></div>);
if (name === 'TabBar') return wrap(<div style={{ background: 'var(--bg-2)', border: '1px solid var(--border-2)', borderRadius: 8, overflow: 'hidden' }}><TabBar active="a" onSelect={() => {}} items={[
{ id: 'a', icon: 'grid', label: 'accueil' },
{ id: 'b', icon: 'chart', label: 'stats' },
{ id: 'c', icon: 'cog', label: 'réglages' },
]} /></div>);
if (name === 'ActionCard') return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, width: '100%', maxWidth: 280 }}>
<ActionCard icon="cpu" iconColor="var(--accent)" title="Système" subtitle="CPU 64%" value="OK" />
<ActionCard icon="bell" iconColor="var(--warn)" title="Alertes" subtitle="2 nouvelles" badge="2" />
</div>
);
if (name === 'ListSection / ListRow') return wrap(
<ListSection title="Notifications">
<ListRow icon="refresh" iconColor="var(--ok)" label="Auto-refresh" right={<Toggle on={true} onChange={() => {}} />} />
<ListRow icon="bell" iconColor="var(--purple)" label="Push" right={<Toggle on={false} onChange={() => {}} />} />
</ListSection>
);
if (name === 'PrimaryButton') return wrap(
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<PrimaryButton icon="download">Enregistrer</PrimaryButton>
<PrimaryButton variant="ghost">Annuler</PrimaryButton>
</div>
);
if (name === 'SegmentedControl') return wrap(
<SegmentedControl value="a" onChange={() => {}} options={[
{ value: 'a', label: 'Sombre', icon: 'moon' },
{ value: 'b', label: 'Clair', icon: 'sun' },
{ value: 'c', label: 'Auto', icon: 'clock' },
]} />
);
if (name === 'SearchBar') return wrap(<SearchBar value="" onChange={() => {}} placeholder="rechercher…" />);
if (name === 'FAB') return (
<div style={{ position: 'relative', width: 220, height: 90, background: 'var(--bg-2)', border: '1px solid var(--border-2)', borderRadius: 8, overflow: 'hidden' }}>
<div style={{ position: 'absolute', inset: 0, padding: 10, color: 'var(--ink-4)', fontSize: 11, fontFamily: 'var(--font-mono)' }}>écran…</div>
<div style={{ position: 'absolute', bottom: 10, right: 10 }}>
<button className="touch-press" style={{
width: 48, height: 48, borderRadius: '50%',
background: 'var(--accent)', color: 'var(--bg-1)',
border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 6px 14px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.25)',
}}><Icon name="plus" size={20} /></button>
</div>
</div>
);
return null;
}
function WindowType({ name, when, why, gesture, example }) {
return (
<div className="card">
<div style={{ display: 'grid', gridTemplateColumns: '90px 1fr', gap: 14, alignItems: 'flex-start' }}>
<WindowVisual type={name} />
<div style={{ minWidth: 0 }}>
<span className="pill-name" style={{ marginBottom: 10, display: 'inline-flex' }}>&lt;{name}/&gt;</span>
<div className="row-use"><span className="k">Quand</span><span className="v">{when}</span></div>
<div className="row-use"><span className="k">Pourquoi</span><span className="v">{why}</span></div>
<div className="row-use"><span className="k">Gestes</span><span className="v">{gesture}</span></div>
<div className="row-use"><span className="k">Tester</span><span className="v" style={{ color:'var(--accent)' }}>{example}</span></div>
</div>
</div>
</div>
);
}
/* ============================================================
WindowVisual — mini SVG phone + zone modale colorée
============================================================ */
function WindowVisual({ type }) {
const phone = (inner) => (
<svg viewBox="0 0 100 180" width="80" height="144" style={{ display: 'block', margin: '0 auto' }}>
{/* Cadre téléphone */}
<rect x="3" y="2" width="94" height="176" rx="14" fill="var(--bg-3)" stroke="var(--border-3)" strokeWidth="1.5"/>
<rect x="38" y="6" width="24" height="3" rx="1.5" fill="var(--ink-4)"/>
{/* indication contenu */}
<rect x="10" y="18" width="50" height="3" rx="1.5" fill="var(--ink-4)" opacity="0.5"/>
<rect x="10" y="26" width="60" height="2" rx="1" fill="var(--ink-4)" opacity="0.3"/>
<rect x="10" y="32" width="40" height="2" rx="1" fill="var(--ink-4)" opacity="0.3"/>
{inner}
</svg>
);
if (type === 'BottomSheet') return phone(
<g>
<rect x="6" y="108" width="88" height="68" rx="8" fill="var(--accent)" opacity="0.92"/>
<rect x="44" y="114" width="12" height="2.5" rx="1.25" fill="var(--bg-1)"/>
<path d="M 50 145 v 14 M 46 155 l 4 5 l 4 -5" stroke="var(--bg-1)" strokeWidth="1.5" fill="none" opacity="0.7"/>
</g>
);
if (type === 'ActionSheet') return phone(
<g>
<rect x="6" y="108" width="88" height="50" rx="6" fill="var(--accent)" opacity="0.85"/>
<line x1="10" y1="122" x2="90" y2="122" stroke="var(--bg-1)" strokeWidth="0.5" opacity="0.4"/>
<line x1="10" y1="135" x2="90" y2="135" stroke="var(--bg-1)" strokeWidth="0.5" opacity="0.4"/>
<line x1="10" y1="148" x2="90" y2="148" stroke="var(--bg-1)" strokeWidth="0.5" opacity="0.4"/>
<rect x="6" y="162" width="88" height="14" rx="6" fill="var(--bg-1)" stroke="var(--accent)" strokeWidth="1"/>
<text x="50" y="172" textAnchor="middle" fill="var(--accent)" fontSize="7" fontFamily="Inter" fontWeight="700">Annuler</text>
</g>
);
if (type === 'AlertDialog') return phone(
<g>
<rect x="0" y="0" width="100" height="180" fill="#000" opacity="0.45"/>
<rect x="3" y="2" width="94" height="176" rx="14" fill="none" stroke="var(--border-3)" strokeWidth="1.5"/>
<rect x="38" y="6" width="24" height="3" rx="1.5" fill="var(--ink-4)"/>
<rect x="16" y="66" width="68" height="56" rx="8" fill="var(--err)" opacity="0.92"/>
<circle cx="50" cy="82" r="6" fill="var(--bg-1)" opacity="0.95"/>
<line x1="30" y1="96" x2="70" y2="96" stroke="var(--bg-1)" strokeWidth="1.4" opacity="0.85"/>
<line x1="36" y1="102" x2="64" y2="102" stroke="var(--bg-1)" strokeWidth="1" opacity="0.6"/>
<line x1="16" y1="112" x2="84" y2="112" stroke="var(--bg-1)" strokeWidth="0.5" opacity="0.4"/>
<line x1="50" y1="112" x2="50" y2="122" stroke="var(--bg-1)" strokeWidth="0.5" opacity="0.4"/>
</g>
);
if (type === 'Toast') return phone(
<g>
<rect x="8" y="18" width="84" height="14" rx="7" fill="var(--ok)" opacity="0.95"/>
<circle cx="16" cy="25" r="2.5" fill="var(--bg-1)"/>
<line x1="22" y1="25" x2="80" y2="25" stroke="var(--bg-1)" strokeWidth="1.5" opacity="0.7"/>
</g>
);
return phone(null);
}
/* ============================================================
GestureAnim — animation SVG par geste
============================================================ */
function GestureAnim({ name }) {
const sty = {
width: '100%', height: 80,
background: 'var(--bg-1)',
border: '1px dashed var(--border-2)',
borderRadius: 8,
};
const dot = (cx, cy, r = 6) => <circle cx={cx} cy={cy} r={r} fill="var(--accent)" />;
const trail = (path) => (
<path d={path} stroke="var(--ink-4)" strokeWidth="0.8" strokeDasharray="3 3" fill="none" />
);
const arrow = (x, y, dir) => {
const v = { l: 'l 5 -4 m -5 4 l 5 4', r: 'l -5 -4 m 5 4 l -5 4', u: 'l -4 5 m 4 -5 l 4 5', d: 'l -4 -5 m 4 5 l 4 -5' }[dir];
return <path d={`M ${x} ${y} ${v}`} stroke="var(--ink-3)" strokeWidth="1.2" fill="none" strokeLinecap="round"/>;
};
if (name === 'Tap') return (
<svg viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet" style={sty}>
<circle cx="50" cy="30" r="6" fill="none" stroke="var(--accent)" strokeWidth="2">
<animate attributeName="r" values="6;22;6" dur="1.6s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.9;0;0.9" dur="1.6s" repeatCount="indefinite"/>
</circle>
{dot(50, 30)}
</svg>
);
if (name === 'DoubleTap') return (
<svg viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet" style={sty}>
<circle cx="50" cy="30" r="6" fill="none" stroke="var(--accent)" strokeWidth="2">
<animate attributeName="r" values="6;14;6;14;6" keyTimes="0;0.15;0.3;0.45;1" dur="1.8s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.9;0;0.9;0;0.9" keyTimes="0;0.15;0.3;0.45;1" dur="1.8s" repeatCount="indefinite"/>
</circle>
{dot(50, 30)}
</svg>
);
if (name === 'LongPress') return (
<svg viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet" style={sty}>
<circle cx="50" cy="30" r="6" fill="none" stroke="var(--accent)" strokeWidth="2">
<animate attributeName="r" values="6;24" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;0" dur="2s" repeatCount="indefinite"/>
</circle>
{dot(50, 30)}
<text x="50" y="54" textAnchor="middle" fontSize="7" fontFamily="JetBrains Mono" fill="var(--ink-3)">500ms</text>
</svg>
);
if (name === 'SwipeLeft') return (
<svg viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet" style={sty}>
{trail('M 78 30 L 22 30')}
{arrow(22, 30, 'l')}
<circle r="6" fill="var(--accent)">
<animate attributeName="cx" values="78;22" dur="1.6s" repeatCount="indefinite"/>
<animate attributeName="cy" values="30;30" dur="1.6s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.1;0.85;1" dur="1.6s" repeatCount="indefinite"/>
</circle>
</svg>
);
if (name === 'SwipeRight') return (
<svg viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet" style={sty}>
{trail('M 22 30 L 78 30')}
{arrow(78, 30, 'r')}
<circle r="6" fill="var(--accent)">
<animate attributeName="cx" values="22;78" dur="1.6s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.1;0.85;1" dur="1.6s" repeatCount="indefinite"/>
</circle>
</svg>
);
if (name === 'SwipeUp') return (
<svg viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet" style={sty}>
{trail('M 50 52 L 50 10')}
{arrow(50, 10, 'u')}
<circle r="6" fill="var(--accent)" cx="50">
<animate attributeName="cy" values="52;10" dur="1.6s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.1;0.85;1" dur="1.6s" repeatCount="indefinite"/>
</circle>
</svg>
);
if (name === 'SwipeDown') return (
<svg viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet" style={sty}>
{trail('M 50 10 L 50 52')}
{arrow(50, 52, 'd')}
<circle r="6" fill="var(--accent)" cx="50">
<animate attributeName="cy" values="10;52" dur="1.6s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.1;0.85;1" dur="1.6s" repeatCount="indefinite"/>
</circle>
</svg>
);
if (name === 'Pan') return (
<svg viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet" style={sty}>
<path d="M 20 45 Q 35 8 50 30 T 80 18" stroke="var(--ink-4)" strokeWidth="0.8" strokeDasharray="3 3" fill="none"/>
<circle r="6" fill="var(--accent)">
<animateMotion dur="2s" repeatCount="indefinite" path="M 20 45 Q 35 8 50 30 T 80 18"/>
</circle>
</svg>
);
if (name === 'Pinch') return (
<svg viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet" style={sty}>
<circle r="5" fill="var(--accent)" cy="30">
<animate attributeName="cx" values="30;46;30" dur="1.8s" repeatCount="indefinite"/>
</circle>
<circle r="5" fill="var(--accent)" cy="30">
<animate attributeName="cx" values="70;54;70" dur="1.8s" repeatCount="indefinite"/>
</circle>
<line y1="30" y2="30" stroke="var(--ink-4)" strokeWidth="0.8" strokeDasharray="3 3">
<animate attributeName="x1" values="30;46;30" dur="1.8s" repeatCount="indefinite"/>
<animate attributeName="x2" values="70;54;70" dur="1.8s" repeatCount="indefinite"/>
</line>
</svg>
);
return null;
}
/* ============================================================
ROOT
============================================================ */
function App() {
const [theme, setTheme] = useState('dark');
const [device, setDevice] = useState('ios');
useEffect(() => { document.documentElement.dataset.theme = theme; }, [theme]);
return (
<>
<header className="page-top">
<div style={{
width: 32, height: 32, borderRadius: 8,
background: 'var(--accent)', color: 'var(--bg-1)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.3), 0 2px 6px var(--accent-glow)',
}}>
<Icon name="memory" size={16} />
</div>
<h1>Exemple mobile <small>composants nommés · gestes testables · v1.0</small></h1>
<span style={{ flex: 1 }}></span>
<a href="exemple-tout.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 desktop
</a>
</header>
<div className="layout">
<div className="phone-col">
<div className="phone-controls">
<div className="seg">
<button className={device === 'ios' ? 'active' : ''} onClick={() => setDevice('ios')}>iOS</button>
<button className={device === 'android' ? 'active' : ''} onClick={() => setDevice('android')}>Android</button>
</div>
<div className="seg">
<button className={theme === 'dark' ? 'active' : ''} onClick={() => setTheme('dark')}>Sombre</button>
<button className={theme === 'light' ? 'active' : ''} onClick={() => setTheme('light')}>Clair</button>
</div>
</div>
<div className="phone" style={device === 'android' ? { borderRadius: 28 } : {}}>
{device === 'ios' && <div className="phone-notch"></div>}
<div className="phone-screen" style={device === 'android' ? { borderRadius: 18 } : {}}>
<PhoneApp theme={theme} />
</div>
</div>
<div className="legend">↑ utilise le smartphone comme un vrai téléphone</div>
</div>
<Doc />
</div>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>