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.
This commit is contained in:
Maurice
2026-11-06 13:00:58 +01:00
parent c7a369a0ca
commit 7b29a5d34e
2 changed files with 50 additions and 27 deletions
+17 -8
View File
@@ -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> | 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 };
}
+33 -19
View File
@@ -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() {
<button
type="button"
onClick={() => v.openMonthSlot(slot)}
className="mb-[6px] w-full rounded-[11px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] py-[7px] text-center text-[0.75rem] font-extrabold capitalize"
className="mb-[6px] w-full rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] py-[6px] text-center text-[0.75rem] font-extrabold capitalize shadow-[0_4px_12px_-8px_rgba(0,0,0,.25)] backdrop-blur-[24px] backdrop-saturate-[1.7] transition-transform active:scale-[.97]"
>
{v.monthNamesShort[slot]}
</button>
@@ -225,24 +227,9 @@ export default function MVacay() {
{/* Edit view: month quick nav + single month */}
{edit && (
<>
<div className="mb-[10px] rounded-2xl border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] p-2">
<div className="grid grid-cols-6 gap-[5px]">
{v.monthNamesShort.map((name, slot) => (
<button
key={slot}
type="button"
onClick={() => v.setMonthSlot(slot)}
className={`whitespace-nowrap rounded-[11px] py-[7px] text-center text-[0.71875rem] font-bold capitalize ${
slot === v.monthSlot
? 'bg-m-act text-m-actfg'
: 'border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] text-m-ink'
}`}
>
{name}
</button>
))}
</div>
</div>
{/* One glass surface for the twelve months, the picked one under a pill
that glides to it, across the rows too. */}
<MonthQuickNav names={v.monthNamesShort} slot={v.monthSlot} onPick={v.setMonthSlot} />
{/* Swipe between the months of the window, like pages. */}
<MDayPager
inFlow
@@ -503,3 +490,30 @@ function FuseInfo({ icon: Icon, text }: { icon: LucideIcon; text: string }) {
</div>
)
}
/** 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<HTMLDivElement>(null)
const pill = useActivePill(trackRef, '[data-active="true"]', [slot, names.length], true)
return (
<div className="mb-[10px] rounded-[22px] border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] p-1.5 shadow-[0_8px_24px_-16px_rgba(0,0,0,.3)] backdrop-blur-[24px] backdrop-saturate-[1.7]">
<div ref={trackRef} className="relative grid grid-cols-6 gap-1">
{pill && <span aria-hidden="true" className="pointer-events-none absolute left-0 rounded-full bg-m-act" style={pill} />}
{names.map((name, i) => (
<button
key={i}
type="button"
data-active={i === slot}
aria-pressed={i === slot}
onClick={() => onPick(i)}
className={`relative z-[1] whitespace-nowrap rounded-full py-[7px] text-center text-[0.71875rem] font-bold capitalize transition-colors duration-200 ${
i === slot ? `text-m-actfg ${pill ? '' : 'bg-m-act'}` : 'text-m-ink active:bg-[color:var(--m-ic)]'
}`}
>
{name}
</button>
))}
</div>
</div>
)
}