Files
home_hub/design_system/package-smartphone/examples/mobile-swipeable.jsx
T
gilles 4518ed8311 chore(design): ajout du package design system smartphone
Contient les tokens, composants et exemples adaptés au mobile,
à utiliser comme référence lors du développement des vues smartphone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 08:53:36 +02:00

138 lines
4.9 KiB
React

/* ============================================================
mobile-swipeable.jsx
SwipeableRow — ligne qui révèle des actions au swipe.
============================================================ */
const { useState: uSw, useRef: rSw, useEffect: eSw } = React;
/* ============================================================
SwipeableRow
Nom système : SwipeableRow
Cas d'usage : ligne d'une liste avec actions cachées
(archive, suppression, marquer comme lu…).
Style iOS Mail / Things / Apple Reminders.
Gestes : SwipeLeft (révèle leftActions à droite),
SwipeRight (révèle rightActions à gauche),
Tap sur la ligne (action principale),
Tap sur une action (déclenche l'action puis ferme).
============================================================ */
function SwipeableRow({ children, leftActions = [], rightActions = [], onTap }) {
// leftActions s'affichent quand on swipe vers la GAUCHE
// (la ligne se décale à gauche, dévoilant les actions à DROITE)
const [tx, setTx] = uSw(0);
const [dragging, setDragging] = uSw(false);
const startX = rSw(0);
const initialTx = rSw(0);
const leftW = leftActions.length * 76; // actions à droite (révélées par swipe gauche)
const rightW = rightActions.length * 76; // actions à gauche (révélées par swipe droit)
const snap = (x) => {
if (x < -leftW * 0.5) setTx(-leftW);
else if (x > rightW * 0.5) setTx(rightW);
else setTx(0);
};
const onStart = (e) => {
setDragging(true);
startX.current = (e.touches ? e.touches[0].clientX : e.clientX);
initialTx.current = tx;
};
const onMove = (e) => {
if (!dragging) return;
const x = (e.touches ? e.touches[0].clientX : e.clientX);
let d = initialTx.current + (x - startX.current);
// limite + élasticité hors zone
if (d > rightW) d = rightW + (d - rightW) * 0.3;
if (d < -leftW) d = -leftW + (d + leftW) * 0.3;
setTx(d);
};
const onEnd = () => {
setDragging(false);
snap(tx);
};
const fire = (action) => {
setTx(0);
setTimeout(() => action.onClick && action.onClick(), 200);
};
const handleTap = (e) => {
if (tx !== 0) { setTx(0); return; }
if (Math.abs(tx) < 4 && onTap) onTap(e);
};
return (
<div style={{
position: 'relative',
overflow: 'hidden',
background: 'var(--bg-3)',
WebkitUserSelect: 'none', userSelect: 'none',
}}>
{/* Actions à GAUCHE (révélées par swipe droit) */}
{rightActions.length > 0 && (
<div style={{
position: 'absolute', left: 0, top: 0, bottom: 0,
display: 'flex', alignItems: 'stretch',
width: rightW,
}}>
{rightActions.map((a, i) => (
<button key={i} onClick={() => fire(a)} className="touch-press" style={{
width: 76,
background: a.color || 'var(--info)',
color: a.fg || '#fff',
border: 'none', cursor: 'pointer',
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4,
fontFamily: 'var(--font-ui)', fontSize: 12, fontWeight: 600,
WebkitTapHighlightColor: 'transparent',
}}>
{a.icon && <Icon name={a.icon} size={20} />}
{a.label}
</button>
))}
</div>
)}
{/* Actions à DROITE (révélées par swipe gauche) */}
{leftActions.length > 0 && (
<div style={{
position: 'absolute', right: 0, top: 0, bottom: 0,
display: 'flex', alignItems: 'stretch',
width: leftW,
}}>
{leftActions.map((a, i) => (
<button key={i} onClick={() => fire(a)} className="touch-press" style={{
width: 76,
background: a.color || 'var(--err)',
color: a.fg || '#fff',
border: 'none', cursor: 'pointer',
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4,
fontFamily: 'var(--font-ui)', fontSize: 12, fontWeight: 600,
WebkitTapHighlightColor: 'transparent',
}}>
{a.icon && <Icon name={a.icon} size={20} />}
{a.label}
</button>
))}
</div>
)}
{/* Ligne déplaçable */}
<div
onTouchStart={onStart} onTouchMove={onMove} onTouchEnd={onEnd}
onMouseDown={onStart} onMouseMove={onMove} onMouseUp={onEnd} onMouseLeave={onEnd}
onClick={handleTap}
style={{
position: 'relative',
background: 'var(--bg-3)',
transform: `translateX(${tx}px)`,
transition: dragging ? 'none' : 'transform .25s cubic-bezier(.3,.7,.3,1.1)',
cursor: dragging ? 'grabbing' : (onTap ? 'pointer' : 'default'),
touchAction: 'pan-y',
}}>
{children}
</div>
</div>
);
}
Object.assign(window, { SwipeableRow });