Files
TREK/client/src/mobile/components/MSegmented.tsx
T
Maurice 6835fd7bf3 feat(mobile): page transitions, pull-to-refresh, share sheet and haptics
Page transitions: on the phone, pages move like a native navigation stack,
built on the View Transitions API (mobile/routeTransition.ts). A page opened
from another slides in from the trailing edge, back slides it away, and
switching between the dock's sections cross-fades while the dock stands
still. Lazy routes do not flash their skeleton: the swap waits, behind the
frozen snapshot, until the new page rendered. The edge-swipe back gesture
already moves the page and skips the transition. RTL mirrors the motion,
reduced motion and desktop get none.

Pull-to-refresh on the mobile dashboard: the page follows the finger with
resistance, ticks once past the threshold and re-reads trips and
notifications quietly, without a loading state.

Share: trip share and invite links and the journey link get a share button
next to copy, opening the system share sheet (Capacitor Share in the apps,
Web Share in browsers that have it).

Haptics: a selection click for switches and segmented controls, a tick when
a sheet is flung closed, and a firmer bump for the chat's long press.

Theme lint: this branch's new screens use the type and z-index tokens, the
Vacay company tint is a token, and the baseline is lowered to the new
counts.
2026-11-06 15:02:55 +01:00

66 lines
2.0 KiB
TypeScript

import { ReactNode, useRef } from 'react'
import { useActivePill } from './useActivePill'
import { selectionHaptic } from '../../native/haptics'
export interface MSegmentedOption<T extends string = string> {
value: T
label: ReactNode
}
interface MSegmentedProps<T extends string = string> {
options: MSegmentedOption<T>[]
value: T
onChange: (value: T) => void
/** stretch = equal-width segments (costs tabs), intrinsic = pill hugs its label (dashboard filter) */
variant?: 'stretch' | 'intrinsic'
className?: string
}
/**
* Segment pill with a sliding active indicator. Active segment reads in
* --m-actfg on the --m-act pill, idle segments in --m-ink (Spec 07 §6).
*/
export default function MSegmented<T extends string = string>({
options,
value,
onChange,
variant = 'stretch',
className = '',
}: MSegmentedProps<T>) {
const trackRef = useRef<HTMLDivElement>(null)
const pill = useActivePill(trackRef, '[data-active="true"]', [value, options.length])
return (
<div
ref={trackRef}
role="tablist"
className={`relative flex rounded-full bg-[color:var(--m-ic)] p-[3px] ${className}`}
>
{pill && <div aria-hidden="true" className="absolute bottom-[3px] left-0 top-[3px] rounded-full bg-m-act" style={pill} />}
{options.map((opt) => {
const active = opt.value === value
return (
<button
key={opt.value}
type="button"
role="tab"
aria-selected={active}
data-active={active}
onClick={() => {
if (!active) selectionHaptic()
onChange(opt.value)
}}
className={`relative z-[1] rounded-full py-[7px] text-center ${
variant === 'intrinsic'
? 'whitespace-nowrap px-[11px] text-[0.75rem]'
: 'flex-1 text-[0.71875rem]'
} ${active ? 'font-semibold text-m-actfg' : 'font-medium text-m-ink'}`}
>
{opt.label}
</button>
)
})}
</div>
)
}