diff --git a/client/src/App.tsx b/client/src/App.tsx
index f8ff987c7..ac01a09bb 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -14,6 +14,7 @@ import SaveToCollectionModal from './components/Collections/SaveToCollectionModa
import MSaveToCollectionSheet from './components/Collections/MSaveToCollectionSheet'
import BackgroundTasksWidget from './components/BackgroundTasks/BackgroundTasksWidget'
import MobileShell from './mobile/MobileShell'
+import MRouteFallback from './mobile/components/MRouteFallback'
import ErrorBoundary from './components/shared/ErrorBoundary'
import { lazyWithRetry } from './utils/lazyWithRetry'
import { useIsPhone } from './mobile/useIsPhone'
@@ -52,6 +53,23 @@ const JoinTripPage = lazyWithRetry(() => import('./pages/JoinTripPage'))
const InAppNotificationsPage = lazyWithRetry(() => import('./pages/InAppNotificationsPage.tsx'))
const OAuthAuthorizePage = lazyWithRetry(() => import('./pages/OAuthAuthorizePage'))
+// The ten phone screens are chunks of their own, alongside the desktop pages
+// rather than inside them. Each page used to import its M screen statically and
+// decide while rendering, so the route chunk always carried both trees and the
+// viewport only picked which half stayed dark. Here the branch decides the
+// chunk: a phone never loads the desktop planner, a desktop never the mobile
+// shell.
+const MDashboardScreen = lazyWithRetry(() => import('./mobile/screens/dashboard/MDashboard'))
+const MTripScreen = lazyWithRetry(() => import('./mobile/screens/trip/MTripShell'))
+const MAdminScreen = lazyWithRetry(() => import('./mobile/screens/admin/MAdmin'))
+const MSettingsScreen = lazyWithRetry(() => import('./mobile/screens/settings/MSettings'))
+const MVacayScreen = lazyWithRetry(() => import('./mobile/screens/vacay/MVacay'))
+const MAtlasScreen = lazyWithRetry(() => import('./mobile/screens/atlas/MAtlas'))
+const MJourneyScreen = lazyWithRetry(() => import('./mobile/screens/journey/MJourney'))
+const MJourneyDetailScreen = lazyWithRetry(() => import('./mobile/screens/journey/MJourneyDetail'))
+const MCollectionsScreen = lazyWithRetry(() => import('./mobile/screens/collections/MCollections'))
+const MNotificationsScreen = lazyWithRetry(() => import('./mobile/screens/notifications/MNotifications'))
+
interface ProtectedRouteProps {
children: ReactNode
adminRequired?: boolean
@@ -140,6 +158,20 @@ function PublicRoute({ children }: { children: React.ReactNode }) {
)
}
+/**
+ * Picks the chunk, not just the branch. Both sides come through lazyWithRetry,
+ * so exactly one of them is fetched for a given viewport — which is the whole
+ * point: the ternary that used to sit in each page only ran once the browser
+ * had already paid for both trees.
+ */
+function ViewportRoute({ phone: Phone, desktop: Desktop }: {
+ phone: React.ComponentType
+ desktop: React.ComponentType
+}): React.ReactElement {
+ const isPhone = useIsPhone()
+ return isPhone ? :
+}
+
function RootRedirect() {
const { isAuthenticated, isLoading } = useAuthStore()
@@ -160,6 +192,11 @@ function RootRedirect() {
* get to inherit its raw slate.
*/
function RouteFallback() {
+ // The fallback renders above MobileShell, so it has to pick its own palette —
+ // on a phone the desktop spinner on bg-surface would be a foreign white sheet
+ // in front of the mobile screen.
+ const isPhone = useIsPhone()
+ if (isPhone) return
return (
@@ -290,7 +327,7 @@ export default function App() {
path="/dashboard"
element={
-
+
}
/>
@@ -324,7 +361,7 @@ export default function App() {
path="/trips/:id"
element={
-
+
}
/>
@@ -340,7 +377,7 @@ export default function App() {
path="/admin"
element={
-
+
}
/>
@@ -348,7 +385,7 @@ export default function App() {
path="/settings"
element={
-
+
}
/>
@@ -364,7 +401,7 @@ export default function App() {
path="/vacay"
element={
-
+
}
/>
@@ -372,7 +409,7 @@ export default function App() {
path="/atlas"
element={
-
+
}
/>
@@ -380,7 +417,7 @@ export default function App() {
path="/journey"
element={
-
+
}
/>
@@ -388,7 +425,7 @@ export default function App() {
path="/journey/:id"
element={
-
+
}
/>
@@ -396,7 +433,7 @@ export default function App() {
path="/collections"
element={
-
+
}
/>
@@ -404,7 +441,7 @@ export default function App() {
path="/collections/:id"
element={
-
+
}
/>
@@ -412,7 +449,7 @@ export default function App() {
path="/notifications"
element={
-
+
}
/>
diff --git a/client/src/App.viewport.test.tsx b/client/src/App.viewport.test.tsx
new file mode 100644
index 000000000..13c37d5e5
--- /dev/null
+++ b/client/src/App.viewport.test.tsx
@@ -0,0 +1,111 @@
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import { MemoryRouter } from 'react-router'
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { useAuthStore } from './store/authStore'
+import { useAddonStore } from './store/addonStore'
+import { resetAllStores } from '../tests/helpers/store'
+import { buildUser } from '../tests/helpers/factories'
+import App from './App'
+
+/**
+ * The viewport decides which chunk gets fetched, so the switch lives in App.tsx
+ * and no longer in the pages. This is that contract, for all ten routes in one
+ * place — it used to be five scattered spot checks, one per page test.
+ *
+ * A stub per screen keeps the test about one thing: below the breakpoint the M
+ * root hangs off the route, and the desktop page is never rendered at all.
+ */
+
+const isPhone = vi.hoisted(() => ({ value: false }))
+vi.mock('./mobile/useIsPhone', () => ({ useIsPhone: () => isPhone.value }))
+
+vi.mock('./mobile/screens/dashboard/MDashboard', () => ({ default: () =>
m-dashboard
}))
+vi.mock('./mobile/screens/trip/MTripShell', () => ({ default: () =>
m-trip
}))
+vi.mock('./mobile/screens/admin/MAdmin', () => ({ default: () =>
m-admin
}))
+vi.mock('./mobile/screens/settings/MSettings', () => ({ default: () =>
m-settings
}))
+vi.mock('./mobile/screens/vacay/MVacay', () => ({ default: () =>
m-vacay
}))
+vi.mock('./mobile/screens/atlas/MAtlas', () => ({ default: () =>
m-atlas
}))
+vi.mock('./mobile/screens/journey/MJourney', () => ({ default: () =>
m-journey
}))
+vi.mock('./mobile/screens/journey/MJourneyDetail', () => ({ default: () =>
m-journey-detail
}))
+vi.mock('./mobile/screens/collections/MCollections', () => ({ default: () =>
m-collections
}))
+vi.mock('./mobile/screens/notifications/MNotifications', () => ({ default: () =>
m-notifications
}))
+
+vi.mock('./pages/DashboardPage', () => ({ default: () =>
d-dashboard
}))
+vi.mock('./pages/TripPlannerPage', () => ({ default: () =>
d-trip
}))
+vi.mock('./pages/AdminPage', () => ({ default: () =>
d-admin
}))
+vi.mock('./pages/SettingsPage', () => ({ default: () =>
d-settings
}))
+vi.mock('./pages/VacayPage', () => ({ default: () =>
d-vacay
}))
+vi.mock('./pages/AtlasPage', () => ({ default: () =>
d-atlas
}))
+vi.mock('./pages/JourneyPage', () => ({ default: () =>
d-journey
}))
+vi.mock('./pages/JourneyDetailPage', () => ({ default: () =>
d-journey-detail
}))
+vi.mock('./pages/CollectionsPage', () => ({ default: () =>
d-collections
}))
+vi.mock('./pages/InAppNotificationsPage.tsx', () => ({ default: () =>
d-notifications
}))
+
+// The notification listener opens a WebSocket on mount.
+vi.mock('./hooks/useInAppNotificationListener.ts', () => ({
+ useInAppNotificationListener: vi.fn(),
+}))
+
+/** path, mobile marker, desktop marker */
+const ROUTES: [string, string, string][] = [
+ ['/dashboard', 'm-dashboard', 'd-dashboard'],
+ ['/trips/1', 'm-trip', 'd-trip'],
+ ['/admin', 'm-admin', 'd-admin'],
+ ['/settings', 'm-settings', 'd-settings'],
+ ['/vacay', 'm-vacay', 'd-vacay'],
+ ['/atlas', 'm-atlas', 'd-atlas'],
+ ['/journey', 'm-journey', 'd-journey'],
+ ['/journey/1', 'm-journey-detail', 'd-journey-detail'],
+ ['/collections', 'm-collections', 'd-collections'],
+ ['/collections/1', 'm-collections', 'd-collections'],
+ ['/notifications', 'm-notifications', 'd-notifications'],
+]
+
+function renderAt(path: string) {
+ return render(
+
+
+
+ )
+}
+
+beforeEach(() => {
+ resetAllStores()
+ vi.clearAllMocks()
+ isPhone.value = false
+ // An admin passes the role check on /admin; every other route ignores it.
+ useAuthStore.setState({
+ isLoading: false,
+ isAuthenticated: true,
+ user: buildUser({ role: 'admin', mfa_enabled: true }),
+ appRequireMfa: false,
+ loadUser: vi.fn().mockResolvedValue(undefined),
+ })
+ // /journey and /collections sit behind addons; without this they redirect to
+ // the dashboard and the route under test never renders.
+ useAddonStore.setState({ loaded: true, isEnabled: () => true })
+})
+
+describe('App — viewport routing', () => {
+ it.each(ROUTES)(
+ 'FE-COMP-APPVP-001: %s renders the mobile screen below the breakpoint',
+ async (path, mobile, desktop) => {
+ isPhone.value = true
+ renderAt(path)
+
+ expect(await screen.findByText(mobile)).toBeInTheDocument()
+ expect(screen.queryByText(desktop)).not.toBeInTheDocument()
+ }
+ )
+
+ it.each(ROUTES)(
+ 'FE-COMP-APPVP-002: %s renders the desktop page above the breakpoint',
+ async (path, mobile, desktop) => {
+ renderAt(path)
+
+ expect(await screen.findByText(desktop)).toBeInTheDocument()
+ expect(screen.queryByText(mobile)).not.toBeInTheDocument()
+ }
+ )
+})
diff --git a/client/src/mobile/components/MRouteFallback.tsx b/client/src/mobile/components/MRouteFallback.tsx
new file mode 100644
index 000000000..ba802c0e3
--- /dev/null
+++ b/client/src/mobile/components/MRouteFallback.tsx
@@ -0,0 +1,19 @@
+/**
+ * Placeholder while a route chunk is in flight — the phone counterpart to
+ * RouteFallback in App.tsx. It carries m-root itself, because it renders outside
+ * MobileShell and the --m-* tokens would not resolve otherwise.
+ *
+ * An outline rather than a spinner: on a phone an empty screen is the loudest
+ * state the app has, and a pulsing silhouette of what is about to arrive reads
+ * as "loading" instead of "broken".
+ */
+export default function MRouteFallback() {
+ return (
+
+ )
+}
diff --git a/client/src/pages/AdminPage.test.tsx b/client/src/pages/AdminPage.test.tsx
index 463383a74..1cd09f0f4 100644
--- a/client/src/pages/AdminPage.test.tsx
+++ b/client/src/pages/AdminPage.test.tsx
@@ -69,19 +69,8 @@ vi.mock('../components/Admin/DefaultUserSettingsTab', () => ({
default: () =>
,
}));
-vi.mock('../mobile/screens/admin/MAdmin', () => ({
- default: () =>
,
-}));
-
-// Viewport switch — flipped per test instead of at module scope.
-let phoneViewport = false;
-vi.mock('../mobile/useIsPhone', () => ({
- useIsPhone: () => phoneViewport,
-}));
-
beforeEach(() => {
resetAllStores();
- phoneViewport = false;
});
describe('AdminPage', () => {
@@ -1617,18 +1606,6 @@ describe('AdminPage', () => {
});
});
- describe('FE-PAGE-ADMIN-058: Phone viewport', () => {
- it('renders the mobile admin screen instead of the desktop layout', async () => {
- phoneViewport = true;
- seedStore(useAuthStore, { isAuthenticated: true, user: buildAdmin() });
-
- render(
);
-
- expect(screen.getByTestId('mobile-admin')).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: /^users$/i })).not.toBeInTheDocument();
- });
- });
-
describe('FE-PAGE-ADMIN-059: Stats fallbacks', () => {
it('shows a zero file count when the API omits totalFiles', async () => {
server.use(
diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx
index ba5233b09..8b3a73f23 100644
--- a/client/src/pages/AdminPage.tsx
+++ b/client/src/pages/AdminPage.tsx
@@ -14,8 +14,6 @@ import AdminMcpTokensPanel from '../components/Admin/AdminMcpTokensPanel'
import AdminPluginsPanel from '../components/Admin/AdminPluginsPanel'
import { Users, Map, Briefcase, Shield, FileText, SlidersHorizontal, UserCog, Puzzle, Blocks, Settings as SettingsIcon, Bell, Database, ScrollText, KeyRound, GitBranch, Bug } from 'lucide-react'
import PageSidebar, { type PageSidebarTab } from '../components/Layout/PageSidebar'
-import { useIsPhone } from '../mobile/useIsPhone'
-import MAdmin from '../mobile/screens/admin/MAdmin'
import { useAdmin } from './admin/useAdmin'
import AdminUpdateBanner from './admin/AdminUpdateBanner'
import AdminStatCard from './admin/AdminStatCard'
@@ -25,8 +23,9 @@ import AdminNotificationsTab from './admin/AdminNotificationsTab'
import AdminUserModals from './admin/AdminUserModals'
export default function AdminPage(): React.ReactElement {
- const isPhone = useIsPhone()
- return isPhone ?
:
+ // ViewportRoute in App.tsx picks the branch now, so the phone screen is a
+ // chunk of its own instead of a dead limb in this one.
+ return
}
function AdminPageDesktop(): React.ReactElement {
diff --git a/client/src/pages/AtlasPage.tsx b/client/src/pages/AtlasPage.tsx
index f259c9e27..716707354 100644
--- a/client/src/pages/AtlasPage.tsx
+++ b/client/src/pages/AtlasPage.tsx
@@ -13,12 +13,11 @@ import AtlasCountrySearch from './atlas/AtlasCountrySearch'
import AtlasLayerToggle from './atlas/AtlasLayerToggle'
import { useToast } from '../components/shared/Toast'
import { getApiErrorMessage } from '../types'
-import { useIsPhone } from '../mobile/useIsPhone'
-import MAtlas from '../mobile/screens/atlas/MAtlas'
export default function AtlasPage(): React.ReactElement {
- const isPhone = useIsPhone()
- return isPhone ?
:
+ // ViewportRoute in App.tsx picks the branch now, so the phone screen is a
+ // chunk of its own instead of a dead limb in this one.
+ return
}
function AtlasPageDesktop(): React.ReactElement {
diff --git a/client/src/pages/AtlasPage.wiring.test.tsx b/client/src/pages/AtlasPage.wiring.test.tsx
index 32e2883d6..40c1a098e 100644
--- a/client/src/pages/AtlasPage.wiring.test.tsx
+++ b/client/src/pages/AtlasPage.wiring.test.tsx
@@ -14,10 +14,9 @@ import AtlasPage from './AtlasPage';
// confirm popup and the sidebar can be driven into every state directly, which the
// map-click driven suite in AtlasPage.test.tsx cannot reach.
-const mocks = vi.hoisted(() => ({ atlas: {} as AtlasController, isPhone: false }));
+const mocks = vi.hoisted(() => ({ atlas: {} as AtlasController }));
vi.mock('./atlas/useAtlas', () => ({ useAtlas: () => mocks.atlas }));
-vi.mock('../mobile/useIsPhone', () => ({ useIsPhone: () => mocks.isPhone }));
vi.mock('../components/Layout/Navbar', () => ({
default: () => React.createElement('nav', { 'data-testid': 'navbar' }),
}));
@@ -35,7 +34,6 @@ function selectTriggers(): HTMLElement[] {
}
beforeEach(() => {
- mocks.isPhone = false;
toast.mockClear();
window.__addToast = toast;
setAtlas();
@@ -54,14 +52,6 @@ afterEach(() => {
});
describe('AtlasPage wiring', () => {
- it('FE-PAGE-ATLASW-001: hands the whole screen to the mobile atlas on a phone', () => {
- mocks.isPhone = true;
- render(
);
-
- expect(screen.getByRole('button', { name: 'atlas.bucketTab' })).toBeInTheDocument();
- expect(screen.queryByTestId('navbar')).not.toBeInTheDocument();
- });
-
it('FE-PAGE-ATLASW-002: shows navbar and spinner while the atlas is loading', () => {
setAtlas({ loading: true });
render(
);
diff --git a/client/src/pages/CollectionsPage.tsx b/client/src/pages/CollectionsPage.tsx
index 4ad98fbc1..ccbe9c8a9 100644
--- a/client/src/pages/CollectionsPage.tsx
+++ b/client/src/pages/CollectionsPage.tsx
@@ -16,15 +16,14 @@ import CollectionPlaceDetail from '../components/Collections/CollectionPlaceDeta
import LabelManager from '../components/Collections/LabelManager'
import BulkAssignLabelModal from '../components/Collections/BulkAssignLabelModal'
import { useCollections } from './collections/useCollections'
-import { useIsPhone } from '../mobile/useIsPhone'
-import MCollections from '../mobile/screens/collections/MCollections'
import EmptyState from '../components/shared/EmptyState'
import '../styles/dashboard.css'
import '../styles/collections.css'
export default function CollectionsPage(): React.ReactElement {
- const isPhone = useIsPhone()
- return isPhone ?
:
+ // ViewportRoute in App.tsx picks the branch now, so the phone screen is a
+ // chunk of its own instead of a dead limb in this one.
+ return
}
function CollectionsPageDesktop(): React.ReactElement {
diff --git a/client/src/pages/CollectionsPage.wiring.test.tsx b/client/src/pages/CollectionsPage.wiring.test.tsx
index a13f395e0..59e1d4a6c 100644
--- a/client/src/pages/CollectionsPage.wiring.test.tsx
+++ b/client/src/pages/CollectionsPage.wiring.test.tsx
@@ -8,10 +8,7 @@ import { render, screen, fireEvent } from '../../tests/helpers/render'
import CollectionsPage from './CollectionsPage'
vi.mock('../components/Layout/Navbar', () => ({ default: () =>
}))
-vi.mock('../mobile/screens/collections/MCollections', () => ({ default: () =>
}))
-let isPhone = false
-vi.mock('../mobile/useIsPhone', () => ({ useIsPhone: () => isPhone }))
vi.mock('../components/Collections/ListsRail', () => ({
default: (p: {
@@ -299,19 +296,10 @@ function renderPage(overrides: Hook = {}): Hook {
}
beforeEach(() => {
- isPhone = false
mockUseCollections.mockReset()
})
describe('CollectionsPage — shell', () => {
- it('FE-PAGE-COLLPAGE-001: hands a phone viewport to the mobile screen', () => {
- isPhone = true
- mockUseCollections.mockReturnValue(makeHook())
- render(
)
- expect(screen.getByTestId('mobile-collections')).toBeInTheDocument()
- expect(screen.queryByTestId('navbar')).toBeNull()
- })
-
it('FE-PAGE-COLLPAGE-002: renders the desktop shell with hero, rail and list', () => {
renderPage()
expect(screen.getByTestId('navbar')).toBeInTheDocument()
diff --git a/client/src/pages/DashboardPage.desktop.test.tsx b/client/src/pages/DashboardPage.desktop.test.tsx
index 3fd3af78d..3a79a06f7 100644
--- a/client/src/pages/DashboardPage.desktop.test.tsx
+++ b/client/src/pages/DashboardPage.desktop.test.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { http, HttpResponse } from 'msw';
-import { act } from '@testing-library/react';
import { render, screen, fireEvent, waitFor, within } from '../../tests/helpers/render';
import { server } from '../../tests/helpers/msw/server';
import { resetAllStores, seedStore } from '../../tests/helpers/store';
@@ -43,11 +42,6 @@ function installMatchMedia(): void {
});
}
-function setPhone(next: boolean): void {
- phone = next;
- mqListeners.forEach(set => set.forEach(l => l({ matches: next } as MediaQueryListEvent)));
-}
-
const TRIP = buildTrip({ id: 101, title: 'Paris Adventure', start_date: '2026-07-01', end_date: '2026-07-10' });
function onlyTrips(trips: unknown[]) {
@@ -375,14 +369,4 @@ describe('DashboardPage (desktop)', () => {
}
});
- it('FE-PAGE-DESKDASH-017: crossing the phone breakpoint swaps in the mobile dashboard', async () => {
- const { container } = render(
);
- await waitFor(() => expect(screen.getAllByText('Paris Adventure').length).toBeGreaterThan(0));
- expect(container.querySelector('.trek-dash')).toBeInTheDocument();
-
- act(() => { setPhone(true); });
-
- await waitFor(() => expect(container.querySelector('.trek-dash')).toBeNull());
- expect(screen.getByRole('button', { name: 'TREK' })).toBeInTheDocument();
- });
});
diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx
index c43994ce1..be8e70887 100644
--- a/client/src/pages/DashboardPage.tsx
+++ b/client/src/pages/DashboardPage.tsx
@@ -32,8 +32,6 @@ import { convertDistance, getDistanceUnitLabel } from '../utils/units'
import { useSettingsStore } from '../store/settingsStore'
import { useAddonStore } from '../store/addonStore'
import { normalizeAppearance } from '@trek/shared'
-import { useIsPhone } from '../mobile/useIsPhone'
-import MDashboard from '../mobile/screens/dashboard/MDashboard'
import '../styles/dashboard.css'
const GRADIENTS = [
@@ -97,10 +95,9 @@ const RES_ICON: Record
= {
const RES_TYPE_CLASS: Record = { flight: 'flight', hotel: 'hotel', restaurant: 'food' }
export default function DashboardPage(): React.ReactElement {
- // Phones get the mobile screen, everything else the untouched desktop page.
- // Both branches call hooks of their own, so the split lives above them.
- const isPhone = useIsPhone()
- return isPhone ? :
+ // ViewportRoute in App.tsx picks the branch now, so the phone screen is a
+ // chunk of its own instead of a dead limb in this one.
+ return
}
function DashboardPageDesktop(): React.ReactElement {
diff --git a/client/src/pages/InAppNotificationsPage.tsx b/client/src/pages/InAppNotificationsPage.tsx
index e1a4baf7a..ea66e3886 100644
--- a/client/src/pages/InAppNotificationsPage.tsx
+++ b/client/src/pages/InAppNotificationsPage.tsx
@@ -6,12 +6,11 @@ import EmptyState from '../components/shared/EmptyState'
import { Spinner } from '../components/shared/Spinner'
import InAppNotificationItem from '../components/Notifications/InAppNotificationItem.tsx'
import { useInAppNotifications } from './inAppNotifications/useInAppNotifications'
-import { useIsPhone } from '../mobile/useIsPhone'
-import MNotifications from '../mobile/screens/notifications/MNotifications'
export default function InAppNotificationsPage(): React.ReactElement {
- const isPhone = useIsPhone()
- return isPhone ? :
+ // ViewportRoute in App.tsx picks the branch now, so the phone screen is a
+ // chunk of its own instead of a dead limb in this one.
+ return
}
function InAppNotificationsPageDesktop(): React.ReactElement {
diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx
index 444e202f9..aa03cc08c 100644
--- a/client/src/pages/JourneyDetailPage.tsx
+++ b/client/src/pages/JourneyDetailPage.tsx
@@ -23,12 +23,11 @@ import { GalleryView } from '../components/Journey/JourneyDetailPageGalleryView'
import { EntryEditor } from '../components/Journey/JourneyDetailPageEntryEditor'
import { AddTripDialog } from '../components/Journey/JourneyDetailPageAddTripDialog'
import { JourneySettingsDialog } from '../components/Journey/JourneyDetailPageSettingsDialog'
-import { useIsPhone } from '../mobile/useIsPhone'
-import MJourneyDetail from '../mobile/screens/journey/MJourneyDetail'
export default function JourneyDetailPage() {
- const isPhone = useIsPhone()
- return isPhone ? :
+ // ViewportRoute in App.tsx picks the branch now, so the phone screen is a
+ // chunk of its own instead of a dead limb in this one.
+ return
}
function JourneyDetailPageDesktop() {
diff --git a/client/src/pages/JourneyPage.tsx b/client/src/pages/JourneyPage.tsx
index 10dbcd7d7..2839ed81d 100644
--- a/client/src/pages/JourneyPage.tsx
+++ b/client/src/pages/JourneyPage.tsx
@@ -7,8 +7,6 @@ import {
import type { Journey } from '../store/journeyStore'
import { computeJourneyLifecycle } from '../utils/journeyLifecycle'
import { useJourney } from './journey/useJourney'
-import { useIsPhone } from '../mobile/useIsPhone'
-import MJourney from '../mobile/screens/journey/MJourney'
const GRADIENTS = [
'linear-gradient(135deg, #0F172A 0%, #6366F1 45%, #EC4899 100%)',
@@ -24,8 +22,9 @@ function pickGradient(id: number): string {
}
export default function JourneyPage() {
- const isPhone = useIsPhone()
- return isPhone ? :
+ // ViewportRoute in App.tsx picks the branch now, so the phone screen is a
+ // chunk of its own instead of a dead limb in this one.
+ return
}
function JourneyPageDesktop() {
diff --git a/client/src/pages/SettingsPage.tsx b/client/src/pages/SettingsPage.tsx
index 4f9316c78..24bebc758 100644
--- a/client/src/pages/SettingsPage.tsx
+++ b/client/src/pages/SettingsPage.tsx
@@ -14,12 +14,11 @@ import OfflineTab from '../components/Settings/OfflineTab'
import PluginSettingsTab from '../components/Settings/PluginSettingsTab'
import { usePluginStore } from '../store/pluginStore'
import { useSettings } from './settings/useSettings'
-import { useIsPhone } from '../mobile/useIsPhone'
-import MSettings from '../mobile/screens/settings/MSettings'
export default function SettingsPage(): React.ReactElement {
- const isPhone = useIsPhone()
- return isPhone ? :
+ // ViewportRoute in App.tsx picks the branch now, so the phone screen is a
+ // chunk of its own instead of a dead limb in this one.
+ return
}
function SettingsPageDesktop(): React.ReactElement {
diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx
index 6cb68251b..e70960fe2 100644
--- a/client/src/pages/TripPlannerPage.tsx
+++ b/client/src/pages/TripPlannerPage.tsx
@@ -48,9 +48,7 @@ import { ListTodo, Upload, Plus, Trash2, FolderPlus } from 'lucide-react'
import { useTripPlanner } from './tripPlanner/useTripPlanner'
import { usePoiExplore } from '../components/Map/usePoiExplore'
import PoiCategoryPill from '../components/Map/PoiCategoryPill'
-import { useIsPhone } from '../mobile/useIsPhone'
import { useTouchDragBridge } from '../hooks/useTouchDragBridge'
-import MTripShell from '../mobile/screens/trip/MTripShell'
// The tab panels are the planner's dead weight: each one mounts only while its
// own tab is active, so the page chunk carried code most sessions never run. They
@@ -226,11 +224,9 @@ function ListsContainer({ tripId, packingItems, todoItems }: { tripId: number; p
}
export default function TripPlannerPage(): React.ReactElement | null {
- // Below md the trip renders as the new mobile shell; the desktop planner is
- // untouched for tablets and up. Each branch mounts its own useTripPlanner,
- // so no hook is called conditionally here.
- const isPhone = useIsPhone()
- return isPhone ? :
+ // ViewportRoute in App.tsx picks the branch now, so the phone screen is a
+ // chunk of its own instead of a dead limb in this one.
+ return
}
function TripPlannerPageDesktop(): React.ReactElement | null {
diff --git a/client/src/pages/TripPlannerPage.wiring.test.tsx b/client/src/pages/TripPlannerPage.wiring.test.tsx
index be0dc2e9c..8eed8bb8c 100644
--- a/client/src/pages/TripPlannerPage.wiring.test.tsx
+++ b/client/src/pages/TripPlannerPage.wiring.test.tsx
@@ -42,12 +42,6 @@ function props(name: string): Record {
return (captured[name] ?? {}) as Record
}
-const isPhone = vi.hoisted(() => ({ value: false }))
-vi.mock('../mobile/useIsPhone', () => ({ useIsPhone: () => isPhone.value }))
-vi.mock('../mobile/screens/trip/MTripShell', () => ({
- default: () => React.createElement('div', { 'data-testid': 'm-trip-shell' }),
-}))
-
vi.mock('../components/Map/MapViewAuto', () => ({ MapViewAuto: stub('map', 'map-view') }))
vi.mock('../components/Map/MapCompassPill', () => ({ MapCompassPill: stub('compass', 'compass-pill') }))
vi.mock('../components/Map/PoiCategoryPill', () => ({ default: stub('poiPill', 'poi-pill') }))
@@ -301,21 +295,12 @@ beforeEach(() => {
resetAllStores()
for (const key of Object.keys(captured)) delete captured[key]
confirmDialogs.length = 0
- isPhone.value = false
hookState = baseState()
seedStore(useAuthStore, { isAuthenticated: true, user: buildUser({ id: 5 }) })
useTripStore.setState({ loadBudgetItems: vi.fn(async () => undefined) } as never)
})
describe('TripPlannerPage — shell', () => {
- it('FE-PAGE-TPW-001: below the md breakpoint the mobile trip shell replaces the planner', () => {
- isPhone.value = true
- renderPage()
-
- expect(screen.getByTestId('m-trip-shell')).toBeInTheDocument()
- expect(screen.queryByTestId('map-view')).not.toBeInTheDocument()
- })
-
it('FE-PAGE-TPW-002: the splash holds the page until the trip finished loading', () => {
renderPage({ isLoading: true, splashDone: false })
diff --git a/client/src/pages/VacayPage.tsx b/client/src/pages/VacayPage.tsx
index 09e05660b..7401d8a68 100644
--- a/client/src/pages/VacayPage.tsx
+++ b/client/src/pages/VacayPage.tsx
@@ -10,12 +10,11 @@ import VacaySettings from '../components/Vacay/VacaySettings'
import { Plus, Minus, ChevronLeft, ChevronRight, Settings, CalendarDays, AlertTriangle, Eye, Pencil, Trash2, Unlink, ShieldCheck, SlidersHorizontal } from 'lucide-react'
import Modal from '../components/shared/Modal'
import { useVacay } from './vacay/useVacay'
-import { useIsPhone } from '../mobile/useIsPhone'
-import MVacay from '../mobile/screens/vacay/MVacay'
export default function VacayPage(): React.ReactElement {
- const isPhone = useIsPhone()
- return isPhone ? :
+ // ViewportRoute in App.tsx picks the branch now, so the phone screen is a
+ // chunk of its own instead of a dead limb in this one.
+ return
}
function VacayPageDesktop(): React.ReactElement {