/* ============================================================ mobile-sheets.jsx Types de fenêtres mobiles + composants spécifiques. Chaque type a un nom système ET un cas d'usage préconisé. ============================================================ */ const { useState: uS, useRef: rS, useEffect: eS } = React; /* ============================================================ BottomSheet — feuille modale qui monte du bas Nom système : BottomSheet Cas d'usage : action contextuelle, formulaire court, choix dans une liste. À privilégier sur mobile à la place d'une popup centrée (plus accessible au pouce). Gestes : swipe down pour fermer. ============================================================ */ function BottomSheet({ open, onClose, title, children, footer, height = 'auto' }) { const [dragY, setDragY] = uS(0); const [closing, setClosing] = uS(false); const startY = rS(0); eS(() => { if (open) { setDragY(0); setClosing(false); } }, [open]); if (!open && !closing) return null; const onStart = (e) => { startY.current = (e.touches ? e.touches[0].clientY : e.clientY); }; const onMove = (e) => { const y = (e.touches ? e.touches[0].clientY : e.clientY); const d = Math.max(0, y - startY.current); setDragY(d); }; const onEnd = () => { if (dragY > 80) { setClosing(true); setTimeout(() => { setClosing(false); setDragY(0); onClose(); }, 200); } else { setDragY(0); } }; return (
e.stopPropagation()} style={{ width: '100%', maxHeight: '85%', height: height === 'auto' ? 'auto' : height, background: 'var(--bg-2)', borderTop: '1px solid var(--border-2)', borderRadius: '20px 20px 0 0', boxShadow: '0 -8px 32px rgba(0,0,0,0.5)', transform: `translateY(${closing ? '100%' : dragY + 'px'})`, transition: closing ? 'transform .2s ease-in' : (dragY === 0 ? 'transform .3s cubic-bezier(.3,.7,.3,1.2)' : 'none'), display: 'flex', flexDirection: 'column', overflow: 'hidden', }}> {/* Drag handle */}
{title && (
{title}
)}
{children}
{footer && (
{footer}
)}
); } /* ============================================================ ActionSheet — menu d'actions style iOS Nom système : ActionSheet Cas d'usage : choix parmi 2-6 actions sur un élément (équivalent menu contextuel desktop). ============================================================ */ function ActionSheet({ open, onClose, title, actions, cancelLabel = 'Annuler' }) { if (!open) return null; return (
e.stopPropagation()} style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 8, animation: 'as-slide .25s cubic-bezier(.3,.7,.3,1.2)', }}>
{title && (
{title}
)} {actions.map((a, i) => ( ))}
); } /* ============================================================ AlertDialog — alerte modale centrée Nom système : AlertDialog Cas d'usage : message critique, demande de confirmation ferme (suppression, déconnexion). ============================================================ */ function AlertDialog({ open, onClose, icon, iconColor, title, message, actions }) { if (!open) return null; return (
{icon && (
)}
{title}
{message &&
{message}
}
{actions.map((a, i) => ( ))}
); } /* ============================================================ Toast — notification éphémère en haut Nom système : Toast Cas d'usage : feedback succès/erreur après une action. Disparaît automatiquement après 2.5s. ============================================================ */ function Toast({ open, onClose, icon, message, variant = 'ok', duration = 2500 }) { eS(() => { if (open) { const t = setTimeout(onClose, duration); return () => clearTimeout(t); } }, [open, duration, onClose]); if (!open) return null; const colors = { ok: { bg: 'var(--ok)', fg: 'var(--bg-1)', icon: 'play' }, warn: { bg: 'var(--warn)', fg: 'var(--bg-1)', icon: 'alert' }, err: { bg: 'var(--err)', fg: '#fff', icon: 'close' }, info: { bg: 'var(--info)', fg: 'var(--bg-1)', icon: 'bell' }, }[variant]; return (
{message}
); } /* ============================================================ FAB — Floating Action Button (Android Material) Nom système : FAB Cas d'usage : action principale unique sur un écran (créer, ajouter). Toujours en bas à droite. ============================================================ */ function FAB({ icon, label, onClick }) { return ( ); } /* ============================================================ PullToRefresh — wrapper pour rafraîchir au pull-down Nom système : PullToRefresh Geste associé : swipe down depuis le haut du contenu. ============================================================ */ function PullToRefresh({ onRefresh, children }) { const [pull, setPull] = uS(0); const [refreshing, setRefreshing] = uS(false); const startY = rS(0); const wrap = rS(); const onStart = (e) => { if (wrap.current && wrap.current.scrollTop === 0) { startY.current = e.touches[0].clientY; } else { startY.current = null; } }; const onMove = (e) => { if (startY.current == null) return; const d = e.touches[0].clientY - startY.current; if (d > 0) setPull(Math.min(d, 100)); }; const onEnd = async () => { if (pull > 60 && !refreshing) { setRefreshing(true); setPull(60); try { await Promise.resolve(onRefresh && onRefresh()); } finally { await new Promise((r) => setTimeout(r, 600)); setRefreshing(false); setPull(0); } } else { setPull(0); } startY.current = null; }; return (
{/* indicateur */}
{children}
); } Object.assign(window, { BottomSheet, ActionSheet, AlertDialog, Toast, FAB, PullToRefresh, });