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

1117 lines
58 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
exemple-mobile-apps-app.jsx
10 écrans navigables : Onboarding, Accueil, Chat (liste+détail),
Calendrier, Maps, Recherche, Scanner QR, Caméra, Fichiers, Paramètres.
Avatar+menu présents sur tous les écrans en haut à droite.
============================================================ */
const { useState: uX, useEffect: eX } = React;
/* ---------- DONNÉES MOCK ---------- */
const CHATS = [
{ id: 1, name: 'Marc Dubois', avatar: 'var(--accent)', last: 'Tu peux jeter un œil au scan ?', time: '14:02', unread: 2 },
{ id: 2, name: 'Équipe Ops', avatar: 'var(--blue)', last: 'Léa : déploiement OK 🚀', time: '13:14', unread: 0 },
{ id: 3, name: 'Sophie', avatar: 'var(--purple)', last: 'On se voit ce soir ?', time: '11:30', unread: 1 },
{ id: 4, name: 'Antoine', avatar: 'var(--ok)', last: 'Merci pour le tuyau !', time: 'hier', unread: 0 },
{ id: 5, name: 'Notifs syst.', avatar: 'var(--warn)', last: 'Backup horaire OK · 812 Mo', time: 'hier', unread: 0 },
];
const CHAT_MESSAGES = [
{ id: 1, me: false, text: 'Salut ! Tu as eu le temps de regarder le scan d\'hier soir ?', time: '13:58' },
{ id: 2, me: true, text: 'Oui, je viens de finir. Le node-03 a un soucis de latence', time: '13:59', status: 'read' },
{ id: 3, me: false, text: 'Aïe. Tu peux me partager les détails ?', time: '14:00' },
{ id: 4, me: true, text: 'Je t\'envoie le rapport dans 5 min', time: '14:01', status: 'read' },
{ id: 5, me: false, text: 'Top, merci 🙏', time: '14:02' },
];
const FILES = [
{ name: 'Documents', type: 'folder', date: '21 mai' },
{ name: 'Photos 2026', type: 'folder', date: '12 mai' },
{ name: 'rapport-scan.pdf', type: 'pdf', date: '21 mai 14:02', size: 384000 },
{ name: 'capture.png', type: 'image', date: '21 mai 13:58', size: 1280000 },
{ name: 'meeting.mp4', type: 'video', date: '20 mai', size: 124000000 },
{ name: 'notes.md', type: 'code', date: '20 mai', size: 4800 },
{ name: 'audio-memo.m4a',type: 'audio', date: '19 mai', size: 820000 },
{ name: 'backup.zip', type: 'archive', date: '19 mai 03:00', size: 850000000 },
];
const ONBOARDING_SLIDES = [
{ icon: 'server', color: 'var(--accent)', title: 'Bienvenue 👋', desc: 'Cette app te permet de superviser tes serveurs et capteurs depuis ton téléphone.' },
{ icon: 'bell', color: 'var(--blue)', title: 'Tu es alerté en temps réel', desc: 'Reçois une notification dès qu\'un service tombe ou qu\'une métrique sort des seuils.' },
{ icon: 'network', color: 'var(--purple)', title: 'Maîtrise ton réseau', desc: 'Lance un scan, repère les nouveaux hôtes, ouvre une session SSH depuis le mobile.' },
];
const NOW = new Date(2026, 4, 21); // 21 mai 2026
const EVENTS = new Set([3, 7, 12, 14, 21, 24, 28]);
/* ============================================================
HeaderAvatar — header partagé (titre + avatar rond)
Le bouton Avatar ouvre AvatarMenu (popup).
============================================================ */
function HeaderAvatar({ title, large, subtitle, onBack, onAvatar, avatarColor = 'var(--accent)' }) {
return (
<NavBar
title={title} large={large} subtitle={subtitle} onBack={onBack}
right={<Avatar name="Marc" color={avatarColor} size={36} onClick={onAvatar} />}
/>
);
}
/* ============================================================
ÉCRANS
============================================================ */
/* --- Accueil : grille d'apps --- */
function ScreenHome({ goto, onAvatar }) {
const apps = [
{ id: 'chat', icon: 'bell', color: 'var(--accent)', title: 'Messages', subtitle: '3 non lus', badge: 3 },
{ id: 'calendar', icon: 'clock', color: 'var(--blue)', title: 'Calendrier', subtitle: 'mai 2026' },
{ id: 'maps', icon: 'network', color: 'var(--ok)', title: 'Carte', subtitle: '8 pins' },
{ id: 'search', icon: 'search', color: 'var(--info)', title: 'Recherche', subtitle: 'global' },
{ id: 'scanner', icon: 'grid', color: 'var(--purple)', title: 'Scanner QR', subtitle: 'caméra' },
{ id: 'camera', icon: 'memory', color: 'var(--warn)', title: 'Appareil photo', subtitle: 'caméra' },
{ id: 'files', icon: 'folder', color: 'var(--info)', title: 'Fichiers', subtitle: '124 \u00e9l\u00e9ments' },
{ id: 'settings', icon: 'cog', color: 'var(--ink-3)', title: 'Paramètres', subtitle: 'préférences' },
{ id: 'onboard', icon: 'play', color: 'var(--accent-soft)', title: 'Onboarding', subtitle: 'revoir l\'intro' },
];
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<HeaderAvatar large title="Hello Marc" subtitle="prêt à explorer l'app ?" onAvatar={onAvatar} />
<div style={{ flex: 1, overflowY: 'auto', padding: 14 }}>
<SearchBar value="" onChange={() => {}} placeholder="Rechercher dans tout…" />
<div style={{ marginTop: 14, display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
{apps.map((a) => (
<ActionCard key={a.id} {...a} onClick={() => goto(a.id)} />
))}
</div>
</div>
</div>
);
}
/* --- Chat list --- */
function ScreenChatList({ onAvatar, open }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<HeaderAvatar large title="Messages" subtitle={`${CHATS.reduce((a, c) => a + c.unread, 0)} non lus`} onAvatar={onAvatar} />
<div style={{ padding: '4px 14px 8px' }}>
<SearchBar value="" onChange={() => {}} placeholder="Rechercher une conversation" />
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
{CHATS.map((c) => (
<SwipeableRow key={c.id}
onTap={() => open(c)}
rightActions={[
{ label: 'Lu', icon: 'play', color: 'var(--info)' },
{ label: 'Épingl.', icon: 'bell', color: 'var(--accent)' },
]}
leftActions={[
{ label: 'Archiv.', icon: 'folder', color: 'var(--ok)' },
{ label: 'Suppr.', icon: 'close', color: 'var(--err)' },
]}>
<div style={{
padding: '12px 14px', display: 'flex', gap: 12, alignItems: 'center',
borderBottom: '1px solid var(--border-1)',
background: 'var(--bg-3)',
}}>
<Avatar name={c.name} color={c.avatar} size={48} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 15, fontWeight: c.unread ? 700 : 500 }}>{c.name}</span>
<span style={{ fontSize: 11, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>{c.time}</span>
</div>
<div style={{ fontSize: 13, color: 'var(--ink-3)', marginTop: 2, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{c.last}</div>
</div>
{c.unread > 0 && (
<span style={{
background: 'var(--accent)', color: 'var(--bg-1)',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700,
padding: '2px 7px', borderRadius: 999,
}}>{c.unread}</span>
)}
</div>
</SwipeableRow>
))}
</div>
</div>
);
}
/* --- Chat detail --- */
function ScreenChatDetail({ chat, onBack, onAvatar }) {
const [msgs, setMsgs] = uX(CHAT_MESSAGES);
const send = (text) => setMsgs([...msgs, { id: Date.now(), me: true, text, time: 'maint.', status: 'sent' }]);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<div style={{
padding: '8px 12px',
display: 'flex', alignItems: 'center', gap: 10,
borderBottom: '1px solid var(--border-2)',
background: 'var(--surf-glass-strong)',
backdropFilter: 'blur(14px)',
}}>
<button onClick={onBack} className="touch-press" style={{
width: 36, height: 36, borderRadius: 8, background: 'transparent',
border: 'none', color: 'var(--accent)', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
WebkitTapHighlightColor: 'transparent',
}}><Icon name="chevL" size={20} /></button>
<Avatar name={chat.name} color={chat.avatar} size={32} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 700 }}>{chat.name}</div>
<div style={{ fontSize: 11, color: 'var(--ok)', fontFamily: 'var(--font-mono)' }}> en ligne</div>
</div>
<Avatar name="M" color="var(--accent)" size={34} onClick={onAvatar} />
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '12px 0', background: 'var(--bg-1)' }}>
{msgs.map((m) => <ChatBubble key={m.id} {...m} />)}
</div>
<ChatComposer onSend={send} />
</div>
);
}
/* --- Calendar --- */
function ScreenCalendar({ onAvatar, showToast }) {
const [sel, setSel] = uX(NOW);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<HeaderAvatar large title="Calendrier" subtitle="mai 2026" onAvatar={onAvatar} />
<div style={{ flex: 1, overflowY: 'auto', paddingBottom: 80 }}>
<CalendarMonth
year={2026} month={4}
selected={sel} onSelect={(d) => { setSel(d); showToast(`${d.getDate()} mai sélectionné`); }}
events={EVENTS} />
<ListSection title={`Évènements · ${sel.getDate()} mai`}>
<ListRow icon="clock" iconColor="var(--accent)" label="Standup équipe" value="09:00" onClick={() => {}} />
<ListRow icon="user" iconColor="var(--blue)" label="1:1 avec Sophie" value="14:30" onClick={() => {}} />
<ListRow icon="bell" iconColor="var(--warn)" label="Rappel : sauvegarde" value="18:00" onClick={() => {}} />
</ListSection>
</div>
<FAB icon="plus" label="Nouvel évènement" onClick={() => showToast('Nouvel évènement')} />
</div>
);
}
/* --- Maps --- */
function ScreenMaps({ onAvatar }) {
const [active, setActive] = uX(0);
const pins = [
{ x: 30, y: 35, color: 'var(--accent)', icon: 'server', label: 'Bureau' },
{ x: 60, y: 50, color: 'var(--ok)', icon: 'grid', label: 'Maison' },
{ x: 75, y: 25, color: 'var(--blue)', icon: 'bell', label: 'Café' },
{ x: 22, y: 70, color: 'var(--err)', icon: 'power', label: 'Atelier' },
{ x: 50, y: 80, color: 'var(--warn)', icon: 'clock' },
{ x: 80, y: 65, color: 'var(--purple)', icon: 'memory' },
];
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative' }}>
<StatusBar />
<NavBar title="Carte" right={<Avatar name="M" size={36} onClick={onAvatar} />} />
<div style={{ position: 'absolute', top: 88, left: 14, right: 14, zIndex: 10 }}>
<SearchBar value="" onChange={() => {}} placeholder="Adresse, lieu…" />
</div>
<div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
<MapView pins={pins} />
{/* contrôles flottants */}
<div style={{
position: 'absolute', top: 70, right: 12,
display: 'flex', flexDirection: 'column', gap: 8,
}}>
<IconButton icon="plus" label="Zoom +" size={40} />
<IconButton icon="close" label="Zoom " size={40} />
<IconButton icon="network" label="Ma position" size={40} primary />
</div>
{/* bottom info */}
<div className="glass-strong" style={{
position: 'absolute', bottom: 16, left: 14, right: 14,
borderRadius: 14, padding: 14,
display: 'flex', gap: 12, alignItems: 'center',
}}>
<div style={{
width: 40, height: 40, borderRadius: 10,
background: pins[active].color,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--bg-1)',
}}><Icon name={pins[active].icon} size={18} /></div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 700 }}>{pins[active].label || 'Lieu sans nom'}</div>
<div style={{ fontSize: 11, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>48.8566° N · 2.3522° E</div>
</div>
<IconButton icon="chevR" label="Itinéraire" primary />
</div>
</div>
</div>
);
}
/* --- Search avec filtres --- */
function ScreenSearch({ onBack, onAvatar }) {
const [q, setQ] = uX('');
const [filters, setFilters] = uX(['all']);
const allResults = [
{ id: 1, type: 'people', title: 'Sophie Martin', meta: 'sophie@exemple.com' },
{ id: 2, type: 'message', title: 'Tu peux jeter un œil…', meta: 'Marc · hier 14:02' },
{ id: 3, type: 'file', title: 'rapport-scan.pdf', meta: '21 mai · 384 Ko' },
{ id: 4, type: 'event', title: 'Standup équipe', meta: 'mardi 09:00' },
{ id: 5, type: 'place', title: 'Bureau Paris 11e', meta: '48.8566° N · 2.3522° E' },
{ id: 6, type: 'file', title: 'capture.png', meta: '21 mai · 1.2 Mo' },
];
const results = allResults.filter((r) => {
if (q && !r.title.toLowerCase().includes(q.toLowerCase())) return false;
if (filters.includes('all')) return true;
return filters.includes(r.type);
});
const iconFor = (t) => ({ people: 'user', message: 'bell', file: 'folder', event: 'clock', place: 'network' })[t];
const colorFor = (t) => ({ people: 'var(--accent)', message: 'var(--blue)', file: 'var(--info)', event: 'var(--purple)', place: 'var(--ok)' })[t];
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<NavBar title="Recherche" onBack={onBack} right={<Avatar name="M" size={36} onClick={onAvatar} />} />
<div style={{ padding: '4px 14px' }}>
<SearchBar value={q} onChange={setQ} placeholder="Personnes, messages, fichiers…" />
<div style={{ marginTop: 10 }}>
<FilterChips value={filters} onChange={(v) => setFilters(v.length ? v : ['all'])}
options={[
{ value: 'all', label: 'Tout', icon: 'grid' },
{ value: 'people', label: 'Personnes', icon: 'user' },
{ value: 'message', label: 'Messages', icon: 'bell' },
{ value: 'file', label: 'Fichiers', icon: 'folder' },
{ value: 'event', label: 'Agenda', icon: 'clock' },
{ value: 'place', label: 'Lieux', icon: 'network' },
]} />
</div>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '14px 14px 80px' }}>
<div className="label" style={{ marginBottom: 8 }}>{results.length} résultat{results.length > 1 ? 's' : ''}</div>
<div style={{
background: 'var(--bg-3)', border: '1px solid var(--border-2)',
borderRadius: 12, overflow: 'hidden',
}}>
{results.map((r) => (
<ListRow key={r.id} icon={iconFor(r.type)} iconColor={colorFor(r.type)}
label={r.title} value={r.meta} onClick={() => {}} />
))}
{results.length === 0 && (
<div style={{ padding: 32, textAlign: 'center', color: 'var(--ink-3)' }}>
Aucun résultat pour "<b>{q}</b>"
</div>
)}
</div>
</div>
</div>
);
}
/* --- QR Scanner --- */
function ScreenScanner({ onBack, showToast }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<div style={{
position: 'absolute', top: 44, left: 0, right: 0, zIndex: 10,
padding: '8px 12px',
display: 'flex', alignItems: 'center', gap: 10,
}}>
<button onClick={onBack} className="touch-press" style={{
width: 36, height: 36, borderRadius: 18,
background: 'rgba(0,0,0,0.55)', border: 'none', color: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', WebkitTapHighlightColor: 'transparent',
}}><Icon name="chevL" size={18} /></button>
<div style={{ flex: 1 }}/>
<div style={{
padding: '4px 12px', borderRadius: 999,
background: 'rgba(0,0,0,0.55)',
color: '#fff', fontFamily: 'var(--font-mono)', fontSize: 11,
}}>Scanner QR / code-barres</div>
</div>
<div style={{ flex: 1, position: 'relative' }}>
<QrScannerView onCapture={() => showToast('Code détecté : https://exemple.com')} />
</div>
</div>
);
}
/* --- Caméra --- */
function ScreenCamera({ onBack, showToast }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<div style={{
position: 'absolute', top: 44, left: 0, right: 0, zIndex: 10,
padding: '8px 12px',
display: 'flex', alignItems: 'center', gap: 10,
}}>
<button onClick={onBack} className="touch-press" style={{
width: 36, height: 36, borderRadius: 18,
background: 'rgba(0,0,0,0.55)', border: 'none', color: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', WebkitTapHighlightColor: 'transparent',
}}><Icon name="chevL" size={18} /></button>
</div>
<div style={{ flex: 1, position: 'relative' }}>
<CameraView onShoot={() => showToast('Photo prise')} />
</div>
</div>
);
}
/* --- Fichiers --- */
function ScreenFiles({ onBack, onAvatar, showToast }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<NavBar title="Fichiers" subtitle="Espace personnel" onBack={onBack}
right={<Avatar name="M" size={36} onClick={onAvatar} />} />
<div style={{ padding: '6px 14px' }}>
<SearchBar value="" onChange={() => {}} placeholder="Rechercher un fichier" />
</div>
<div style={{
padding: '8px 14px',
fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)',
display: 'flex', alignItems: 'center', gap: 6,
}}>
<Icon name="folder" size={12} style={{ color: 'var(--accent)' }} />
Mes fichiers / Espace personnel
</div>
<div style={{ flex: 1, overflowY: 'auto', paddingBottom: 80 }}>
<FileExplorer items={FILES} onOpen={(it) => showToast(`Ouvrir : ${it.name}`)}
onAction={(act, it) => showToast(`${act} · ${it.name}`)} />
</div>
<FAB icon="plus" label="Importer" onClick={() => showToast('Importer un fichier')} />
</div>
);
}
/* --- Settings --- */
function ScreenSettings({ onBack, onAvatar, showToast, openSheet }) {
const [notif, setNotif] = uX(true);
const [bio, setBio] = uX(false);
const [sync, setSync] = uX(true);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<StatusBar />
<HeaderAvatar large title="Paramètres" onBack={onBack} onAvatar={onAvatar} />
<div style={{ flex: 1, overflowY: 'auto', paddingBottom: 80 }}>
<ListSection title="Compte">
<ListRow icon="user" iconColor="var(--blue)" label="Mon profil"
value="Marc Dupont" onClick={() => showToast('Profil')} />
<ListRow icon="bell" iconColor="var(--accent)" label="Email"
value="marc@exemple.com" onClick={() => {}} />
</ListSection>
<ListSection title="Préférences">
<ListRow icon="moon" iconColor="var(--info)"
label="Thème" value="Sombre" onClick={() => openSheet('theme')} />
<ListRow icon="cog" iconColor="var(--ink-3)"
label="Langue" value="Français" onClick={() => openSheet('lang')} />
<ListRow icon="bell" iconColor="var(--accent)" label="Notifications"
right={<Toggle on={notif} onChange={setNotif} />} />
<ListRow icon="refresh" iconColor="var(--ok)" label="Sync auto"
right={<Toggle on={sync} onChange={setSync} />} />
<ListRow icon="power" iconColor="var(--purple)" label="Face ID au démarrage"
right={<Toggle on={bio} onChange={setBio} />} />
</ListSection>
<ListSection title="Confidentialité">
<ListRow icon="folder" iconColor="var(--blue)" label="Données stockées" value="248 Mo" onClick={() => {}} />
<ListRow icon="download" iconColor="var(--ok)" label="Exporter mes données" onClick={() => {}} />
<ListRow icon="close" iconColor="var(--err)" label="Effacer le cache" onClick={() => showToast('Cache effacé')} />
</ListSection>
<ListSection title="À propos" hint="Application v1.0.6 · build 2026.05.21">
<ListRow icon="list" iconColor="var(--ink-3)" label="Conditions générales" onClick={() => {}} />
<ListRow icon="list" iconColor="var(--ink-3)" label="Politique de confidentialité" onClick={() => {}} />
<ListRow icon="bell" iconColor="var(--ink-3)" label="Centre d'aide" onClick={() => {}} />
</ListSection>
<ListSection>
<ListRow icon="power" iconColor="var(--err)" label="Se déconnecter"
danger onClick={() => showToast('Déconnexion')} />
</ListSection>
</div>
</div>
);
}
Object.assign(window, {
ScreenHome, ScreenChatList, ScreenChatDetail,
ScreenCalendar, ScreenMaps, ScreenSearch,
ScreenScanner, ScreenCamera, ScreenFiles, ScreenSettings,
ONBOARDING_SLIDES, CHATS, CHAT_MESSAGES, FILES, EVENTS,
});
/* ============================================================
exemple-mobile-apps-doc.jsx
Doc panneau droite (catalogue commenté avec visuels) + ROOT.
============================================================ */
const { useState: uAD, useEffect: eAD } = React;
/* ============================================================
ScreenVisual — mini SVG de chaque écran (pour la doc)
============================================================ */
function ScreenVisual({ type }) {
const frame = (inner, bg) => (
<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={bg || '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 === 'onboarding') return frame(
<g>
<rect x="34" y="48" width="32" height="32" rx="8" fill="var(--accent)"/>
<rect x="20" y="92" width="60" height="4" rx="2" fill="var(--ink-1)"/>
<rect x="26" y="102" width="48" height="2.5" rx="1" fill="var(--ink-3)"/>
<rect x="30" y="108" width="40" height="2.5" rx="1" fill="var(--ink-3)"/>
{/* dots */}
<circle cx="42" cy="138" r="1.5" fill="var(--ink-4)"/>
<rect x="46" y="136.5" width="8" height="3" rx="1.5" fill="var(--accent)"/>
<circle cx="58" cy="138" r="1.5" fill="var(--ink-4)"/>
{/* button */}
<rect x="20" y="150" width="60" height="12" rx="5" fill="var(--accent)"/>
</g>
);
if (type === 'home') return frame(
<g>
<rect x="3" y="14" width="94" height="20" fill="var(--bg-2)"/>
<rect x="10" y="18" width="40" height="4" rx="2" fill="var(--ink-1)"/>
<rect x="10" y="25" width="28" height="2.5" rx="1" fill="var(--ink-3)"/>
<circle cx="88" cy="24" r="6" fill="var(--accent)"/>
<rect x="10" y="38" width="80" height="9" rx="4" fill="var(--bg-2)" stroke="var(--border-2)" strokeWidth="0.5"/>
{/* 9 grid */}
{[0,1,2,0,1,2,0,1,2].map((c, i) => {
const r = Math.floor(i / 3);
return <rect key={i} x={10 + c * 27} y={52 + r * 32} width="24" height="28" rx="4"
fill="var(--bg-2)" stroke="var(--border-2)" strokeWidth="0.5"/>;
})}
</g>
);
if (type === 'chat-list') return frame(
<g>
<rect x="3" y="14" width="94" height="20" fill="var(--bg-2)"/>
<rect x="10" y="20" width="40" height="4" rx="2" fill="var(--ink-1)"/>
<circle cx="88" cy="24" r="6" fill="var(--accent)"/>
{[0,1,2,3].map((i) => (
<g key={i} transform={`translate(0, ${42 + i * 30})`}>
<circle cx="14" cy="14" r="8" fill={['var(--accent)', 'var(--blue)', 'var(--purple)', 'var(--ok)'][i]}/>
<rect x="26" y="8" width="40" height="3" rx="1.5" fill="var(--ink-1)"/>
<rect x="26" y="14" width="55" height="2" rx="1" fill="var(--ink-3)"/>
<rect x="72" y="6" width="14" height="2" rx="1" fill="var(--ink-4)"/>
{i === 0 && <circle cx="88" cy="18" r="4" fill="var(--accent)"/>}
</g>
))}
</g>
);
if (type === 'chat-detail') return frame(
<g>
<rect x="3" y="14" width="94" height="16" fill="var(--bg-2)"/>
<path d="M 10 22 l 4 -4 m -4 4 l 4 4" stroke="var(--accent)" strokeWidth="1.5" fill="none"/>
<circle cx="22" cy="22" r="5" fill="var(--purple)"/>
<rect x="32" y="18" width="28" height="3" rx="1.5" fill="var(--ink-1)"/>
<rect x="32" y="23" width="14" height="2" rx="1" fill="var(--ok)"/>
{/* messages */}
<rect x="10" y="40" width="50" height="14" rx="6" fill="var(--bg-2)" stroke="var(--border-2)" strokeWidth="0.5"/>
<rect x="40" y="60" width="50" height="14" rx="6" fill="var(--accent)"/>
<rect x="10" y="80" width="60" height="18" rx="6" fill="var(--bg-2)" stroke="var(--border-2)" strokeWidth="0.5"/>
<rect x="46" y="104" width="44" height="14" rx="6" fill="var(--accent)"/>
{/* composer */}
<rect x="3" y="156" width="94" height="20" fill="var(--bg-2)"/>
<rect x="20" y="162" width="55" height="9" rx="4" fill="var(--bg-3)" stroke="var(--border-2)" strokeWidth="0.5"/>
<circle cx="86" cy="167" r="5" fill="var(--accent)"/>
</g>
);
if (type === 'calendar') return frame(
<g>
<rect x="3" y="14" width="94" height="20" fill="var(--bg-2)"/>
<rect x="10" y="20" width="40" height="4" rx="2" fill="var(--ink-1)"/>
<circle cx="88" cy="24" r="6" fill="var(--accent)"/>
{/* day labels */}
{[0,1,2,3,4,5,6].map((d) => (
<text key={d} x={11 + d * 12} y="48" fontSize="3.5" fontFamily="JetBrains Mono" fill="var(--ink-3)">{['L','M','M','J','V','S','D'][d]}</text>
))}
{/* days */}
{Array.from({length:28}).map((_, i) => {
const x = 8 + (i % 7) * 12;
const y = 52 + Math.floor(i / 7) * 13;
const sel = i === 20;
const today = i === 7;
return (
<g key={i}>
<rect x={x} y={y} width="11" height="11" rx="2" fill={sel ? 'var(--accent)' : today ? 'var(--accent-tint)' : 'transparent'}/>
<text x={x + 5.5} y={y + 8} textAnchor="middle" fontSize="4" fontFamily="JetBrains Mono"
fill={sel ? 'var(--bg-1)' : 'var(--ink-1)'}>{i + 1}</text>
{[2,6,11,13,20].includes(i) && <circle cx={x + 5.5} cy={y + 11} r="1" fill={sel ? 'var(--bg-1)' : 'var(--accent)'}/>}
</g>
);
})}
<circle cx="84" cy="160" r="9" fill="var(--accent)"/>
<path d="M 81 160 L 87 160 M 84 157 L 84 163" stroke="var(--bg-1)" strokeWidth="1.5"/>
</g>
);
if (type === 'maps') return frame(
<g>
<rect x="3" y="14" width="94" height="16" fill="var(--bg-2)"/>
<rect x="10" y="18" width="80" height="9" rx="4" fill="var(--bg-3)" stroke="var(--border-2)" strokeWidth="0.5"/>
<rect x="3" y="30" width="94" height="118" fill="var(--bg-2)"/>
{/* routes */}
<path d="M 3 60 Q 50 50 97 90" stroke="var(--ink-4)" strokeWidth="2" fill="none" opacity="0.3"/>
<path d="M 30 30 Q 50 80 70 148" stroke="var(--ink-4)" strokeWidth="2" fill="none" opacity="0.3"/>
{/* pins */}
{[[30,55,'var(--accent)'],[60,80,'var(--ok)'],[75,45,'var(--blue)'],[22,110,'var(--err)']].map(([x,y,c], i) => (
<g key={i}>
<path d={`M ${x} ${y - 4} A 4 4 0 1 1 ${x - 0.01} ${y - 4} L ${x} ${y + 2} Z`} fill={c} stroke="var(--bg-1)" strokeWidth="0.5"/>
</g>
))}
{/* bottom card */}
<rect x="8" y="152" width="84" height="22" rx="5" fill="var(--bg-3)" stroke="var(--border-2)" strokeWidth="0.5"/>
<rect x="14" y="158" width="8" height="8" rx="2" fill="var(--accent)"/>
<rect x="26" y="159" width="30" height="3" rx="1" fill="var(--ink-1)"/>
<rect x="26" y="164" width="36" height="2" rx="1" fill="var(--ink-3)"/>
</g>
);
if (type === 'search') return frame(
<g>
<rect x="3" y="14" width="94" height="16" fill="var(--bg-2)"/>
<rect x="10" y="34" width="80" height="9" rx="4" fill="var(--bg-3)" stroke="var(--border-2)" strokeWidth="0.5"/>
{/* chips */}
<rect x="10" y="48" width="18" height="6" rx="3" fill="var(--accent)"/>
<rect x="31" y="48" width="18" height="6" rx="3" fill="var(--bg-3)" stroke="var(--border-2)" strokeWidth="0.5"/>
<rect x="52" y="48" width="18" height="6" rx="3" fill="var(--bg-3)" stroke="var(--border-2)" strokeWidth="0.5"/>
<rect x="73" y="48" width="18" height="6" rx="3" fill="var(--bg-3)" stroke="var(--border-2)" strokeWidth="0.5"/>
{/* results */}
{[0,1,2,3,4].map((i) => (
<g key={i} transform={`translate(0, ${62 + i * 22})`}>
<rect x="10" y="0" width="80" height="20" fill="var(--bg-3)" stroke="var(--border-2)" strokeWidth="0.4"/>
<circle cx="20" cy="10" r="5" fill={['var(--accent)','var(--blue)','var(--info)','var(--purple)','var(--ok)'][i]}/>
<rect x="30" y="6" width="35" height="3" rx="1" fill="var(--ink-1)"/>
<rect x="30" y="12" width="48" height="2" rx="1" fill="var(--ink-3)"/>
</g>
))}
</g>
);
if (type === 'scanner') return frame(
<g>
<rect x="3" y="14" width="94" height="164" fill="#15110c"/>
{/* coins */}
<path d="M 30 60 L 30 50 L 40 50" stroke="var(--accent)" strokeWidth="2.5" fill="none"/>
<path d="M 70 50 L 80 50 L 80 60" stroke="var(--accent)" strokeWidth="2.5" fill="none"/>
<path d="M 80 110 L 80 120 L 70 120" stroke="var(--accent)" strokeWidth="2.5" fill="none"/>
<path d="M 40 120 L 30 120 L 30 110" stroke="var(--accent)" strokeWidth="2.5" fill="none"/>
{/* scan line */}
<line x1="32" y1="80" x2="78" y2="80" stroke="var(--accent)" strokeWidth="1"
style={{filter:'drop-shadow(0 0 2px var(--accent))'}}/>
{/* shutter */}
<circle cx="50" cy="155" r="9" fill="var(--accent)" stroke="var(--bg-1)" strokeWidth="2"/>
</g>,
'#000'
);
if (type === 'camera') return frame(
<g>
<rect x="3" y="14" width="94" height="164" fill="#15110c"/>
<rect x="3" y="14" width="94" height="120" fill="url(#cam-bg)"/>
<defs>
<linearGradient id="cam-bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#6b4423"/>
<stop offset="1" stopColor="#2a1f15"/>
</linearGradient>
</defs>
{/* règle des tiers */}
<line x1="34" y1="14" x2="34" y2="134" stroke="#fff" strokeWidth="0.2" opacity="0.3"/>
<line x1="66" y1="14" x2="66" y2="134" stroke="#fff" strokeWidth="0.2" opacity="0.3"/>
<line x1="3" y1="54" x2="97" y2="54" stroke="#fff" strokeWidth="0.2" opacity="0.3"/>
<line x1="3" y1="94" x2="97" y2="94" stroke="#fff" strokeWidth="0.2" opacity="0.3"/>
{/* shutter */}
<circle cx="50" cy="158" r="11" fill="#fff" stroke="rgba(255,255,255,0.4)" strokeWidth="2"/>
<rect x="18" y="152" width="10" height="10" rx="2" fill="var(--accent-soft)"/>
<circle cx="78" cy="158" r="6" fill="none" stroke="#fff" strokeWidth="1"/>
</g>,
'#000'
);
if (type === 'files') return frame(
<g>
<rect x="3" y="14" width="94" height="20" fill="var(--bg-2)"/>
<rect x="10" y="20" width="40" height="4" rx="2" fill="var(--ink-1)"/>
{[
['folder','var(--accent)','Documents'],
['folder','var(--accent)','Photos'],
['list','var(--err)','rapport.pdf'],
['grid','var(--blue)','capture.png'],
['play','var(--purple)','meeting.mp4'],
['terminal','var(--ok)','memo.m4a'],
].map((it, i) => (
<g key={i} transform={`translate(0, ${42 + i * 22})`}>
<rect x="3" y="0" width="94" height="20" fill="var(--bg-3)"/>
<rect x="10" y="4" width="12" height="12" rx="2" fill="var(--bg-1)" stroke={it[1]} strokeWidth="0.6"/>
<rect x="26" y="6" width="50" height="3" rx="1" fill="var(--ink-1)"/>
<rect x="26" y="12" width="35" height="2" rx="1" fill="var(--ink-3)"/>
</g>
))}
<circle cx="84" cy="160" r="9" fill="var(--accent)"/>
<path d="M 81 160 L 87 160 M 84 157 L 84 163" stroke="var(--bg-1)" strokeWidth="1.5"/>
</g>
);
if (type === 'settings') return frame(
<g>
<rect x="3" y="14" width="94" height="20" fill="var(--bg-2)"/>
<rect x="10" y="20" width="40" height="4" rx="2" fill="var(--ink-1)"/>
<circle cx="88" cy="24" r="6" fill="var(--accent)"/>
{[
['user','var(--blue)','Compte'],
['cog','var(--ink-3)','Préférences'],
['bell','var(--accent)','Notifications', true],
['refresh','var(--ok)','Sync', true],
['folder','var(--purple)','Données'],
['power','var(--err)','Se déconnecter', null, true],
].map((it, i) => (
<g key={i} transform={`translate(0, ${42 + i * 20})`}>
<rect x="3" y="0" width="94" height="18" fill="var(--bg-3)"/>
<rect x="10" y="3" width="11" height="11" rx="2" fill={it[1]} opacity="0.85"/>
<rect x="25" y="6" width="40" height="3" rx="1" fill={it[4] ? 'var(--err)' : 'var(--ink-1)'}/>
{it[3] && <rect x="76" y="5" width="14" height="7" rx="3.5" fill="var(--accent)"/>}
</g>
))}
</g>
);
if (type === 'avatar-menu') return frame(
<g>
<rect x="3" y="14" width="94" height="20" fill="var(--bg-2)"/>
<circle cx="86" cy="24" r="7" fill="var(--accent)"/>
<text x="86" y="27" textAnchor="middle" fontSize="6" fontFamily="Inter" fontWeight="700" fill="var(--bg-1)">M</text>
{/* écran assombri derrière */}
<rect x="3" y="34" width="94" height="144" fill="#000" opacity="0.4"/>
{/* popup menu */}
<rect x="50" y="38" width="45" height="80" rx="6" fill="var(--bg-3)" stroke="var(--border-2)"/>
<rect x="50" y="38" width="45" height="14" rx="6" fill="var(--bg-2)"/>
<circle cx="58" cy="45" r="3.5" fill="var(--accent)"/>
<rect x="64" y="42" width="20" height="2.5" rx="1" fill="var(--ink-1)"/>
<rect x="64" y="47" width="14" height="2" rx="1" fill="var(--ink-3)"/>
{/* items */}
{[0,1,2,3].map((i) => (
<g key={i} transform={`translate(0, ${56 + i * 14})`}>
<line x1="50" y1="0" x2="95" y2="0" stroke="var(--border-1)" strokeWidth="0.4"/>
<circle cx="55" cy="7" r="2" fill="var(--accent)"/>
<rect x="60" y="6" width="25" height="2" rx="1" fill={i === 3 ? 'var(--err)' : 'var(--ink-1)'}/>
</g>
))}
</g>
);
return frame(null);
}
/* ============================================================
ScreenCard — carte de présentation d'un écran
============================================================ */
function ScreenCard({ type, name, when, why, components, gestures }) {
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>
{components && <div className="row-use"><span className="k">Composants</span><span className="v" style={{color:'var(--ink-3)', fontFamily:'var(--font-mono)', fontSize: 11.5}}>{components}</span></div>}
{gestures && <div className="row-use"><span className="k">Gestes</span><span className="v">{gestures}</span></div>}
</div>
</div>
</div>
);
}
/* ============================================================
NamedItem (avec preview live)
============================================================ */
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>
);
}
/* ============================================================
DOC PANEL
============================================================ */
function Doc() {
return (
<div className="doc">
<section>
<h2><Icon name="grid" size={22} style={{ color: 'var(--accent)' }} /> Patterns d'app courants</h2>
<p className="desc">
Ensemble de patterns que toute app mobile moderne utilise : <strong>onboarding,
chat, calendrier, maps, recherche, scanner, caméra, fichiers, paramètres</strong>,
plus le <strong>bouton avatar en haut à droite</strong> qui ouvre un menu utilisateur.
Chaque écran est navigable dans le smartphone à gauche.
</p>
</section>
<section id="screens">
<h2><Icon name="list" size={22} style={{ color: 'var(--accent)' }} /> 10 écrans modèles</h2>
<p className="desc">Bascule entre eux depuis l'Accueil du smartphone (grille d'icônes). L'écran <strong>Onboarding</strong> se lance automatiquement au premier passage.</p>
<ScreenCard type="onboarding" name="Onboarding"
when="Premier lancement de l'app, ou pour ré-expliquer une fonctionnalité importante."
why="Présente l'app en 2-4 slides illustrées avec pagination claire."
components="OnboardingSlider · AvatarLogo"
gestures="Tap sur les dots pour aller à une slide · bouton Suivant / Passer" />
<ScreenCard type="home" name="Accueil"
when="Page d'entrée après login. Donne accès à tous les autres écrans."
why="Grille d'ActionCards 3 colonnes : visuel immédiat, hit targets larges."
components="SearchBar · ActionCard · Avatar"
gestures="Tap sur une carte ouvre l'écran correspondant" />
<ScreenCard type="chat-list" name="Liste conversations"
when="L'utilisateur veut voir ses conversations récentes et en ouvrir une."
why="Liste swipeable avec avatar + nom + dernier message + heure + badge non lus."
components="Avatar · SwipeableRow · SearchBar"
gestures="SwipeLeft : archive/supprime · SwipeRight : marque lu/épingle · Tap : ouvre" />
<ScreenCard type="chat-detail" name="Conversation"
when="Échange de messages avec un contact."
why="Bulles différenciées (envoyé orange à droite, reçu sombre à gauche), composer en bas avec joindre/audio/envoyer."
components="ChatBubble · ChatComposer · Avatar"
gestures="Tap retour · Tap envoyer · Tap micro pour audio" />
<ScreenCard type="calendar" name="Calendrier"
when="Voir / créer des évènements datés."
why="Vue mois compacte avec points sous les jours marqués. Sélection met à jour la liste d'évènements du jour."
components="CalendarMonth · ListSection · FAB"
gestures="Tap sur une cellule · FAB pour créer · swipe (hors démo) pour changer de mois" />
<ScreenCard type="maps" name="Carte / Maps"
when="Voir des points d'intérêt géolocalisés ou se diriger."
why="Carte plein écran avec pins colorés + carte d'info en bas + boutons zoom flottants."
components="MapView · SearchBar · IconButton · glass-strong"
gestures="Pan pour déplacer · Pinch pour zoomer · Tap pin pour voir détails" />
<ScreenCard type="search" name="Recherche avancée"
when="L'utilisateur cherche dans tout le contenu de l'app."
why="SearchBar en haut + FilterChips horizontaux + liste de résultats unifiée toutes catégories."
components="SearchBar · FilterChips · ListRow"
gestures="Tap sur un chip filtre · scroll horizontal des chips" />
<ScreenCard type="scanner" name="Scanner QR / code-barres"
when="Lecture rapide d'un QR code ou code-barres pour récupérer une info."
why="Plein écran caméra avec viseur central animé (ligne scan) et boutons galerie/shutter/flash."
components="QrScannerView · IconButton"
gestures="Tap shutter pour capturer · auto-détection en vrai usage" />
<ScreenCard type="camera" name="Appareil photo"
when="Prendre une photo dans l'app."
why="Plein écran avec règle des tiers, sélecteur de mode (vidéo/photo/portrait), shutter rond classique."
components="CameraView · IconButton"
gestures="Tap shutter · swipe les modes en bas (hors démo) · Tap aperçu pour revoir" />
<ScreenCard type="files" name="Gestion de fichiers"
when="Parcourir, ouvrir, partager des fichiers."
why="Liste avec icône colorée par type + nom + taille + date. Actions cachées révélées au swipe."
components="FileExplorer (SwipeableRow + ListRow) · SearchBar · FAB"
gestures="SwipeLeft : supprimer · SwipeRight : renommer/partager · Tap : ouvrir" />
<ScreenCard type="settings" name="Paramètres"
when="Toujours accessible. Configuration globale + déconnexion."
why="Sections groupées (Compte, Préférences, Confidentialité, À propos) avec ListRow et toggles inline."
components="ListSection · ListRow · Toggle · BottomSheet"
gestures="Tap ligne · Toggle inline · Tap se déconnecter en bas (rouge)" />
</section>
<section id="avatar">
<h2><Icon name="user" size={22} style={{ color: 'var(--accent)' }} /> Bouton Avatar + menu utilisateur</h2>
<p className="desc">
Pattern récurrent : un <strong>bouton rond avec initiales</strong> en haut à droite de chaque écran.
Au tap, il ouvre un <strong>menu déroulant</strong> avec accès rapide à Profil, Paramètres, Aide et Déconnexion.
Pour le tester : tape sur le rond orange en haut à droite de n'importe quel écran (sauf Onboarding/Scanner/Caméra).
</p>
<ScreenCard type="avatar-menu" name="Avatar Menu"
when="L'utilisateur veut un accès rapide à son compte sans passer par un onglet dédié."
why="Toujours visible, identité claire (initiales + couleur), menu compact. Standard sur la plupart des apps web/mobiles."
components="Avatar · AvatarMenu" />
<NamedItem name="Avatar" location="En haut à droite de chaque écran"
desc="Bouton rond utilisateur. Affiche les initiales du nom avec un dégradé coloré. Accepte name, color, size, onClick, active (ring orange si actif). Pas d'image bitmap nécessaire — l'initial sur fond coloré reste lisible et identifiable."
preview={<div style={{display:'flex', gap: 12, justifyContent:'center'}}>
<Avatar name="Marc Dubois" color="var(--accent)" />
<Avatar name="Sophie M." color="var(--blue)" />
<Avatar name="Antoine" color="var(--ok)" />
<Avatar name="Léa" color="var(--purple)" active />
</div>} />
<NamedItem name="AvatarMenu" location="Popup descendant depuis l'Avatar"
desc="Menu déroulant affiché en haut à droite, avec header (avatar + nom + email) et liste d'actions. Anime à l'ouverture (drop-in + fade backdrop). Item avec danger:true s'affiche en rouge. Items typiques : Profil, Paramètres, Notifications, Aide, Se déconnecter."
preview={
<div style={{
width: '100%', maxWidth: 260,
background: 'var(--bg-3)', border: '1px solid var(--border-2)',
borderRadius: 12, overflow: 'hidden', margin: '0 auto',
}}>
<div style={{ padding: 12, display: 'flex', gap: 10, alignItems: 'center', borderBottom: '1px solid var(--border-1)', background: 'var(--bg-2)' }}>
<Avatar name="Marc" size={32} />
<div>
<div style={{ fontSize: 13, fontWeight: 700 }}>Marc Dupont</div>
<div style={{ fontSize: 10, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>marc@exemple.com</div>
</div>
</div>
{[
{ icon: 'user', label: 'Mon profil' },
{ icon: 'cog', label: 'Paramètres' },
{ icon: 'bell', label: 'Notifications' },
{ icon: 'power', label: 'Se déconnecter', danger: true },
].map((it, i) => (
<div key={i} style={{
padding: '8px 12px', display: 'flex', alignItems: 'center', gap: 8,
borderTop: i > 0 ? '1px solid var(--border-1)' : 'none',
color: it.danger ? 'var(--err)' : 'var(--ink-1)', fontSize: 13,
}}>
<Icon name={it.icon} size={13} style={{ color: it.danger ? 'var(--err)' : 'var(--accent)' }}/>
<span style={{ flex: 1 }}>{it.label}</span>
</div>
))}
</div>
} />
</section>
<section id="components">
<h2><Icon name="cog" size={22} style={{ color: 'var(--accent)' }} /> Nouveaux composants</h2>
<p className="desc">Tous accessibles globalement après chargement de <code className="mono" style={{color:'var(--accent)'}}>mobile-apps.jsx</code>.</p>
<NamedItem name="OnboardingSlider" location="Premier lancement"
desc="Slider plein-écran avec pagination (dots), bouton 'Passer' en haut, bouton 'Suivant' (puis 'Commencer') en bas. Prop slides = [{icon, color, title, desc}]."
preview={<div style={{display:'flex', justifyContent:'center', gap: 4}}>
{[0,1,2].map((i) => (
<span key={i} style={{ width: i === 0 ? 24 : 8, height: 8, borderRadius: 4, background: i === 0 ? 'var(--accent)' : 'var(--border-3)' }}/>
))}
</div>} />
<NamedItem name="ChatBubble" location="Conversation"
desc="Bulle de message. me:true => orange à droite avec coin bas-droit aplati. me:false => sombre à gauche avec coin bas-gauche aplati. Affiche heure + statut ( sent, read)."
preview={
<div>
<ChatBubble me={false} text="Salut, ça va ?" time="14:00" />
<ChatBubble me={true} text="Oui et toi !" time="14:01" status="read" />
</div>
} />
<NamedItem name="ChatComposer" location="Bas conversation"
desc="Barre d'envoi : bouton + (joindre) · input texte arrondi · bouton micro (vide) ou envoi (avec texte). Auto-bascule selon le contenu."
preview={<ChatComposer onSend={() => {}} />} />
<NamedItem name="CalendarMonth" location="Calendrier"
desc="Vue mois 7 colonnes. Aujourd'hui = fond accent-tint. Sélectionné = fond accent plein. Jours avec évènement = point sous le chiffre. Props : year, month (0-11), selected, onSelect, events (Set de numéros)."
preview={<div style={{maxWidth: 280, margin: '0 auto'}}>
<CalendarMonth year={2026} month={4} selected={new Date(2026, 4, 21)} onSelect={() => {}} events={new Set([3, 7, 14, 21])} />
</div>} />
<NamedItem name="MapView" location="Carte"
desc="Placeholder visuel de carte avec routes/zones stylisées + pins colorés. Props pins = [{x, y, color, icon, label}] (x/y en % du viewport)."
preview={<div style={{height: 140, width: '100%', border: '1px solid var(--border-2)', borderRadius: 8, overflow: 'hidden'}}>
<MapView pins={[
{ x: 30, y: 35, color: 'var(--accent)', icon: 'server', label: 'Ici' },
{ x: 70, y: 60, color: 'var(--ok)', icon: 'grid' },
]} />
</div>} />
<NamedItem name="FilterChips" location="Recherche"
desc="Liste horizontale de chips multi-sélectionnables. Scrollable. Options = liste de strings ou {value, label, icon}."
preview={<FilterChips value={['a']} onChange={() => {}} options={[
{ value: 'a', label: 'Tout', icon: 'grid' },
{ value: 'b', label: 'Personnes', icon: 'user' },
{ value: 'c', label: 'Fichiers', icon: 'folder' },
{ value: 'd', label: 'Messages', icon: 'bell' },
]} />} />
<NamedItem name="QrScannerView" location="Scanner"
desc="Plein écran caméra noir avec viseur carré central (4 coins angle accent) + ligne scan animée verticalement. Overlay assombri hors viseur. Boutons galerie / shutter / flash en bas."
preview={<div style={{height: 160, width: '100%', border: '1px solid var(--border-2)', borderRadius: 8, overflow: 'hidden'}}>
<QrScannerView onCapture={() => {}} />
</div>} />
<NamedItem name="CameraView" location="Appareil photo"
desc="Plein écran caméra avec règle des tiers, top bar (flash/timer/grille), sélecteur de mode (vidéo/photo/portrait), shutter rond blanc bas-centre."
preview={<div style={{height: 160, width: '100%', border: '1px solid var(--border-2)', borderRadius: 8, overflow: 'hidden'}}>
<CameraView onShoot={() => {}} />
</div>} />
<NamedItem name="FileExplorer" location="Fichiers"
desc="Liste de fichiers/dossiers. Chaque item a {name, type, date, size}. Type définit icône+couleur : folder/image/video/audio/pdf/code/archive. Wrappe chaque ligne dans SwipeableRow (renommer, partager, supprimer)."
preview={
<div style={{maxWidth: 320, margin: '0 auto', border: '1px solid var(--border-2)', borderRadius: 8, overflow: 'hidden'}}>
<FileExplorer items={[
{ name: 'Documents', type: 'folder', date: '21 mai' },
{ name: 'rapport.pdf', type: 'pdf', date: '20 mai', size: 384000 },
{ name: 'capture.png', type: 'image', date: '20 mai', size: 1280000 },
]} onOpen={() => {}} />
</div>
} />
</section>
<section id="checklist">
<h2><Icon name="download" size={22} style={{ color: 'var(--accent)' }} /> Antisèche — pattern par cas</h2>
<div className="card">
<div className="row-use"><span className="k">Premier lancement</span><span className="v">OnboardingSlider en plein écran, store flag "vu" en localStorage</span></div>
<div className="row-use"><span className="k">Liste à actions</span><span className="v">SwipeableRow avec leftActions (destructives) + rightActions (utiles)</span></div>
<div className="row-use"><span className="k">Identité utilisateur</span><span className="v">Avatar en haut à droite + AvatarMenu au tap → profil/réglages/déconnexion</span></div>
<div className="row-use"><span className="k">Recherche multi-types</span><span className="v">SearchBar + FilterChips + ListRow (un seul container pour tous types)</span></div>
<div className="row-use"><span className="k">Caméra/Scanner</span><span className="v">Plein écran sombre, header floating semi-transparent, contrôles flottants en bas</span></div>
<div className="row-use"><span className="k">Carte</span><span className="v">MapView + boutons flottants (zoom, position) + BottomSheet/glass-strong pour les détails du pin</span></div>
<div className="row-use"><span className="k">Réglages</span><span className="v">Empilement de ListSection avec en bas un ListRow danger "Se déconnecter"</span></div>
</div>
</section>
</div>
);
}
/* ============================================================
APP ROOT
============================================================ */
function PhoneAppApps({ theme }) {
const [screen, setScreen] = uAD('home');
const [openedChat, setOpenedChat] = uAD(null);
const [toast, setToast] = uAD(null);
const [avatarOpen, setAvatarOpen] = uAD(false);
const [sheet, setSheet] = uAD(null);
const showToast = (msg) => setToast(msg);
const goto = (s) => { setScreen(s); setOpenedChat(null); };
const onAvatar = () => setAvatarOpen(true);
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' }}>
{screen === 'onboard' && (
<OnboardingSlider slides={ONBOARDING_SLIDES} onFinish={() => goto('home')} />
)}
{screen === 'home' && <ScreenHome goto={goto} onAvatar={onAvatar} />}
{screen === 'chat' && !openedChat && <ScreenChatList onAvatar={onAvatar} open={setOpenedChat} />}
{screen === 'chat' && openedChat && <ScreenChatDetail chat={openedChat} onBack={() => setOpenedChat(null)} onAvatar={onAvatar} />}
{screen === 'calendar' && <ScreenCalendar onAvatar={onAvatar} showToast={showToast} />}
{screen === 'maps' && <ScreenMaps onAvatar={onAvatar} />}
{screen === 'search' && <ScreenSearch onBack={() => goto('home')} onAvatar={onAvatar} />}
{screen === 'scanner' && <ScreenScanner onBack={() => goto('home')} showToast={showToast} />}
{screen === 'camera' && <ScreenCamera onBack={() => goto('home')} showToast={showToast} />}
{screen === 'files' && <ScreenFiles onBack={() => goto('home')} onAvatar={onAvatar} showToast={showToast} />}
{screen === 'settings' && <ScreenSettings onBack={() => goto('home')} onAvatar={onAvatar} showToast={showToast} openSheet={setSheet} />}
</div>
{screen !== 'onboard' && screen !== 'scanner' && screen !== 'camera' && (
<TabBar
active={screen === 'chat' ? 'chat' : screen}
onSelect={(id) => { goto(id); }}
items={[
{ id: 'home', icon: 'grid', label: 'accueil' },
{ id: 'chat', icon: 'bell', label: 'chat' },
{ id: 'calendar', icon: 'clock', label: 'agenda' },
{ id: 'maps', icon: 'network', label: 'carte' },
]}
/>
)}
<AvatarMenu
open={avatarOpen}
onClose={() => setAvatarOpen(false)}
name="Marc Dupont"
email="marc@exemple.com"
items={[
{ icon: 'user', label: 'Mon profil', onClick: () => showToast('Profil') },
{ icon: 'cog', label: 'Paramètres', onClick: () => goto('settings') },
{ icon: 'bell', label: 'Notifications', onClick: () => showToast('Notifications') },
{ icon: 'folder', label: 'Mes fichiers', onClick: () => goto('files') },
{ icon: 'list', label: "Centre d'aide", onClick: () => showToast('Aide') },
{ icon: 'power', label: 'Se déconnecter', danger: true, onClick: () => goto('onboard') },
]}
/>
<BottomSheet open={sheet === 'theme'} onClose={() => setSheet(null)} title="Choisir le thème">
<SegmentedControl value="dark" onChange={() => {}} options={[
{ value: 'dark', label: 'Sombre', icon: 'moon' },
{ value: 'light', label: 'Clair', icon: 'sun' },
]} />
</BottomSheet>
<BottomSheet open={sheet === 'lang'} onClose={() => setSheet(null)} title="Langue">
<RadioGroup value="fr" onChange={() => {}} options={[
{ value: 'fr', label: 'Français' },
{ value: 'en', label: 'English' },
{ value: 'de', label: 'Deutsch' },
]} />
</BottomSheet>
<Toast open={toast !== null} onClose={() => setToast(null)} message={toast} variant="ok" />
</div>
);
}
function App() {
const [theme, setTheme] = uAD('dark');
const [device, setDevice] = uAD('ios');
eAD(() => { 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="grid" size={16} />
</div>
<h1>Exemple mobile · patterns <small>chat · calendrier · maps · scanner · caméra · fichiers · avatar menu</small></h1>
<span style={{ flex: 1 }}></span>
<a href="exemple-mobile-saisie.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 saisie</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 } : {}}>
<PhoneAppApps theme={theme} />
</div>
</div>
<div className="legend">↑ commence par l'accueil, puis explore les 9 autres écrans</div>
</div>
<Doc />
</div>
</React.Fragment>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);