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>
660 lines
27 KiB
React
660 lines
27 KiB
React
/* ============================================================
|
|
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 (
|
|
<button onClick={onClick} className="touch-press" style={{
|
|
width: size, height: size, borderRadius: '50%',
|
|
background: `linear-gradient(135deg, ${color}, color-mix(in oklch, ${color} 60%, black))`,
|
|
color: 'var(--bg-1)',
|
|
border: active ? '2px solid var(--accent)' : 'none',
|
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
fontFamily: 'var(--font-ui)', fontSize: size * 0.4, fontWeight: 700,
|
|
cursor: 'pointer',
|
|
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.25), 0 2px 6px rgba(0,0,0,0.3)',
|
|
WebkitTapHighlightColor: 'transparent',
|
|
}}>{initials}</button>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
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 (
|
|
<div onClick={onClose} style={{
|
|
position: 'absolute', inset: 0, zIndex: 200,
|
|
background: 'rgba(0,0,0,0.35)',
|
|
animation: 'fade-in .15s',
|
|
}}>
|
|
<style>{`
|
|
@keyframes fade-in { from { opacity: 0 } to { opacity: 1 } }
|
|
@keyframes drop-in { from { opacity: 0; transform: translateY(-8px) scale(.95) } to { opacity: 1; transform: translateY(0) scale(1) } }
|
|
`}</style>
|
|
<div onClick={(e) => 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',
|
|
}}>
|
|
<div style={{
|
|
padding: '14px 14px 12px',
|
|
display: 'flex', alignItems: 'center', gap: 10,
|
|
borderBottom: '1px solid var(--border-1)',
|
|
background: 'var(--bg-2)',
|
|
}}>
|
|
<Avatar name={name} size={36} />
|
|
<div style={{ minWidth: 0, flex: 1 }}>
|
|
<div style={{ fontSize: 14, fontWeight: 700 }}>{name}</div>
|
|
{email && <div style={{ fontSize: 11, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>{email}</div>}
|
|
</div>
|
|
</div>
|
|
{items.map((it, i) => (
|
|
<button key={i} onClick={() => { onClose(); it.onClick && it.onClick(); }}
|
|
className="touch-press" style={{
|
|
width: '100%', minHeight: 44,
|
|
padding: '10px 14px',
|
|
background: 'transparent', border: 'none',
|
|
borderTop: i > 0 ? '1px solid var(--border-1)' : 'none',
|
|
color: it.danger ? 'var(--err)' : 'var(--ink-1)',
|
|
display: 'flex', alignItems: 'center', gap: 10,
|
|
fontFamily: 'var(--font-ui)', fontSize: 14, fontWeight: 500,
|
|
cursor: 'pointer', textAlign: 'left',
|
|
WebkitTapHighlightColor: 'transparent',
|
|
}}>
|
|
<Icon name={it.icon} size={15} style={{ color: it.danger ? 'var(--err)' : 'var(--accent)' }} />
|
|
<span style={{ flex: 1 }}>{it.label}</span>
|
|
{!it.danger && <Icon name="chevR" size={12} style={{ color: 'var(--ink-3)' }} />}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
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 (
|
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
<div style={{
|
|
padding: '14px 20px',
|
|
display: 'flex', justifyContent: 'flex-end',
|
|
}}>
|
|
<button onClick={onFinish} style={{
|
|
padding: '6px 12px', background: 'transparent', border: 'none',
|
|
color: 'var(--ink-3)', fontFamily: 'var(--font-ui)',
|
|
fontWeight: 600, fontSize: 14, cursor: 'pointer',
|
|
WebkitTapHighlightColor: 'transparent',
|
|
}}>Passer</button>
|
|
</div>
|
|
<div style={{
|
|
flex: 1, padding: '0 32px',
|
|
display: 'flex', flexDirection: 'column',
|
|
alignItems: 'center', justifyContent: 'center',
|
|
textAlign: 'center',
|
|
}}>
|
|
<div style={{
|
|
width: 110, height: 110, borderRadius: 28,
|
|
background: `linear-gradient(135deg, ${slides[i].color}, color-mix(in oklch, ${slides[i].color} 60%, black))`,
|
|
color: 'var(--bg-1)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
marginBottom: 28,
|
|
boxShadow: `inset 0 2px 0 rgba(255,255,255,0.2), 0 12px 28px rgba(0,0,0,0.4)`,
|
|
animation: 'pop-in .35s cubic-bezier(.3,.7,.3,1.3)',
|
|
}}>
|
|
<style>{`@keyframes pop-in { from { transform: scale(.7); opacity: 0 } }`}</style>
|
|
<Icon name={slides[i].icon} size={56} />
|
|
</div>
|
|
<div style={{ fontSize: 26, fontWeight: 700, marginBottom: 12 }}>{slides[i].title}</div>
|
|
<div style={{ fontSize: 15, color: 'var(--ink-3)', lineHeight: 1.5, maxWidth: 280 }}>{slides[i].desc}</div>
|
|
</div>
|
|
<div style={{ padding: '20px 24px 30px' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'center', gap: 8, marginBottom: 22 }}>
|
|
{slides.map((_, j) => (
|
|
<span key={j} onClick={() => 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',
|
|
}} />
|
|
))}
|
|
</div>
|
|
<PrimaryButton icon={isLast ? 'play' : 'chevR'}
|
|
onClick={() => isLast ? onFinish() : setI(i + 1)}>
|
|
{isLast ? 'Commencer' : 'Suivant'}
|
|
</PrimaryButton>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
ChatBubble — bulle de message (envoyé/reçu)
|
|
Nom système : ChatBubble
|
|
============================================================ */
|
|
function ChatBubble({ text, time, me, status }) {
|
|
return (
|
|
<div style={{
|
|
display: 'flex',
|
|
justifyContent: me ? 'flex-end' : 'flex-start',
|
|
padding: '4px 14px',
|
|
}}>
|
|
<div style={{
|
|
maxWidth: '78%',
|
|
padding: '8px 12px',
|
|
background: me ? 'var(--accent)' : 'var(--bg-3)',
|
|
color: me ? 'var(--bg-1)' : 'var(--ink-1)',
|
|
borderRadius: me ? '16px 16px 4px 16px' : '16px 16px 16px 4px',
|
|
fontSize: 14, lineHeight: 1.4,
|
|
boxShadow: me ? '0 2px 6px var(--accent-glow)' : 'var(--shadow-1)',
|
|
border: me ? 'none' : '1px solid var(--border-2)',
|
|
}}>
|
|
<div>{text}</div>
|
|
<div style={{
|
|
fontSize: 10,
|
|
color: me ? 'rgba(0,0,0,0.55)' : 'var(--ink-3)',
|
|
marginTop: 4, textAlign: 'right',
|
|
fontFamily: 'var(--font-mono)',
|
|
display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
float: 'right',
|
|
}}>
|
|
{time}
|
|
{me && status === 'sent' && <span>✓</span>}
|
|
{me && status === 'read' && <span>✓✓</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
ChatComposer — barre d'envoi en bas (input + + + send)
|
|
Nom système : ChatComposer
|
|
============================================================ */
|
|
function ChatComposer({ onSend }) {
|
|
const [v, setV] = uA('');
|
|
return (
|
|
<div style={{
|
|
padding: '8px 10px 18px',
|
|
display: 'flex', alignItems: 'flex-end', gap: 8,
|
|
borderTop: '1px solid var(--border-2)',
|
|
background: 'var(--surf-glass-strong)',
|
|
backdropFilter: 'blur(14px)',
|
|
}}>
|
|
<IconButton icon="plus" label="Joindre" size={36} />
|
|
<div style={{
|
|
flex: 1, minHeight: 36,
|
|
display: 'flex', alignItems: 'center', gap: 6,
|
|
padding: '6px 12px',
|
|
background: 'var(--bg-3)',
|
|
border: '1px solid var(--border-2)',
|
|
borderRadius: 18,
|
|
}}>
|
|
<input type="text" value={v} onChange={(e) => 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,
|
|
}} />
|
|
</div>
|
|
{v ? (
|
|
<button onClick={() => { onSend && onSend(v); setV(''); }}
|
|
className="touch-press" style={{
|
|
width: 36, height: 36, borderRadius: '50%',
|
|
background: 'var(--accent)', color: 'var(--bg-1)',
|
|
border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
cursor: 'pointer', boxShadow: '0 2px 6px var(--accent-glow)',
|
|
WebkitTapHighlightColor: 'transparent',
|
|
}}><Icon name="chevR" size={16} /></button>
|
|
) : (
|
|
<IconButton icon="terminal" label="Audio" size={36} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
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 (
|
|
<div>
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
padding: '0 14px 12px',
|
|
}}>
|
|
<IconButton icon="chevL" label="Mois précédent" size={32} />
|
|
<div style={{ fontSize: 16, fontWeight: 700, textTransform: 'capitalize' }}>{monthName}</div>
|
|
<IconButton icon="chevR" label="Mois suivant" size={32} />
|
|
</div>
|
|
<div style={{
|
|
display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 4,
|
|
padding: '0 8px',
|
|
}}>
|
|
{['L', 'M', 'M', 'J', 'V', 'S', 'D'].map((d, i) => (
|
|
<div key={i} style={{
|
|
textAlign: 'center', fontSize: 10,
|
|
color: 'var(--ink-3)', fontFamily: 'var(--font-mono)',
|
|
fontWeight: 700, padding: '4px 0',
|
|
letterSpacing: '0.08em',
|
|
}}>{d}</div>
|
|
))}
|
|
{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 (
|
|
<button key={i} onClick={() => d && onSelect && onSelect(new Date(year, month, d))}
|
|
disabled={!d}
|
|
className="touch-press"
|
|
style={{
|
|
aspectRatio: '1',
|
|
background: isSel ? 'var(--accent)' : isToday ? 'var(--accent-tint)' : 'transparent',
|
|
color: isSel ? 'var(--bg-1)' : isToday ? 'var(--accent)' : (d ? 'var(--ink-1)' : 'transparent'),
|
|
border: 'none', borderRadius: 8,
|
|
fontFamily: 'var(--font-mono)', fontSize: 13,
|
|
fontWeight: isSel || isToday ? 700 : 500,
|
|
cursor: d ? 'pointer' : 'default',
|
|
position: 'relative',
|
|
WebkitTapHighlightColor: 'transparent',
|
|
}}>
|
|
{d}
|
|
{hasEvent && (
|
|
<span style={{
|
|
position: 'absolute', bottom: 4, left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
width: 4, height: 4, borderRadius: '50%',
|
|
background: isSel ? 'var(--bg-1)' : 'var(--accent)',
|
|
}}/>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
MapView — placeholder visuel d'une carte avec pins
|
|
Nom système : MapView
|
|
============================================================ */
|
|
function MapView({ pins = [] }) {
|
|
return (
|
|
<div style={{
|
|
position: 'relative',
|
|
height: '100%', width: '100%',
|
|
background: 'var(--bg-2)',
|
|
overflow: 'hidden',
|
|
}}>
|
|
{/* fond carte stylisé */}
|
|
<svg width="100%" height="100%" viewBox="0 0 400 600" preserveAspectRatio="xMidYMid slice" style={{ position: 'absolute', inset: 0 }}>
|
|
<defs>
|
|
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
|
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="var(--border-1)" strokeWidth="0.5"/>
|
|
</pattern>
|
|
</defs>
|
|
<rect width="100%" height="100%" fill="url(#grid)"/>
|
|
{/* routes */}
|
|
<path d="M 0 200 Q 200 150 400 250" stroke="var(--ink-4)" strokeWidth="6" fill="none" opacity="0.3"/>
|
|
<path d="M 100 0 Q 150 200 200 400 T 250 600" stroke="var(--ink-4)" strokeWidth="6" fill="none" opacity="0.3"/>
|
|
<path d="M 200 100 L 350 500" stroke="var(--ink-4)" strokeWidth="4" fill="none" opacity="0.25"/>
|
|
{/* zones */}
|
|
<path d="M 0 0 L 150 0 L 100 120 L 0 100 Z" fill="var(--bg-3)" opacity="0.5"/>
|
|
<path d="M 280 350 L 400 380 L 400 550 L 320 600 L 250 500 Z" fill="var(--bg-3)" opacity="0.4"/>
|
|
<circle cx="240" cy="380" r="60" fill="var(--ok)" opacity="0.12"/>
|
|
{/* fleuve */}
|
|
<path d="M 0 450 Q 100 420 200 460 T 400 440" stroke="var(--info)" strokeWidth="10" fill="none" opacity="0.4"/>
|
|
</svg>
|
|
{/* pins */}
|
|
{pins.map((p, i) => (
|
|
<div key={i} style={{
|
|
position: 'absolute', left: `${p.x}%`, top: `${p.y}%`,
|
|
transform: 'translate(-50%, -100%)',
|
|
pointerEvents: 'none',
|
|
}}>
|
|
<div style={{
|
|
width: 28, height: 28, borderRadius: '50% 50% 50% 0',
|
|
background: p.color || 'var(--accent)',
|
|
transform: 'rotate(-45deg)',
|
|
border: '2px solid var(--bg-1)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
boxShadow: '0 4px 10px rgba(0,0,0,0.5)',
|
|
}}>
|
|
<Icon name={p.icon || 'grid'} size={12} style={{ color: 'var(--bg-1)', transform: 'rotate(45deg)' }}/>
|
|
</div>
|
|
{p.label && (
|
|
<div style={{
|
|
position: 'absolute', top: -28, left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
padding: '3px 8px',
|
|
background: 'var(--bg-3)',
|
|
border: '1px solid var(--border-2)',
|
|
borderRadius: 6,
|
|
fontFamily: 'var(--font-mono)', fontSize: 10,
|
|
color: 'var(--ink-1)',
|
|
whiteSpace: 'nowrap',
|
|
boxShadow: 'var(--shadow-2)',
|
|
}}>{p.label}</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
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 (
|
|
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', padding: '4px 0', WebkitOverflowScrolling: 'touch' }}>
|
|
{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 (
|
|
<button key={v} onClick={() => toggle(v)} className="touch-press" style={{
|
|
flex: '0 0 auto',
|
|
padding: '6px 12px',
|
|
background: active ? 'var(--accent)' : 'var(--bg-3)',
|
|
color: active ? 'var(--bg-1)' : 'var(--ink-2)',
|
|
border: `1px solid ${active ? 'var(--accent)' : 'var(--border-2)'}`,
|
|
borderRadius: 999,
|
|
display: 'inline-flex', alignItems: 'center', gap: 6,
|
|
cursor: 'pointer',
|
|
fontFamily: 'var(--font-ui)', fontSize: 12, fontWeight: 600,
|
|
WebkitTapHighlightColor: 'transparent',
|
|
}}>
|
|
{ic && <Icon name={ic} size={12} />}
|
|
{l}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
QrScannerView — viseur scanner code-barres / QR
|
|
Nom système : QrScannerView
|
|
============================================================ */
|
|
function QrScannerView({ onCapture }) {
|
|
return (
|
|
<div style={{
|
|
position: 'relative', width: '100%', height: '100%',
|
|
background: '#000',
|
|
overflow: 'hidden',
|
|
}}>
|
|
{/* fake camera feed = grain animé */}
|
|
<div style={{
|
|
position: 'absolute', inset: 0,
|
|
background: `
|
|
radial-gradient(ellipse at 30% 40%, rgba(80,60,40,0.4), transparent 60%),
|
|
radial-gradient(ellipse at 70% 60%, rgba(40,40,60,0.5), transparent 50%),
|
|
#15110c
|
|
`,
|
|
}}/>
|
|
{/* visée centrale */}
|
|
<div style={{
|
|
position: 'absolute', top: '50%', left: '50%',
|
|
transform: 'translate(-50%, -50%)',
|
|
width: 220, height: 220,
|
|
}}>
|
|
{/* 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) => (
|
|
<div key={i} style={{
|
|
position: 'absolute', ...c, width: 28, height: 28,
|
|
borderTop: c.top !== undefined ? '3px solid var(--accent)' : 'none',
|
|
borderBottom: c.bottom !== undefined ? '3px solid var(--accent)' : 'none',
|
|
borderLeft: c.left !== undefined ? '3px solid var(--accent)' : 'none',
|
|
borderRight: c.right !== undefined ? '3px solid var(--accent)' : 'none',
|
|
borderRadius: c.br,
|
|
}}/>
|
|
))}
|
|
{/* ligne scan animée */}
|
|
<div style={{
|
|
position: 'absolute', left: 6, right: 6, height: 2,
|
|
background: 'linear-gradient(90deg, transparent, var(--accent), transparent)',
|
|
boxShadow: '0 0 12px var(--accent), 0 0 20px var(--accent)',
|
|
animation: 'qr-scan 2.4s ease-in-out infinite',
|
|
}}/>
|
|
<style>{`@keyframes qr-scan {
|
|
0%, 100% { top: 6px; opacity: 1 }
|
|
50% { top: calc(100% - 8px); opacity: 0.7 }
|
|
}`}</style>
|
|
</div>
|
|
{/* overlay assombri hors visée */}
|
|
<div style={{
|
|
position: 'absolute', inset: 0,
|
|
boxShadow: '0 0 0 9999px rgba(0,0,0,0.55) inset',
|
|
clipPath: 'polygon(0% 0%, 0% 100%, 100% 100%, 100% 0%, calc(50% + 110px) 0%, calc(50% + 110px) calc(50% + 110px), calc(50% - 110px) calc(50% + 110px), calc(50% - 110px) calc(50% - 110px), calc(50% + 110px) calc(50% - 110px), calc(50% + 110px) 0%)',
|
|
pointerEvents: 'none',
|
|
}}/>
|
|
{/* texte */}
|
|
<div style={{
|
|
position: 'absolute', top: 'calc(50% + 140px)', left: 0, right: 0,
|
|
textAlign: 'center', color: 'var(--ink-1)',
|
|
fontFamily: 'var(--font-ui)', fontSize: 14, fontWeight: 600,
|
|
}}>Pointe vers un QR code ou code-barres</div>
|
|
{/* boutons bas */}
|
|
<div style={{
|
|
position: 'absolute', bottom: 28, left: 0, right: 0,
|
|
display: 'flex', justifyContent: 'space-around', alignItems: 'center',
|
|
}}>
|
|
<IconButton icon="folder" label="Galerie" size={44} />
|
|
<button onClick={() => onCapture && onCapture('demo')} className="touch-press" style={{
|
|
width: 70, height: 70, borderRadius: '50%',
|
|
background: 'var(--accent)', border: '4px solid #fff',
|
|
color: 'var(--bg-1)', cursor: 'pointer',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
boxShadow: '0 4px 12px var(--accent-glow)',
|
|
WebkitTapHighlightColor: 'transparent',
|
|
}}><Icon name="grid" size={26} /></button>
|
|
<IconButton icon="moon" label="Flash" size={44} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
CameraView — viseur appareil photo avec shutter rond
|
|
Nom système : CameraView
|
|
============================================================ */
|
|
function CameraView({ onShoot }) {
|
|
return (
|
|
<div style={{
|
|
position: 'relative', width: '100%', height: '100%',
|
|
background: '#000', overflow: 'hidden',
|
|
}}>
|
|
{/* fake scene */}
|
|
<div style={{
|
|
position: 'absolute', inset: 0,
|
|
background: `
|
|
linear-gradient(180deg, #4a2e1a 0%, #6b4423 30%, #2a1f15 70%, #15110c 100%),
|
|
radial-gradient(circle at 50% 30%, rgba(254,128,25,0.3), transparent 50%)
|
|
`,
|
|
backgroundBlendMode: 'overlay',
|
|
}}/>
|
|
{/* règle des tiers */}
|
|
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
|
|
{[33.33, 66.66].map((p) => (
|
|
<React.Fragment key={p}>
|
|
<div style={{ position:'absolute', left:0, right:0, top:`${p}%`, height:1, background:'rgba(255,255,255,0.2)' }}/>
|
|
<div style={{ position:'absolute', top:0, bottom:0, left:`${p}%`, width:1, background:'rgba(255,255,255,0.2)' }}/>
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
{/* top bar */}
|
|
<div style={{
|
|
position: 'absolute', top: 20, left: 0, right: 0,
|
|
display: 'flex', justifyContent: 'space-around',
|
|
padding: '0 16px',
|
|
}}>
|
|
{[
|
|
{ icon: 'moon', label: 'Flash' },
|
|
{ icon: 'clock', label: 'Minuteur' },
|
|
{ icon: 'grid', label: 'Grille' },
|
|
].map((b) => (
|
|
<IconButton key={b.label} icon={b.icon} label={b.label} size={36} />
|
|
))}
|
|
</div>
|
|
{/* mode chips */}
|
|
<div style={{
|
|
position: 'absolute', bottom: 130, left: 0, right: 0,
|
|
display: 'flex', justifyContent: 'center', gap: 20,
|
|
color: 'var(--ink-2)', fontFamily: 'var(--font-mono)', fontSize: 12,
|
|
letterSpacing: '0.08em', textTransform: 'uppercase',
|
|
}}>
|
|
<span style={{ opacity: 0.5 }}>Vidéo</span>
|
|
<span style={{ color: 'var(--accent)', fontWeight: 700 }}>Photo</span>
|
|
<span style={{ opacity: 0.5 }}>Portrait</span>
|
|
</div>
|
|
{/* bottom controls */}
|
|
<div style={{
|
|
position: 'absolute', bottom: 28, left: 0, right: 0,
|
|
display: 'flex', justifyContent: 'space-around', alignItems: 'center',
|
|
}}>
|
|
<div style={{
|
|
width: 50, height: 50, borderRadius: 10,
|
|
background: 'linear-gradient(135deg, #6b4423, #2a1f15)',
|
|
border: '2px solid #fff',
|
|
}}/>
|
|
<button onClick={() => onShoot && onShoot()} className="touch-press" style={{
|
|
width: 76, height: 76, borderRadius: '50%',
|
|
background: '#fff', border: '4px solid rgba(255,255,255,0.4)',
|
|
cursor: 'pointer',
|
|
boxShadow: '0 0 0 4px rgba(0,0,0,0.4)',
|
|
WebkitTapHighlightColor: 'transparent',
|
|
}}/>
|
|
<IconButton icon="refresh" label="Caméra avant" size={44} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
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 (
|
|
<div>
|
|
{items.map((it) => (
|
|
<SwipeableRow key={it.name}
|
|
onTap={() => 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) },
|
|
]}>
|
|
<div style={{
|
|
padding: '12px 14px',
|
|
display: 'flex', alignItems: 'center', gap: 12,
|
|
borderBottom: '1px solid var(--border-1)',
|
|
background: 'var(--bg-3)',
|
|
}}>
|
|
<span style={{
|
|
width: 38, height: 38, borderRadius: 8,
|
|
background: 'var(--bg-1)',
|
|
border: `1px solid ${typeColor(it.type)}`,
|
|
color: typeColor(it.type),
|
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
}}>
|
|
<Icon name={typeIcon(it.type)} size={17} />
|
|
</span>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{ fontSize: 14, fontWeight: 500, color: 'var(--ink-1)',
|
|
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{it.name}</div>
|
|
<div style={{ fontSize: 11, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)', marginTop: 2 }}>
|
|
{it.date || ''} {it.size != null && `· ${sizeFmt(it.size)}`}
|
|
</div>
|
|
</div>
|
|
{it.type === 'folder' && <Icon name="chevR" size={13} style={{ color: 'var(--ink-3)' }}/>}
|
|
</div>
|
|
</SwipeableRow>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
Object.assign(window, {
|
|
Avatar, AvatarMenu,
|
|
OnboardingSlider,
|
|
ChatBubble, ChatComposer,
|
|
CalendarMonth,
|
|
MapView,
|
|
FilterChips,
|
|
QrScannerView, CameraView,
|
|
FileExplorer,
|
|
});
|