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>
342 lines
15 KiB
React
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 });
|