From 6835fd7bf303394fff90af49f33d592c2da8a3d6 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 6 Nov 2026 14:49:26 +0100 Subject: [PATCH] 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. --- client/scripts/theme-baseline.json | 10 +- client/src/App.tsx | 8 +- client/src/components/Layout/AppLockGate.tsx | 2 +- .../components/Layout/NativeImportSheet.tsx | 16 ++- .../components/Layout/NativeUpdateNotice.tsx | 2 +- .../src/components/Settings/ToggleSwitch.tsx | 3 +- client/src/mobile/components/MBottomNav.tsx | 2 +- client/src/mobile/components/MEdgeBack.tsx | 2 + .../src/mobile/components/MPullIndicator.tsx | 29 +++++ .../src/mobile/components/MRouteFallback.tsx | 2 +- client/src/mobile/components/MSegmented.tsx | 6 +- client/src/mobile/components/MSheet.tsx | 2 + client/src/mobile/components/MToggle.tsx | 6 +- client/src/mobile/mobile.css | 66 ++++++++++ client/src/mobile/routeTransition.test.tsx | 70 ++++++++++ client/src/mobile/routeTransition.ts | 93 +++++++++++++ .../mobile/screens/dashboard/MDashboard.tsx | 13 +- .../screens/journey/MJourneySettingsSheet.tsx | 13 +- .../screens/trip/plan/MRouteAppsSheet.tsx | 6 +- .../trip/sheets/MTripSettingsSheet.tsx | 12 ++ .../mobile/screens/trip/tabs/MCollabChat.tsx | 2 + client/src/mobile/screens/vacay/MVacay.tsx | 20 +-- client/src/mobile/usePullToRefresh.test.tsx | 83 ++++++++++++ client/src/mobile/usePullToRefresh.ts | 123 ++++++++++++++++++ client/src/native/haptics.test.ts | 21 ++- client/src/native/haptics.ts | 18 +++ client/src/native/shareLink.test.ts | 40 ++++++ client/src/native/shareLink.ts | 33 +++++ client/src/pages/dashboard/useDashboard.ts | 7 +- shared/src/i18n/ar/common.ts | 1 + shared/src/i18n/az/common.ts | 1 + shared/src/i18n/br/common.ts | 1 + shared/src/i18n/ca/common.ts | 1 + shared/src/i18n/cs/common.ts | 1 + shared/src/i18n/de/common.ts | 3 +- shared/src/i18n/en/common.ts | 1 + shared/src/i18n/es/common.ts | 1 + shared/src/i18n/et/common.ts | 1 + shared/src/i18n/fr/common.ts | 1 + shared/src/i18n/gr/common.ts | 1 + shared/src/i18n/he/common.ts | 1 + shared/src/i18n/hu/common.ts | 1 + shared/src/i18n/id/common.ts | 1 + shared/src/i18n/it/common.ts | 1 + shared/src/i18n/ja/common.ts | 1 + shared/src/i18n/ko/common.ts | 1 + shared/src/i18n/nl/common.ts | 3 +- shared/src/i18n/pl/common.ts | 1 + shared/src/i18n/ru/common.ts | 1 + shared/src/i18n/sk/common.ts | 1 + shared/src/i18n/sv/common.ts | 1 + shared/src/i18n/th/common.ts | 1 + shared/src/i18n/tr/common.ts | 1 + shared/src/i18n/uk/common.ts | 1 + shared/src/i18n/vi/common.ts | 1 + shared/src/i18n/zh-TW/common.ts | 1 + shared/src/i18n/zh/common.ts | 1 + 57 files changed, 699 insertions(+), 43 deletions(-) create mode 100644 client/src/mobile/components/MPullIndicator.tsx create mode 100644 client/src/mobile/routeTransition.test.tsx create mode 100644 client/src/mobile/routeTransition.ts create mode 100644 client/src/mobile/usePullToRefresh.test.tsx create mode 100644 client/src/mobile/usePullToRefresh.ts create mode 100644 client/src/native/shareLink.test.ts create mode 100644 client/src/native/shareLink.ts diff --git a/client/scripts/theme-baseline.json b/client/scripts/theme-baseline.json index e9a007d72..198f830c4 100644 --- a/client/scripts/theme-baseline.json +++ b/client/scripts/theme-baseline.json @@ -125,7 +125,6 @@ "components/shared/CustomSelect.tsx": 1, "components/shared/CustomTimePicker.tsx": 1, "components/shared/EmptyState.tsx": 1, - "components/shared/ErrorBoundary.tsx": 2, "components/shared/Modal.tsx": 3, "components/shared/NavigationMenu.tsx": 1, "components/shared/NoteColorPicker.tsx": 2, @@ -166,7 +165,7 @@ "mobile/screens/admin/MAdminUi.tsx": 10, "mobile/screens/admin/MAdminUsersSection.tsx": 14, "mobile/screens/admin/MApiProviderBlocks.tsx": 10, - "mobile/screens/atlas/MAtlas.tsx": 7, + "mobile/screens/atlas/MAtlas.tsx": 5, "mobile/screens/atlas/MAtlasBucketSheet.tsx": 16, "mobile/screens/atlas/MAtlasCountryPopup.tsx": 11, "mobile/screens/atlas/MAtlasSearch.tsx": 5, @@ -219,11 +218,10 @@ "mobile/screens/trip/places/MPlacesBrowser.tsx": 10, "mobile/screens/trip/places/MPlacesBulkCategorySheet.tsx": 4, "mobile/screens/trip/places/MPlacesFilterControls.tsx": 1, - "mobile/screens/trip/places/MPlacesFilterSheet.tsx": 3, "mobile/screens/trip/places/MPlacesSaveToCollectionSheet.tsx": 7, "mobile/screens/trip/places/MPlacesToursModeSwitch.tsx": 1, - "mobile/screens/trip/plan/MPlanTimeline.tsx": 14, - "mobile/screens/trip/plan/MPlanTimelineRows.tsx": 26, + "mobile/screens/trip/plan/MPlanTimeline.tsx": 12, + "mobile/screens/trip/plan/MPlanTimelineRows.tsx": 24, "mobile/screens/trip/roadtrip/MRoadtripRows.tsx": 31, "mobile/screens/trip/roadtrip/MRoadtripTab.tsx": 16, "mobile/screens/trip/roadtrip/MRtAlternativesBar.tsx": 8, @@ -281,7 +279,7 @@ "mobile/screens/trip/tabs/MTransportsTab.tsx": 2, "mobile/screens/trip/tabs/filesModel.ts": 5, "mobile/screens/trip/tabs/tabChrome.tsx": 12, - "mobile/screens/vacay/MVacay.tsx": 23, + "mobile/screens/vacay/MVacay.tsx": 17, "mobile/screens/vacay/MVacayInviteSheet.tsx": 9, "mobile/screens/vacay/MVacayMonth.tsx": 3, "mobile/screens/vacay/MVacaySettingsSheet.tsx": 35, diff --git a/client/src/App.tsx b/client/src/App.tsx index bcf5cad6a..316dd808f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useRef, ReactNode, Suspense } from 'react' -import { Routes, Route, Navigate, useLocation } from 'react-router' +import { Routes, Route, Navigate, useLocation, useNavigationType } from 'react-router' import { useAuthStore } from './store/authStore' import { useSettingsStore } from './store/settingsStore' import { applyAppearance } from './theme/applyAppearance' @@ -21,6 +21,7 @@ import { lazyWithRetry } from './utils/lazyWithRetry' import { reconcileAppVersion } from './utils/versionHandover' import { useServerVersionStore } from './store/serverVersionStore' import { useIsPhone } from './mobile/useIsPhone' +import { useRouteTransition } from './mobile/routeTransition' import { TranslationProvider, useTranslation } from './i18n' import { authApi, isAuthPublicPath } from './api/client' import { tripRepo } from './repo/tripRepo' @@ -411,6 +412,9 @@ export default function App() { }, [settings.dark_mode, settings.appearance, isSharedPage]) const isPhone = useIsPhone() + // On the phone, pages slide like a native navigation stack (mobile/routeTransition.ts). + const navigationType = useNavigationType() + const routedLocation = useRouteTransition(location, navigationType, isPhone) const keepingTour = useSaveToCollectionStore(s => s.tour !== null) const isAuthPage = location.pathname.startsWith('/login') || location.pathname.startsWith('/register') @@ -455,7 +459,7 @@ export default function App() { so a mounted boundary keeps the current page on screen instead of flashing a spinner on every jump — the spinner is for the first paint of a deep link. */} }> - + } /> } /> } /> diff --git a/client/src/components/Layout/AppLockGate.tsx b/client/src/components/Layout/AppLockGate.tsx index 45498c8b7..094f08e6f 100644 --- a/client/src/components/Layout/AppLockGate.tsx +++ b/client/src/components/Layout/AppLockGate.tsx @@ -62,7 +62,7 @@ export default function AppLockGate(): React.ReactElement | null { role="dialog" aria-modal="true" aria-label={t('native.lock.title')} - className="fixed inset-0 z-[100001] flex flex-col items-center justify-center gap-5 bg-surface p-8 text-center backdrop-blur-2xl" + className="fixed inset-0 z-[calc(var(--z-toast)+1)] flex flex-col items-center justify-center gap-5 bg-surface p-8 text-center backdrop-blur-2xl" >