From 7b29a5d34e13c5a0a8c99668545cb477ea410ee6 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 6 Nov 2026 13:00:58 +0100 Subject: [PATCH] feat(mobile): Vacay's month picker and month heads in glass The editor's twelve month chips were twelve grey boxes; they sit on one glass surface now, the picked month under a pill that glides to it across both rows. The month heads in the year overview are glass pills too. --- client/src/mobile/components/useActivePill.ts | 25 ++++++--- client/src/mobile/screens/vacay/MVacay.tsx | 52 ++++++++++++------- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/client/src/mobile/components/useActivePill.ts b/client/src/mobile/components/useActivePill.ts index a23946a32..1b3ddcc4d 100644 --- a/client/src/mobile/components/useActivePill.ts +++ b/client/src/mobile/components/useActivePill.ts @@ -17,9 +17,12 @@ export function useActivePill( /** The track, as a ref or as the element itself (a callback-ref'd rail). */ trackRef: RefObject | HTMLElement | null, selector: string, - deps: DependencyList + deps: DependencyList, + /** Follow the item across rows too (a grid of chips): the pill then also takes + * the item's top and height, and the caller leaves top/bottom unset. */ + rows = false ): CSSProperties | null { - const [box, setBox] = useState<{ left: number; width: number; animate: boolean } | null>(null); + const [box, setBox] = useState<{ left: number; top: number; width: number; height: number; animate: boolean } | null>(null); useLayoutEffect(() => { const track = trackRef && 'current' in trackRef ? trackRef.current : trackRef; @@ -30,7 +33,13 @@ export function useActivePill( setBox(null); return; } - setBox((prev) => ({ left: active.offsetLeft, width: active.offsetWidth, animate: prev !== null })); + setBox((prev) => ({ + left: active.offsetLeft, + top: active.offsetTop, + width: active.offsetWidth, + height: active.offsetHeight, + animate: prev !== null, + })); }; measure(); if (typeof ResizeObserver === 'undefined') return; @@ -42,9 +51,9 @@ export function useActivePill( }, deps); if (!box) return null; - return { - width: box.width, - transform: `translateX(${box.left}px)`, - transition: box.animate ? 'transform 320ms var(--ease-drawer), width 320ms var(--ease-drawer)' : 'none', - }; + const transition = box.animate ? 'transform 320ms var(--ease-drawer), width 320ms var(--ease-drawer)' : 'none'; + if (rows) { + return { top: 0, width: box.width, height: box.height, transform: `translate(${box.left}px, ${box.top}px)`, transition }; + } + return { width: box.width, transform: `translateX(${box.left}px)`, transition }; } diff --git a/client/src/mobile/screens/vacay/MVacay.tsx b/client/src/mobile/screens/vacay/MVacay.tsx index 0cef3ff9e..165e4b0d5 100644 --- a/client/src/mobile/screens/vacay/MVacay.tsx +++ b/client/src/mobile/screens/vacay/MVacay.tsx @@ -1,9 +1,11 @@ +import { useRef } from 'react' import { Building2, Check, ChevronDown, ChevronLeft, ChevronRight, Eye, EyeOff, Minus, PenLine, Pencil, Plus, Settings2, Share2, ShieldCheck, Trash2, Unlink, UserPlus } from 'lucide-react' import type { LucideIcon } from 'lucide-react' import MSheet from '../../components/MSheet' import MIconBtn from '../../components/MIconBtn' import MProgress from '../../components/MProgress' import MDayPager from '../../components/MDayPager' +import { useActivePill } from '../../components/useActivePill' import { useTranslation } from '../../../i18n' import { useMVacay } from './useMVacay' import MVacayMonth from './MVacayMonth' @@ -201,7 +203,7 @@ export default function MVacay() { @@ -225,24 +227,9 @@ export default function MVacay() { {/* Edit view: month quick nav + single month */} {edit && ( <> -
-
- {v.monthNamesShort.map((name, slot) => ( - - ))} -
-
+ {/* One glass surface for the twelve months, the picked one under a pill + that glides to it, across the rows too. */} + {/* Swipe between the months of the window, like pages. */} ) } + +/** The editor's month picker: the window's twelve months on one glass surface. */ +function MonthQuickNav({ names, slot, onPick }: { names: string[]; slot: number; onPick: (slot: number) => void }) { + const trackRef = useRef(null) + const pill = useActivePill(trackRef, '[data-active="true"]', [slot, names.length], true) + return ( +
+
+ {pill &&
+
+ ) +}