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>
138 lines
4.9 KiB
React
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 });
|