From 19c686b4be7bf27ebfdca8406d56b475b0bf2bc9 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Mon, 25 May 2026 07:38:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(ux):=20user-select=20global,=20swipe-gauch?= =?UTF-8?q?e=20=C3=A9dition,=20clavier=20num=C3=A9rique,=20versionnage=20v?= =?UTF-8?q?0.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - user-select:none global (index.css) + reset sur input/textarea/select - ItemRow: swipe gauche → édition (fond bleu), suppression long press, bouton ✕ toujours visible sur mobile - SwipeableRow: prop onSwipeLeft, révèle rightContent entre seuil/2 et seuil, déclenche onSwipeLeft au seuil complet - TodosPage: onSwipeLeft → édition (remplace double-tap) - inputMode=decimal sur tous les champs quantité et prix - formatQty: affiche "2" au lieu de "2.000" - Versionnage: __APP_VERSION__ injecté par Vite depuis package.json v0.4.0 - HomePage: version affichée à côté du titre (v0.4.0) Co-Authored-By: Claude Sonnet 4.6 --- frontend/package.json | 2 +- .../components/shopping/CatalogueModal.tsx | 2 + frontend/src/components/shopping/ItemRow.tsx | 63 +++++++------------ .../src/components/todos/SwipeableRow.tsx | 31 +++------ frontend/src/index.css | 7 +++ frontend/src/pages/HomePage.tsx | 11 +++- frontend/src/pages/ShoppingPage.tsx | 2 + frontend/src/pages/TodosPage.tsx | 2 +- frontend/src/vite-env.d.ts | 3 + frontend/vite.config.ts | 4 ++ 10 files changed, 59 insertions(+), 68 deletions(-) create mode 100644 frontend/src/vite-env.d.ts diff --git a/frontend/package.json b/frontend/package.json index 95640e8..094f9fc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "homehub-frontend", "private": true, - "version": "0.1.0", + "version": "0.4.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/shopping/CatalogueModal.tsx b/frontend/src/components/shopping/CatalogueModal.tsx index 440b4d7..a118766 100644 --- a/frontend/src/components/shopping/CatalogueModal.tsx +++ b/frontend/src/components/shopping/CatalogueModal.tsx @@ -228,11 +228,13 @@ export default function CatalogueModal({ stores, onClose }: CatalogueModalProps) /> setForm(f => ({ ...f, price: e.target.value }))} /> setForm(f => ({ ...f, quantity_per_unit: e.target.value }))} /> diff --git a/frontend/src/components/shopping/ItemRow.tsx b/frontend/src/components/shopping/ItemRow.tsx index bce11ad..c5994d1 100644 --- a/frontend/src/components/shopping/ItemRow.tsx +++ b/frontend/src/components/shopping/ItemRow.tsx @@ -10,57 +10,38 @@ interface ItemRowProps { } const SWIPE_THRESHOLD = 80 -const LONG_PRESS_MS = 500 + +function formatQty(qty: string): string { + const n = parseFloat(qty) + if (isNaN(n)) return qty + return n % 1 === 0 ? String(Math.round(n)) : n.toString() +} export default function ItemRow({ item, onCheck, onDelete, onEdit, storeMode = false }: ItemRowProps) { const [offsetX, setOffsetX] = useState(0) const [isDragging, setIsDragging] = useState(false) const startX = useRef(null) - const longPressTimer = useRef | null>(null) - const didLongPress = useRef(false) - - function clearLongPress() { - if (longPressTimer.current) { - clearTimeout(longPressTimer.current) - longPressTimer.current = null - } - } function onTouchStart(e: React.TouchEvent) { startX.current = e.touches[0].clientX setIsDragging(true) - didLongPress.current = false - - if (onEdit) { - longPressTimer.current = setTimeout(() => { - didLongPress.current = true - onEdit() - }, LONG_PRESS_MS) - } } function onTouchMove(e: React.TouchEvent) { if (startX.current === null) return const dx = e.touches[0].clientX - startX.current - if (Math.abs(dx) > 8) clearLongPress() if (!storeMode) setOffsetX(Math.max(Math.min(dx, 0), -120)) } function onTouchEnd() { - clearLongPress() - if (!storeMode && offsetX < -SWIPE_THRESHOLD) { - onDelete() + if (!storeMode && offsetX < -SWIPE_THRESHOLD && onEdit) { + onEdit() } setOffsetX(0) setIsDragging(false) startX.current = null } - function handleClick() { - if (didLongPress.current) return - onCheck() - } - const minHeight = storeMode ? 64 : 52 return ( @@ -69,11 +50,11 @@ export default function ItemRow({ item, onCheck, onDelete, onEdit, storeMode = f
- + ✏️
)} @@ -81,7 +62,7 @@ export default function ItemRow({ item, onCheck, onDelete, onEdit, storeMode = f onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd} - onClick={handleClick} + onClick={onCheck} style={{ transform: `translateX(${offsetX}px)`, transition: isDragging ? 'none' : 'transform 0.2s ease', @@ -126,7 +107,7 @@ export default function ItemRow({ item, onCheck, onDelete, onEdit, storeMode = f {(item.quantity || item.unit) && (
- {item.quantity}{item.unit ? ` ${item.unit}` : ''} + {item.quantity ? formatQty(item.quantity) : ''}{item.unit ? ` ${item.unit}` : ''}
)} @@ -153,17 +134,15 @@ export default function ItemRow({ item, onCheck, onDelete, onEdit, storeMode = f >✕ - {/* Suppression mode magasin mobile uniquement */} - {storeMode && !item.is_checked && ( - - )} + {/* Suppression mobile — visible sur toutes les lignes */} + ) diff --git a/frontend/src/components/todos/SwipeableRow.tsx b/frontend/src/components/todos/SwipeableRow.tsx index b150252..1ec2012 100644 --- a/frontend/src/components/todos/SwipeableRow.tsx +++ b/frontend/src/components/todos/SwipeableRow.tsx @@ -4,17 +4,15 @@ interface SwipeableRowProps { children: React.ReactNode rightContent: React.ReactNode onSwipeRight?: () => void - onDoubleTap?: () => void + onSwipeLeft?: () => void } const THRESHOLD = 80 -const DOUBLE_TAP_DELAY = 300 -export default function SwipeableRow({ children, rightContent, onSwipeRight, onDoubleTap }: SwipeableRowProps) { +export default function SwipeableRow({ children, rightContent, onSwipeRight, onSwipeLeft }: SwipeableRowProps) { const [offsetX, setOffsetX] = useState(0) const [isDragging, setIsDragging] = useState(false) const startX = useRef(null) - const lastTap = useRef(0) function onTouchStart(e: React.TouchEvent) { startX.current = e.touches[0].clientX @@ -28,27 +26,14 @@ export default function SwipeableRow({ children, rightContent, onSwipeRight, onD } function onTouchEnd() { - const finalOffset = offsetX - - if (finalOffset > THRESHOLD && onSwipeRight) { - onSwipeRight() - } - - // Double-tap : seulement si le doigt n'a pas bougé (tap, pas swipe) - if (Math.abs(finalOffset) < 10 && onDoubleTap) { - const now = Date.now() - if (now - lastTap.current < DOUBLE_TAP_DELAY) { - onDoubleTap() - } - lastTap.current = now - } - + if (offsetX > THRESHOLD && onSwipeRight) onSwipeRight() + if (offsetX < -THRESHOLD && onSwipeLeft) onSwipeLeft() setOffsetX(0) setIsDragging(false) startX.current = null } - const revealActions = offsetX < -(THRESHOLD / 2) + const revealActions = offsetX < -(THRESHOLD / 2) && offsetX > -THRESHOLD return (
@@ -76,7 +61,11 @@ export default function SwipeableRow({ children, rightContent, onSwipeRight, onD style={{ transform: `translateX(${offsetX}px)`, transition: isDragging ? 'none' : 'transform 0.2s ease', - background: offsetX > THRESHOLD / 2 ? 'var(--ok)' : 'var(--bg-3)', + background: offsetX > THRESHOLD / 2 + ? 'var(--ok)' + : offsetX < -THRESHOLD + ? 'var(--info)' + : 'var(--bg-3)', position: 'relative', zIndex: 1, }} diff --git a/frontend/src/index.css b/frontend/src/index.css index dc0085b..3418a00 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -6,6 +6,13 @@ * { box-sizing: border-box; + -webkit-user-select: none; + user-select: none; +} + +input, textarea, select { + -webkit-user-select: text; + user-select: text; } body { diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 610f4ab..1cea986 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -4,9 +4,14 @@ export default function HomePage() { return (
-

- HomeHub -

+
+

+ HomeHub +

+ + v{__APP_VERSION__} + +

Application d'organisation personnelle

diff --git a/frontend/src/pages/ShoppingPage.tsx b/frontend/src/pages/ShoppingPage.tsx index 94066f4..6e09ca0 100644 --- a/frontend/src/pages/ShoppingPage.tsx +++ b/frontend/src/pages/ShoppingPage.tsx @@ -447,6 +447,7 @@ export default function ShoppingPage() { setEditQty(e.target.value)} autoFocus @@ -555,6 +556,7 @@ export default function ShoppingPage() {
setNewItemQty(e.target.value)} /> diff --git a/frontend/src/pages/TodosPage.tsx b/frontend/src/pages/TodosPage.tsx index 1d9cf1f..e3d4512 100644 --- a/frontend/src/pages/TodosPage.tsx +++ b/frontend/src/pages/TodosPage.tsx @@ -197,7 +197,7 @@ export default function TodosPage() { void handleDone(todo.id)} - onDoubleTap={() => setEditingTodo(todo)} + onSwipeLeft={() => setEditingTodo(todo)} rightContent={