From 83f0bd2ead156f99a0279f5ad46de77bc8e322ec Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 6 Nov 2026 12:16:54 +0100 Subject: [PATCH] feat(mobile): pull sheets from anywhere with a mouse too, glide the active day A pull on a sheet's content only answered touch, so with a mouse the sheet could only be dragged by its top edge, and a pull that started on a button read as a press. Mouse and pen now pull the content like a finger, a pull that starts on a button swallows its click, and text fields keep their own gesture. The active day chip and the segmented controls now glide one shared pill to the new item on the compositor, instead of repainting two buttons. --- client/src/mobile/components/MSegmented.tsx | 24 +-- client/src/mobile/components/useActivePill.ts | 50 ++++++ client/src/mobile/components/useSheetDrag.ts | 143 ++++++++++++------ client/src/mobile/screens/trip/MTripShell.tsx | 16 +- client/tests/unit/mobile/MSheet.test.tsx | 28 ++++ 5 files changed, 192 insertions(+), 69 deletions(-) create mode 100644 client/src/mobile/components/useActivePill.ts diff --git a/client/src/mobile/components/MSegmented.tsx b/client/src/mobile/components/MSegmented.tsx index 470128a4e..d89aa60f1 100644 --- a/client/src/mobile/components/MSegmented.tsx +++ b/client/src/mobile/components/MSegmented.tsx @@ -1,4 +1,5 @@ -import { ReactNode, useLayoutEffect, useRef, useState } from 'react' +import { ReactNode, useRef } from 'react' +import { useActivePill } from './useActivePill' export interface MSegmentedOption { value: T @@ -26,19 +27,9 @@ export default function MSegmented({ className = '', }: MSegmentedProps) { const trackRef = useRef(null) - const [indicator, setIndicator] = useState<{ left: number; width: number } | null>(null) + const pill = useActivePill(trackRef, '[data-active="true"]', [value, options.length]) - useLayoutEffect(() => { - const track = trackRef.current - if (!track) return - const measure = () => { - const active = track.querySelector('[data-active="true"]') - if (active) setIndicator({ left: active.offsetLeft, width: active.offsetWidth }) - } - measure() - const ro = new ResizeObserver(measure) - ro.observe(track) - return () => ro.disconnect() + return () => ro.disconnect() }, [value, options.length]) return ( @@ -47,12 +38,7 @@ export default function MSegmented({ role="tablist" className={`relative flex rounded-full bg-[color:var(--m-ic)] p-[3px] ${className}`} > - {indicator && ( -
- )} + {pill &&