4518ed8311
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>
953 lines
43 KiB
HTML
953 lines
43 KiB
HTML
<!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"><{name}/></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' }}><{name}/></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>
|