Files
home_hub/design_system/package-smartphone/examples/exemple-mobile-saisie-doc.jsx
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

487 lines
26 KiB
React

/* ============================================================
exemple-mobile-saisie-doc.jsx — partie 2
Doc panneau droit (catalogue commenté avec visuels) + ROOT.
============================================================ */
const { useState: uDS, useEffect: eDS } = React;
/* ============================================================
VISUALS ============================================================ */
/* Mini-clavier virtuel selon le type */
function KeyboardVisual({ kind }) {
const wrap = (cells) => (
<div style={{
padding: 10, background: 'var(--bg-1)',
border: '1px solid var(--border-2)', borderRadius: 8,
display: 'flex', flexDirection: 'column', gap: 4,
width: '100%',
}}>{cells.map((c, i) => <React.Fragment key={i}>{c}</React.Fragment>)}</div>
);
const row = (keys, big) => (
<div style={{ display: 'flex', gap: 3, justifyContent: 'center' }}>
{keys.map((k, i) => (
<span key={i} style={{
flex: big ? 1 : '0 1 auto',
minWidth: big ? 0 : 16, height: 22, padding: '0 4px',
background: k === '↵' || k === 'Rechercher' || k === 'Aller' || k === 'OK' ? 'var(--accent)' : 'var(--bg-3)',
color: k === '↵' || k === 'Rechercher' || k === 'Aller' || k === 'OK' ? 'var(--bg-1)' : 'var(--ink-1)',
border: '1px solid var(--border-2)',
borderRadius: 4,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
}}>{k}</span>
))}
</div>
);
if (kind === 'text') return wrap([row(['q','w','e','r','t','y','u','i','o','p']), row(['a','s','d','f','g','h','j','k','l']), row(['z','x','c','v','b','n','m','⌫']), row(['123','espace','↵'], true)]);
if (kind === 'numeric') return wrap([row(['1','2','3'], true), row(['4','5','6'], true), row(['7','8','9'], true), row(['','0','⌫'], true)]);
if (kind === 'decimal') return wrap([row(['1','2','3'], true), row(['4','5','6'], true), row(['7','8','9'], true), row([',','0','⌫'], true)]);
if (kind === 'tel') return wrap([row(['1','2 ABC','3 DEF'], true), row(['4 GHI','5 JKL','6 MNO'], true), row(['7 PQRS','8 TUV','9 WXYZ'], true), row(['+ * #','0 +','⌫'], true)]);
if (kind === 'email') return wrap([row(['q','w','e','r','t','y','u','i','o','p']), row(['a','s','d','f','g','h','j','k','l']), row(['z','x','c','v','b','n','m','⌫']), row(['123','@','espace','.','↵'], true)]);
if (kind === 'url') return wrap([row(['q','w','e','r','t','y','u','i','o','p']), row(['a','s','d','f','g','h','j','k','l']), row(['z','x','c','v','b','n','m','⌫']), row(['123','.','/','.com','Aller'], true)]);
if (kind === 'search') return wrap([row(['q','w','e','r','t','y','u','i','o','p']), row(['a','s','d','f','g','h','j','k','l']), row(['z','x','c','v','b','n','m','⌫']), row(['123','espace','Rechercher'], true)]);
if (kind === 'none') return (
<div style={{
padding: 14, background: 'var(--bg-1)',
border: '1px dashed var(--border-3)', borderRadius: 8,
textAlign: 'center', color: 'var(--ink-4)',
fontFamily: 'var(--font-mono)', fontSize: 11,
}}>(aucun clavier picker custom)</div>
);
return null;
}
/* Mini SVG phone pour montrer les écrans */
function ScreenVisual({ type }) {
const phone = (inner) => (
<svg viewBox="0 0 100 180" width="80" height="144" style={{ display: 'block' }}>
<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)"/>
{inner}
</svg>
);
if (type === 'login') return phone(
<g>
<circle cx="50" cy="40" r="12" fill="var(--accent)"/>
<rect x="20" y="68" width="60" height="9" rx="4" fill="var(--bg-1)" stroke="var(--border-2)"/>
<rect x="20" y="82" width="60" height="9" rx="4" fill="var(--bg-1)" stroke="var(--border-2)"/>
<rect x="20" y="100" width="60" height="11" rx="5" fill="var(--accent)"/>
<line x1="22" y1="125" x2="42" y2="125" stroke="var(--ink-4)" strokeWidth="0.5"/>
<line x1="58" y1="125" x2="78" y2="125" stroke="var(--ink-4)" strokeWidth="0.5"/>
<text x="50" y="128" textAnchor="middle" fontSize="6" fontFamily="JetBrains Mono" fill="var(--ink-4)">OU</text>
<circle cx="42" cy="145" r="6" fill="none" stroke="var(--accent)" strokeWidth="1"/>
<circle cx="58" cy="145" r="6" fill="none" stroke="var(--accent)" strokeWidth="1"/>
</g>
);
if (type === 'profile') return phone(
<g>
<rect x="3" y="14" width="94" height="14" fill="var(--bg-2)"/>
<text x="50" y="24" textAnchor="middle" fontSize="6" fontFamily="Inter" fontWeight="700" fill="var(--ink-1)">Profil</text>
<rect x="80" y="17" width="9" height="9" rx="2" fill="var(--accent-tint)" stroke="var(--accent)" strokeWidth="0.5"/>
<circle cx="50" cy="48" r="12" fill="var(--accent)"/>
<rect x="30" y="65" width="40" height="5" rx="2" fill="var(--ink-2)"/>
<rect x="36" y="74" width="28" height="3" rx="1.5" fill="var(--ink-4)"/>
<rect x="10" y="92" width="80" height="14" rx="4" fill="var(--bg-2)"/>
<rect x="10" y="110" width="80" height="14" rx="4" fill="var(--bg-2)"/>
<rect x="10" y="128" width="80" height="14" rx="4" fill="var(--bg-2)"/>
</g>
);
if (type === 'form') return phone(
<g>
<rect x="10" y="22" width="80" height="9" rx="3" fill="var(--bg-2)" stroke="var(--border-2)"/>
<rect x="10" y="36" width="38" height="9" rx="3" fill="var(--bg-2)" stroke="var(--border-2)"/>
<rect x="52" y="36" width="38" height="9" rx="3" fill="var(--bg-2)" stroke="var(--border-2)"/>
<rect x="10" y="50" width="80" height="22" rx="3" fill="var(--bg-2)" stroke="var(--border-2)"/>
<rect x="10" y="78" width="80" height="9" rx="3" fill="var(--bg-2)" stroke="var(--border-2)"/>
<circle cx="16" cy="98" r="2.5" fill="none" stroke="var(--accent)"/>
<rect x="22" y="96" width="40" height="2.5" rx="1" fill="var(--ink-3)"/>
<circle cx="16" cy="106" r="2.5" fill="var(--accent)"/>
<rect x="22" y="104" width="40" height="2.5" rx="1" fill="var(--ink-3)"/>
<rect x="10" y="120" width="24" height="14" rx="3" fill="var(--bg-2)" stroke="var(--accent)"/>
<rect x="38" y="120" width="24" height="14" rx="3" fill="var(--bg-2)" stroke="var(--border-2)"/>
<rect x="66" y="120" width="24" height="14" rx="3" fill="var(--bg-2)" stroke="var(--border-2)"/>
<rect x="10" y="158" width="80" height="12" rx="5" fill="var(--accent)"/>
</g>
);
if (type === 'swipe') return phone(
<g>
<rect x="3" y="14" width="94" height="14" fill="var(--bg-2)"/>
<text x="50" y="24" textAnchor="middle" fontSize="6" fontFamily="Inter" fontWeight="700" fill="var(--ink-1)">Boîte</text>
<rect x="3" y="32" width="94" height="20" fill="var(--bg-3)"/>
<line x1="3" y1="52" x2="97" y2="52" stroke="var(--border-1)" strokeWidth="0.4"/>
<rect x="3" y="52" width="94" height="20" fill="var(--bg-3)"/>
<line x1="3" y1="72" x2="97" y2="72" stroke="var(--border-1)" strokeWidth="0.4"/>
<g transform="translate(-26, 0)">
<rect x="3" y="72" width="94" height="20" fill="var(--bg-3)"/>
</g>
<rect x="71" y="72" width="13" height="20" fill="var(--info)"/>
<rect x="84" y="72" width="13" height="20" fill="var(--accent)"/>
<text x="77.5" y="85" textAnchor="middle" fontSize="5" fontFamily="Inter" fontWeight="700" fill="var(--bg-1)">Lu</text>
<text x="90.5" y="85" textAnchor="middle" fontSize="5" fontFamily="Inter" fontWeight="700" fill="var(--bg-1)">Pin</text>
<rect x="3" y="92" width="94" height="20" fill="var(--bg-3)"/>
<line x1="3" y1="112" x2="97" y2="112" stroke="var(--border-1)" strokeWidth="0.4"/>
<rect x="3" y="112" width="94" height="20" fill="var(--bg-3)"/>
<path d="M 80 102 l -6 0 M 80 102 l 4 -3 M 80 102 l 4 3" stroke="var(--accent)" strokeWidth="1" fill="none"/>
</g>
);
return phone(null);
}
/* ============================================================
DOC PANEL
============================================================ */
function NamedItem({ name, desc, location, preview }) {
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>
{preview && (
<div style={{
marginTop: 12, padding: 12,
background: 'var(--bg-1)',
border: '1px dashed var(--border-2)',
borderRadius: 8,
}}>{preview}</div>
)}
</div>
);
}
function ScreenCard({ type, name, when, why, gestures, example }) {
return (
<div className="card">
<div style={{ display: 'grid', gridTemplateColumns: '90px 1fr', gap: 14, alignItems: 'flex-start' }}>
<ScreenVisual type={type} />
<div style={{ minWidth: 0 }}>
<span className="pill-name" style={{ marginBottom: 10, display: 'inline-flex' }}>Écran {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>
{gestures && <div className="row-use"><span className="k">Gestes</span><span className="v">{gestures}</span></div>}
<div className="row-use"><span className="k">Tester</span><span className="v" style={{ color:'var(--accent)' }}>{example}</span></div>
</div>
</div>
</div>
);
}
function Doc() {
return (
<div className="doc">
{/* INTRO */}
<section>
<h2><Icon name="list" size={22} style={{ color: 'var(--accent)' }} /> Saisie & formulaires mobile</h2>
<p className="desc">
Suite logique de la variante mobile : <strong>écrans de connexion, profil, formulaire complet,
liste swipeable</strong>. Tous les composants sont nommés et le clavier virtuel se configure
précisément (8 types, autocomplete système, touche Entrée personnalisable).
</p>
</section>
{/* ÉCRANS */}
<section id="screens">
<h2><Icon name="grid" size={22} style={{ color: 'var(--accent)' }} /> 4 écrans modèles</h2>
<p className="desc">Chaque écran combine plusieurs composants. Bascule entre eux via les onglets en bas du smartphone.</p>
<ScreenCard
type="login"
name="Connexion"
when="Avant tout accès à l'app, ou pour se reconnecter."
why="Format unifié : logo + email + mot de passe + biométrie + lien créa de compte."
gestures="Tap sur champs · Tap sur Face ID / Touch ID · enterKeyHint='go' soumet le formulaire"
example="Onglet ☐ Login du smartphone à gauche" />
<ScreenCard
type="profile"
name="Profil utilisateur"
when="L'utilisateur veut voir/modifier ses infos."
why="Tête de page avec avatar + actions de compte + bouton ⚙ paramètres en haut à droite."
gestures="Tap sur ⚙ ouvre une BottomSheet de paramètres"
example="Onglet ☐ Profil du smartphone" />
<ScreenCard
type="form"
name="Formulaire de saisie"
when="Création/édition d'un objet (note, tâche, contact…)."
why="Tous les types d'inputs en une seule page : titre, dates, textarea, dropdown, radio, checkboxes, médias."
gestures="Tap sur OK valide · onBack remonte d'un cran"
example="Onglet ☐ Formulaire du smartphone" />
<ScreenCard
type="swipe"
name="Liste swipeable"
when="Liste d'éléments avec actions cachées (mails, notifs, tâches)."
why="Économise l'espace : actions hors-écran révélées au geste."
gestures="SwipeLeft → archive/supprime · SwipeRight → marquer lu/épingler · Tap → ouvrir"
example="Onglet ☐ Notifications du smartphone" />
</section>
{/* COMPOSANTS */}
<section id="components">
<h2><Icon name="cog" size={22} style={{ color: 'var(--accent)' }} /> Composants de saisie</h2>
<p className="desc">Tous ont une API homogène : <code className="mono" style={{color:'var(--accent)'}}>value / onChange / label / hint / error</code>. Les inputs supportent en plus le contrôle clavier virtuel.</p>
<NamedItem name="FormField" location="Wrapper de tout champ"
desc="Cadre standard : label en haut, champ au milieu, hint/erreur en bas. À utiliser autour de chaque champ pour homogénéiser." />
<NamedItem name="TextInput" location="Formulaire, Login"
desc="Champ texte unifié avec contrôle complet du clavier virtuel : type d'entrée (text/email/numeric/tel…), auto-complétion système (email, mot de passe, code OTP), texte de la touche Entrée (next, send, search…), majuscules auto, correction orthographique. Mode multiline pour textarea."
preview={<TextInput value="exemple@..." onChange={() => {}} keyboard="email" icon="bell" />} />
<NamedItem name="DateInput" location="Formulaire"
desc="Date/heure picker natif du téléphone. Modes : date, time, datetime-local, month, week. Affiche le picker iOS/Android natif au focus."
preview={<DateInput value="2026-05-21" onChange={() => {}} mode="date" />} />
<NamedItem name="Dropdown" location="Formulaire"
desc="Select natif avec habillage Gruvbox. Sur mobile, ouvre le sélecteur roulette iOS ou le menu déroulant Android. À utiliser dès 4+ options."
preview={<Dropdown value="" onChange={() => {}} placeholder="Choisir…" options={['Option A', 'Option B', 'Option C']} />} />
<NamedItem name="CheckboxItem" location="Formulaire"
desc="Case à cocher avec label + description optionnelle. Pour des options indépendantes (multi-sélection)."
preview={<CheckboxItem checked={true} onChange={() => {}} label="J'accepte les conditions" description="En cochant tu acceptes notre politique." />} />
<NamedItem name="RadioGroup" location="Formulaire"
desc="Liste d'options exclusives empilées verticalement avec puce circulaire. Pour 2-6 options. Au-delà, utilise un Dropdown."
preview={<RadioGroup value="b" onChange={() => {}} options={[
{ value: 'a', label: 'Option A', description: 'Première option' },
{ value: 'b', label: 'Option B', description: 'Deuxième option' },
]} />} />
<NamedItem name="MediaInsert" location="Formulaire"
desc="Grille 3 colonnes de boutons pour ajouter une pièce jointe : Photo (caméra arrière), Image (galerie), Vidéo, Audio (micro), Fichier (doc), Position (GPS via navigator.geolocation). Chaque type définit l'attribut HTML accept et capture."
preview={<MediaInsert onPick={() => {}} />} />
<NamedItem name="AvatarLogo" location="Login, Profil"
desc="Gros logo carré arrondi avec icône et glow accent. Pour l'identité visuelle d'un écran (login, profil, vide d'état)."
preview={<AvatarLogo icon="server" size={48} />} />
<NamedItem name="BiometricButton" location="Login"
desc="Bouton biométrique (Face ID / Touch ID). Style natif iOS — icône large + label. À placer sous le bouton principal de login."
preview={<div style={{display:'flex', gap: 16, justifyContent:'center'}}><BiometricButton kind="face" /><BiometricButton kind="touch" /></div>} />
<NamedItem name="SwipeableRow" location="Liste swipeable"
desc="Ligne d'une liste qui révèle des actions au swipe. leftActions = actions à droite (révélées en swipant vers la gauche), rightActions = actions à gauche (révélées en swipant vers la droite). Chaque action a icon, label, color, onClick. Tap sur la ligne = onTap principal."
preview={
<SwipeableRow
leftActions={[{ label: 'Suppr.', icon: 'close', color: 'var(--err)' }]}
rightActions={[{ label: 'Lu', icon: 'play', color: 'var(--info)' }]}>
<div style={{ padding: 12, background: 'var(--bg-3)', fontSize: 13 }}>
swipe-moi dans un sens ou l'autre →
</div>
</SwipeableRow>
} />
</section>
{/* CLAVIER VIRTUEL */}
<section id="keyboard">
<h2><Icon name="terminal" size={22} style={{ color: 'var(--accent)' }} /> Clavier virtuel</h2>
<p className="desc">
Sur mobile, le clavier qui s'affiche dépend de la prop <code className="mono" style={{color:'var(--accent)'}}>keyboard</code> (attribut HTML <code className="mono">inputmode</code>).
Choisis le BON type pour faire gagner du temps à l'utilisateur — exemple : <code className="mono">keyboard="numeric"</code> pour un code OTP fait apparaître directement le pavé numérique au lieu du clavier complet.
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{KEYBOARD_CATALOG.map((k) => (
<div key={k.name} className="card" style={{ margin: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span className="pill-name">{k.name}</span>
</div>
<KeyboardVisual kind={k.name} />
<div style={{ fontSize: 13, color: 'var(--ink-2)', marginTop: 10, lineHeight: 1.4 }}>{k.desc}</div>
<div style={{ fontSize: 11, color: 'var(--ink-3)', fontStyle: 'italic', marginTop: 4 }}>
Usage : {k.usage}
</div>
</div>
))}
</div>
</section>
{/* AUTOCOMPLETE */}
<section id="autocomplete">
<h2><Icon name="refresh" size={22} style={{ color: 'var(--accent)' }} /> Aide à la saisie (autocomplete)</h2>
<p className="desc">
L'attribut <code className="mono" style={{color:'var(--accent)'}}>autocomplete</code> dit au système ce que représente le champ.
Sur iOS/Android, ça déclenche : remplissage automatique (nom, email, adresse), proposition du mot de passe enregistré, génération d'un nouveau mot de passe, lecture auto du code SMS reçu.
</p>
<div className="card">
{AUTOCOMPLETE_CATALOG.map((a) => (
<div key={a.name} className="row-use">
<span className="k">{a.name}</span>
<span className="v">{a.usage}</span>
</div>
))}
</div>
</section>
{/* ENTER KEY HINT */}
<section id="enter-hint">
<h2><Icon name="chevR" size={22} style={{ color: 'var(--accent)' }} /> Touche Entrée — enterKeyHint</h2>
<p className="desc">
La touche en bas à droite du clavier peut afficher un mot différent selon le contexte (au lieu du standard "Entrée").
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
{ENTER_HINT_CATALOG.map((e) => (
<div key={e.name} className="card" style={{ margin: 0, padding: 14 }}>
<div style={{
display: 'inline-block',
padding: '4px 12px', borderRadius: 6,
background: 'var(--accent)', color: 'var(--bg-1)',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700,
marginBottom: 8,
}}>{e.name}</div>
<div style={{ fontSize: 13, color: 'var(--ink-2)' }}>{e.usage}</div>
</div>
))}
</div>
</section>
{/* CHEAT SHEET */}
<section id="cheatsheet">
<h2><Icon name="download" size={22} style={{ color: 'var(--accent)' }} /> Antisèche · combinaisons utiles</h2>
<div className="card">
<div className="row-use">
<span className="k">Email</span>
<span className="v"><code className="mono" style={{color:'var(--accent)'}}>keyboard="email"</code> + <code className="mono">autocomplete="email"</code> + <code className="mono">autocapitalize="off"</code></span>
</div>
<div className="row-use">
<span className="k">Mot de passe</span>
<span className="v"><code className="mono" style={{color:'var(--accent)'}}>type="password"</code> + <code className="mono">autocomplete="current-password"</code> (ou <code className="mono">"new-password"</code> en inscription)</span>
</div>
<div className="row-use">
<span className="k">Code OTP SMS</span>
<span className="v"><code className="mono" style={{color:'var(--accent)'}}>keyboard="numeric"</code> + <code className="mono">autocomplete="one-time-code"</code> + <code className="mono">maxLength=6</code></span>
</div>
<div className="row-use">
<span className="k">Téléphone</span>
<span className="v"><code className="mono" style={{color:'var(--accent)'}}>keyboard="tel"</code> + <code className="mono">autocomplete="tel"</code></span>
</div>
<div className="row-use">
<span className="k">Recherche</span>
<span className="v"><code className="mono" style={{color:'var(--accent)'}}>keyboard="search"</code> + <code className="mono">enterHint="search"</code></span>
</div>
<div className="row-use">
<span className="k">Prix / mesure</span>
<span className="v"><code className="mono" style={{color:'var(--accent)'}}>keyboard="decimal"</code></span>
</div>
<div className="row-use">
<span className="k">Adresse</span>
<span className="v"><code className="mono" style={{color:'var(--accent)'}}>autocomplete="address-line1"</code>, puis <code className="mono">postal-code</code>, <code className="mono">country</code></span>
</div>
<div className="row-use">
<span className="k">Texte libre</span>
<span className="v"><code className="mono" style={{color:'var(--accent)'}}>autocapitalize="sentences"</code> + <code className="mono">spellCheck=true</code></span>
</div>
</div>
</section>
</div>
);
}
/* ============================================================
APP ROOT
============================================================ */
function PhoneAppSaisie({ theme }) {
const [tab, setTab] = uDS('login');
const [toast, setToast] = uDS(null);
const [sheet, setSheet] = uDS(false);
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 === 'login' && <ScreenLogin onLogin={() => setTab('profile')} showToast={showToast} />}
{tab === 'profile' && <ScreenProfile openSettings={() => setSheet(true)} />}
{tab === 'form' && <ScreenForm showToast={showToast} />}
{tab === 'swipe' && <ScreenSwipe showToast={showToast} />}
</div>
<TabBar
active={tab}
onSelect={setTab}
items={[
{ id: 'login', icon: 'user', label: 'login' },
{ id: 'profile', icon: 'cog', label: 'profil' },
{ id: 'form', icon: 'list', label: 'form' },
{ id: 'swipe', icon: 'chevR', label: 'notifs' },
]}
/>
<BottomSheet open={sheet} onClose={() => setSheet(false)} title="Paramètres rapides">
<ListSection>
<ListRow icon="moon" iconColor="var(--info)" label="Thème" value="Sombre" onClick={() => {}} />
<ListRow icon="bell" iconColor="var(--accent)" label="Notifications" right={<Toggle on={true} onChange={() => {}} />} />
<ListRow icon="refresh" iconColor="var(--ok)" label="Sync auto" right={<Toggle on={false} onChange={() => {}} />} />
</ListSection>
<ListSection>
<ListRow icon="power" iconColor="var(--err)" label="Se déconnecter" danger onClick={() => { setSheet(false); setTab('login'); }} />
</ListSection>
</BottomSheet>
<Toast open={toast !== null} onClose={() => setToast(null)} message={toast} variant="ok" />
</div>
);
}
function App() {
const [theme, setTheme] = uDS('dark');
const [device, setDevice] = uDS('ios');
eDS(() => { document.documentElement.dataset.theme = theme; }, [theme]);
return (
<React.Fragment>
<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="list" size={16} />
</div>
<h1>Exemple mobile · saisie <small>login · profil · form · swipe · clavier virtuel</small></h1>
<span style={{ flex: 1 }}></span>
<a href="exemple-mobile.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 mobile</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 } : {}}>
<PhoneAppSaisie theme={theme} />
</div>
</div>
<div className="legend">↑ teste les écrans, swipe les lignes, joue avec les formulaires</div>
</div>
<Doc />
</div>
</React.Fragment>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);