/* ============================================================ mobile-apps.jsx Composants pour patterns d'app courants : avatar+menu, onboarding, chat, calendrier, maps, recherche+filtres, scanner QR, caméra, gestion fichiers. ============================================================ */ const { useState: uA, useRef: rA, useEffect: eA } = React; /* ============================================================ Avatar — bouton rond utilisateur (initiales ou icône) Nom système : Avatar ============================================================ */ function Avatar({ name = 'M', color = 'var(--accent)', size = 36, onClick, active }) { const initials = name.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase(); return ( ); } /* ============================================================ AvatarMenu — popup descendant depuis l'avatar Nom système : AvatarMenu Items : [{icon, label, onClick, danger}] ============================================================ */ function AvatarMenu({ open, onClose, name, email, items = [] }) { if (!open) return null; return (
e.stopPropagation()} style={{ position: 'absolute', top: 56, right: 12, width: 240, background: 'var(--bg-3)', border: '1px solid var(--border-2)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 14px 32px rgba(0,0,0,0.5)', animation: 'drop-in .2s cubic-bezier(.3,.7,.3,1.2)', transformOrigin: 'top right', }}>
{name}
{email &&
{email}
}
{items.map((it, i) => ( ))}
); } /* ============================================================ OnboardingSlider — slides + dots + boutons suivant/passer Nom système : OnboardingSlider Cas : présentation d'une nouvelle app à l'utilisateur. slides : [{icon, color, title, desc}] ============================================================ */ function OnboardingSlider({ slides, onFinish }) { const [i, setI] = uA(0); const isLast = i === slides.length - 1; return (
{slides[i].title}
{slides[i].desc}
{slides.map((_, j) => ( setI(j)} style={{ width: i === j ? 24 : 8, height: 8, borderRadius: 4, background: i === j ? 'var(--accent)' : 'var(--border-3)', transition: 'width .25s, background .2s', cursor: 'pointer', }} /> ))}
isLast ? onFinish() : setI(i + 1)}> {isLast ? 'Commencer' : 'Suivant'}
); } /* ============================================================ ChatBubble — bulle de message (envoyé/reçu) Nom système : ChatBubble ============================================================ */ function ChatBubble({ text, time, me, status }) { return (
{text}
{time} {me && status === 'sent' && } {me && status === 'read' && ✓✓}
); } /* ============================================================ ChatComposer — barre d'envoi en bas (input + + + send) Nom système : ChatComposer ============================================================ */ function ChatComposer({ onSend }) { const [v, setV] = uA(''); return (
setV(e.target.value)} placeholder="Message…" style={{ flex: 1, minWidth: 0, background: 'transparent', border: 'none', outline: 'none', color: 'var(--ink-1)', fontFamily: 'var(--font-ui)', fontSize: 14, }} />
{v ? ( ) : ( )}
); } /* ============================================================ CalendarMonth — vue mois avec points sous les jours marqués Nom système : CalendarMonth Props : year, month (0-11), selected (Date), onSelect, events (Set de jours) ============================================================ */ function CalendarMonth({ year, month, selected, onSelect, events = new Set() }) { const today = new Date(); const first = new Date(year, month, 1); const last = new Date(year, month + 1, 0); const startDay = (first.getDay() + 6) % 7; // lundi = 0 const days = last.getDate(); const cells = []; for (let i = 0; i < startDay; i++) cells.push(null); for (let d = 1; d <= days; d++) cells.push(d); const monthName = first.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); return (
{monthName}
{['L', 'M', 'M', 'J', 'V', 'S', 'D'].map((d, i) => (
{d}
))} {cells.map((d, i) => { const isToday = d === today.getDate() && month === today.getMonth() && year === today.getFullYear(); const isSel = selected && d === selected.getDate() && month === selected.getMonth() && year === selected.getFullYear(); const hasEvent = d && events.has(d); return ( ); })}
); } /* ============================================================ MapView — placeholder visuel d'une carte avec pins Nom système : MapView ============================================================ */ function MapView({ pins = [] }) { return (
{/* fond carte stylisé */} {/* routes */} {/* zones */} {/* fleuve */} {/* pins */} {pins.map((p, i) => (
{p.label && (
{p.label}
)}
))}
); } /* ============================================================ FilterChips — barre de chips de filtre Nom système : FilterChips ============================================================ */ function FilterChips({ value = [], onChange, options }) { const toggle = (v) => { if (value.includes(v)) onChange(value.filter((x) => x !== v)); else onChange([...value, v]); }; return (
{options.map((o) => { const v = typeof o === 'string' ? o : o.value; const l = typeof o === 'string' ? o : o.label; const ic = typeof o === 'object' ? o.icon : null; const active = value.includes(v); return ( ); })}
); } /* ============================================================ QrScannerView — viseur scanner code-barres / QR Nom système : QrScannerView ============================================================ */ function QrScannerView({ onCapture }) { return (
{/* fake camera feed = grain animé */}
{/* visée centrale */}
{/* 4 coins */} {[ { top: 0, left: 0, br: '4px 0 0 0' }, { top: 0, right: 0, br: '0 4px 0 0' }, { bottom: 0, left: 0, br: '0 0 0 4px' }, { bottom: 0, right: 0, br: '0 0 4px 0' }, ].map((c, i) => (
))} {/* ligne scan animée */}
{/* overlay assombri hors visée */}
{/* texte */}
Pointe vers un QR code ou code-barres
{/* boutons bas */}
); } /* ============================================================ CameraView — viseur appareil photo avec shutter rond Nom système : CameraView ============================================================ */ function CameraView({ onShoot }) { return (
{/* fake scene */}
{/* règle des tiers */}
{[33.33, 66.66].map((p) => (
))}
{/* top bar */}
{[ { icon: 'moon', label: 'Flash' }, { icon: 'clock', label: 'Minuteur' }, { icon: 'grid', label: 'Grille' }, ].map((b) => ( ))}
{/* mode chips */}
Vidéo Photo Portrait
{/* bottom controls */}
); } /* ============================================================ FileExplorer — liste fichiers/dossiers Nom système : FileExplorer ============================================================ */ function FileExplorer({ items, onOpen, onAction }) { const sizeFmt = (b) => { if (b == null) return ''; if (b < 1024) return `${b} o`; if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} Ko`; if (b < 1024 ** 3) return `${(b / 1024 / 1024).toFixed(1)} Mo`; return `${(b / 1024 / 1024 / 1024).toFixed(1)} Go`; }; const typeIcon = (t) => ({ folder: 'folder', image: 'grid', video: 'play', audio: 'terminal', pdf: 'list', code: 'terminal', archive: 'download', file: 'list', })[t] || 'list'; const typeColor = (t) => ({ folder: 'var(--accent)', image: 'var(--blue)', video: 'var(--purple)', audio: 'var(--ok)', pdf: 'var(--err)', code: 'var(--info)', archive: 'var(--warn)', })[t] || 'var(--ink-3)'; return (
{items.map((it) => ( onOpen && onOpen(it)} leftActions={[ { label: 'Suppr.', icon: 'close', color: 'var(--err)', onClick: () => onAction && onAction('delete', it) }, ]} rightActions={[ { label: 'Renom.', icon: 'cog', color: 'var(--info)', onClick: () => onAction && onAction('rename', it) }, { label: 'Partag.', icon: 'download', color: 'var(--accent)', onClick: () => onAction && onAction('share', it) }, ]}>
{it.name}
{it.date || ''} {it.size != null && `· ${sizeFmt(it.size)}`}
{it.type === 'folder' && }
))}
); } Object.assign(window, { Avatar, AvatarMenu, OnboardingSlider, ChatBubble, ChatComposer, CalendarMonth, MapView, FilterChips, QrScannerView, CameraView, FileExplorer, });