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>
1117 lines
58 KiB
React
1117 lines
58 KiB
React
/* ============================================================
|
||
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"><{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>
|
||
);
|
||
}
|
||
|
||
/* ============================================================
|
||
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 />);
|