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

342 lines
15 KiB
React

/* ============================================================
exemple-mobile-saisie-app.jsx — partie 1
Écrans du smartphone (Login, Profile, Form, SwipeList).
La partie Doc + ROOT est dans exemple-mobile-saisie-doc.jsx.
============================================================ */
const { useState: uMS, useEffect: eMS } = React;
/* ============================================================
ÉCRAN 1 — Login
============================================================ */
function ScreenLogin({ onLogin, showToast }) {
const [email, setEmail] = uMS('');
const [pwd, setPwd] = uMS('');
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<div style={{
flex: 1, padding: '32px 24px', overflowY: 'auto',
display: 'flex', flexDirection: 'column', gap: 16,
}}>
<div style={{ textAlign: 'center', marginTop: 24, marginBottom: 12 }}>
<AvatarLogo icon="server" size={80} />
<div style={{ fontSize: 26, fontWeight: 700, marginTop: 16 }}>Bienvenue</div>
<div style={{ fontSize: 14, color: 'var(--ink-3)', marginTop: 4 }}>Connecte-toi à ton compte</div>
</div>
<FormField label="Email">
<TextInput value={email} onChange={setEmail}
placeholder="prenom@exemple.com"
type="email" icon="bell"
keyboard="email"
autocomplete="email"
autocapitalize="off"
spellCheck={false}
enterHint="next" />
</FormField>
<FormField label="Mot de passe" hint="≥ 8 caractères, 1 chiffre">
<TextInput value={pwd} onChange={setPwd}
placeholder="••••••••"
type="password" icon="power"
keyboard="text"
autocomplete="current-password"
autocapitalize="off"
spellCheck={false}
enterHint="go" />
</FormField>
<a href="#" onClick={(e) => { e.preventDefault(); showToast('Email de réinitialisation envoyé'); }}
style={{ fontSize: 13, color: 'var(--accent)', textAlign: 'right', textDecoration: 'none' }}>
Mot de passe oublié ?
</a>
<div style={{ marginTop: 6 }}>
<PrimaryButton icon="play" onClick={() => { onLogin(); showToast('Bienvenue Marc !'); }}>
Se connecter
</PrimaryButton>
</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 12, margin: '8px 0',
color: 'var(--ink-4)', fontFamily: 'var(--font-mono)', fontSize: 11,
}}>
<div style={{ flex: 1, height: 1, background: 'var(--border-2)' }}></div>
OU
<div style={{ flex: 1, height: 1, background: 'var(--border-2)' }}></div>
</div>
<div style={{ display: 'flex', justifyContent: 'center', gap: 24 }}>
<BiometricButton kind="face" onClick={() => { onLogin(); showToast('Face ID OK'); }} />
<BiometricButton kind="touch" onClick={() => { onLogin(); showToast('Touch ID OK'); }} />
</div>
<div style={{ textAlign: 'center', marginTop: 16, fontSize: 14, color: 'var(--ink-3)' }}>
Pas encore de compte ?{' '}
<a href="#" onClick={(e) => e.preventDefault()} style={{ color: 'var(--accent)', textDecoration: 'none', fontWeight: 600 }}>
S'inscrire
</a>
</div>
</div>
</div>
);
}
/* ============================================================
ÉCRAN 2 — Profile (avec bouton Paramètres haut-droite)
============================================================ */
function ScreenProfile({ openSettings }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<NavBar title="Profil" right={
<IconButton icon="cog" label="Paramètres" onClick={openSettings} size={34} />
} />
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 0 80px' }}>
<div style={{
padding: '20px 16px',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
}}>
<AvatarLogo icon="user" size={72} />
<div style={{ fontSize: 20, fontWeight: 700, marginTop: 8 }}>Marc Dupont</div>
<div style={{ fontSize: 13, color: 'var(--ink-3)' }} className="mono">admin · marc@exemple.com</div>
<div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
<span style={{
padding: '3px 10px', borderRadius: 999,
background: 'var(--ok-glow)', color: 'var(--ok)',
border: '1px solid var(--ok)',
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '0.06em',
}}>● connecté</span>
<span style={{
padding: '3px 10px', borderRadius: 999,
background: 'var(--accent-tint)', color: 'var(--accent)',
border: '1px solid var(--accent)',
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '0.06em',
}}>premium</span>
</div>
</div>
<ListSection title="Mon compte">
<ListRow icon="user" iconColor="var(--blue)" label="Informations personnelles" onClick={() => {}} />
<ListRow icon="bell" iconColor="var(--accent)" label="Notifications" value="3" onClick={() => {}} />
<ListRow icon="power" iconColor="var(--ok)" label="Sécurité & connexion" onClick={() => {}} />
</ListSection>
<ListSection title="Mes données">
<ListRow icon="download" iconColor="var(--info)" label="Exporter mes données" onClick={() => {}} />
<ListRow icon="folder" iconColor="var(--purple)" label="Mes documents" value="124" onClick={() => {}} />
</ListSection>
<ListSection>
<ListRow icon="close" iconColor="var(--ink-4)" label="Se déconnecter" onClick={() => {}} />
</ListSection>
</div>
</div>
);
}
/* ============================================================
ÉCRAN 3 — Form (formulaire de saisie complet)
============================================================ */
function ScreenForm({ showToast, openSheet }) {
const [title, setTitle] = uMS('');
const [date, setDate] = uMS('2026-05-21');
const [time, setTime] = uMS('14:30');
const [body, setBody] = uMS('');
const [category, setCategory] = uMS('');
const [priority, setPriority] = uMS('normal');
const [tags, setTags] = uMS({ urgent: false, perso: true, travail: false });
const [confirmed, setConfirmed] = uMS(false);
const [media, setMedia] = uMS([]);
const onMedia = (kind, data) => {
if (kind === 'gps' && data && data.lat) {
setMedia([...media, { kind: 'gps', label: `GPS · ${data.lat.toFixed(4)}, ${data.lon.toFixed(4)}` }]);
} else if (data && data.name) {
setMedia([...media, { kind, label: `${kind} · ${data.name}` }]);
} else {
showToast(`${kind} sélectionné`);
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<NavBar title="Nouvelle note"
onBack={() => showToast('Retour')}
right={
<button onClick={() => { showToast('Enregistré'); }} style={{
padding: '6px 12px',
background: 'transparent', border: 'none',
color: 'var(--accent)', fontFamily: 'var(--font-ui)',
fontWeight: 700, fontSize: 14, cursor: 'pointer',
WebkitTapHighlightColor: 'transparent',
}}>OK</button>
} />
<div style={{ flex: 1, overflowY: 'auto', padding: '14px 16px 80px' }}>
<FormField label="Titre" required>
<TextInput value={title} onChange={setTitle}
placeholder="Titre de la note"
keyboard="text" autocapitalize="sentences"
enterHint="next" maxLength={80} icon="list" />
</FormField>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
<FormField label="Date">
<DateInput value={date} onChange={setDate} mode="date" />
</FormField>
<FormField label="Heure">
<DateInput value={time} onChange={setTime} mode="time" />
</FormField>
</div>
<FormField label="Contenu" hint="Décris ce qui doit être fait.">
<TextInput value={body} onChange={setBody}
placeholder="Tape ton texte ici…"
multiline rows={4}
keyboard="text" autocapitalize="sentences"
spellCheck={true} />
</FormField>
<FormField label="Catégorie">
<Dropdown value={category} onChange={setCategory}
placeholder="Choisir une catégorie…"
options={[
{ value: 'todo', label: 'À faire' },
{ value: 'note', label: 'Note simple' },
{ value: 'meeting', label: 'Réunion' },
{ value: 'bug', label: 'Bug à corriger' },
]} />
</FormField>
<FormField label="Priorité">
<RadioGroup value={priority} onChange={setPriority} options={[
{ value: 'low', label: 'Basse', description: 'Sans urgence' },
{ value: 'normal', label: 'Normale', description: 'Par défaut' },
{ value: 'high', label: 'Haute', description: 'À traiter rapidement' },
]} />
</FormField>
<FormField label="Étiquettes">
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{Object.entries({ urgent: 'Urgent', perso: 'Perso', travail: 'Travail' }).map(([k, v]) => (
<CheckboxItem key={k}
checked={tags[k]}
onChange={(c) => setTags({ ...tags, [k]: c })}
label={v} />
))}
</div>
</FormField>
<FormField label="Pièces jointes">
<MediaInsert onPick={onMedia} />
{media.length > 0 && (
<div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
{media.map((m, i) => (
<div key={i} style={{
padding: '8px 12px', fontSize: 12, fontFamily: 'var(--font-mono)',
background: 'var(--bg-3)', border: '1px solid var(--border-2)',
borderRadius: 8, color: 'var(--ink-2)',
}}>📎 {m.label}</div>
))}
</div>
)}
</FormField>
<FormField label="Code de confirmation" hint="On t'envoie un code par SMS.">
<TextInput value="" onChange={() => {}}
placeholder="123456"
keyboard="numeric"
autocomplete="one-time-code"
maxLength={6} icon="bell" />
</FormField>
<CheckboxItem checked={confirmed} onChange={setConfirmed}
label="J'accepte les conditions"
description="En cochant, tu acceptes notre politique." />
<div style={{ marginTop: 16 }}>
<PrimaryButton icon="download" onClick={() => showToast('Note enregistrée')}>
Enregistrer la note
</PrimaryButton>
</div>
</div>
</div>
);
}
/* ============================================================
ÉCRAN 4 — Liste avec SwipeableRow
============================================================ */
function ScreenSwipe({ showToast }) {
const [items, setItems] = uMS([
{ id: 1, title: 'Sauvegarde serveur OK', from: 'cron@srv', time: '14:02', unread: true },
{ id: 2, title: 'Latence élevée détectée', from: 'monitoring', time: '13:58', unread: true },
{ id: 3, title: 'Rappel : réunion équipe', from: 'agenda', time: '11:30', unread: false },
{ id: 4, title: 'Mise à jour disponible', from: 'systeme', time: '09:14', unread: false },
{ id: 5, title: 'Nouveau hôte sur le réseau', from: 'ipwatch', time: '08:42', unread: true },
]);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<NavBar large title="Notifications" subtitle="essaie de swiper une ligne ←→" />
<div style={{ flex: 1, overflowY: 'auto', paddingBottom: 80 }}>
{items.map((it) => (
<SwipeableRow key={it.id}
onTap={() => showToast(`Ouvrir : ${it.title}`)}
rightActions={[
{ label: 'Lu', icon: 'play', color: 'var(--info)',
onClick: () => setItems(items.map((x) => x.id === it.id ? { ...x, unread: false } : x)) },
{ label: 'Épingl.', icon: 'bell', color: 'var(--accent)',
onClick: () => showToast('Épinglé') },
]}
leftActions={[
{ label: 'Archiv.', icon: 'folder', color: 'var(--ok)',
onClick: () => { setItems(items.filter((x) => x.id !== it.id)); showToast('Archivé'); } },
{ label: 'Suppr.', icon: 'close', color: 'var(--err)',
onClick: () => { setItems(items.filter((x) => x.id !== it.id)); showToast('Supprimé'); } },
]}>
<div style={{
padding: '14px 16px',
display: 'flex', gap: 12, alignItems: 'flex-start',
borderBottom: '1px solid var(--border-1)',
background: 'var(--bg-3)',
}}>
<span style={{
width: 10, height: 10, borderRadius: '50%',
background: it.unread ? 'var(--accent)' : 'transparent',
marginTop: 6, flex: '0 0 auto',
boxShadow: it.unread ? '0 0 6px var(--accent-glow)' : 'none',
}} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 14, fontWeight: it.unread ? 700 : 500, color: 'var(--ink-1)' }}>{it.from}</span>
<span style={{ fontSize: 11, color: 'var(--ink-3)' }} className="mono">{it.time}</span>
</div>
<div style={{ fontSize: 14, color: 'var(--ink-2)', marginTop: 2 }}>{it.title}</div>
</div>
</div>
</SwipeableRow>
))}
{items.length === 0 && (
<div style={{ padding: 32, textAlign: 'center', color: 'var(--ink-3)' }}>
Plus de notifications — fais un swipe sur une ligne ←→ pour voir les actions.
</div>
)}
<div style={{
padding: '20px 16px', textAlign: 'center', fontSize: 12, color: 'var(--ink-4)',
fontFamily: 'var(--font-mono)',
}}>
swipe gauche : archiver/supprimer · swipe droit : marquer lu/épingler
</div>
</div>
</div>
);
}
Object.assign(window, { ScreenLogin, ScreenProfile, ScreenForm, ScreenSwipe });