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>
487 lines
26 KiB
React
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"><{name}/></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 />);
|