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>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/* ============================================================
|
||||
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 });
|
||||
Reference in New Issue
Block a user