Files
home_hub/design_system/package-smartphone/examples/exemple-mobile.html
T
gilles 4518ed8311 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>
2026-05-30 08:53:36 +02:00

953 lines
43 KiB
HTML
Raw 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 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>