mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
4dfccaa2a4
From the large-screen review (code findings H1, M1, M2, M3, M4, M7, L1, L3, L4, L6, L7): - The size class is one value in JavaScript (hooks/sizeClass.ts) and the stylesheet follows it: MobileShell writes it to the shell as data-size, the wide:/split: variants and mobile.css key on that instead of media queries of their own. A window that only grows shorter at the same width while a field has focus is Android's soft keyboard, not a new window, so the rail, the form sheets and the panes no longer fold away (and take the focused field with them) while one types. - The large-screen layout is the app's only. In a browser the touch shell is the phone's below 768px and stays compact there, as before this branch. - Shortcuts are the app's only, so a browser keeps Ctrl+1 and Alt+Left. The command key is the platform's own (Cmd on Apple, Ctrl elsewhere), digits are matched by the key's place as well (AZERTY), and nothing runs while a sheet or another modal dialog is open: the page listens to nothing and the app is handed no key commands, so Cmd+[ can no longer drop an unsaved form. - Inside a trip Cmd+N belongs to the trip shell and creates what the tab on show creates, named after it. - Inside the touch shell the desktop navbar of the few PageShell routes is hidden and --nav-h is 0. - The rail comes before the page in the DOM, so a keyboard reaches it first. - The phone's deep day colour for a journey card without a photo stays the phone's; the desktop and shared timelines keep their faint wash. Its card is opaque, a suggestion included, so the map's names do not read through. - The trip widget's iPad mini layout keys on the iPad mini's width alone. - The trip dock gets the scroll-edge strip and the denser dock glass the global dock has. - Duplicate test id FE-MOB-PANES-016 renumbered.
106 lines
5.4 KiB
TypeScript
106 lines
5.4 KiB
TypeScript
import { CSSProperties, ReactNode, useRef } from 'react'
|
|
import { useMatch } from 'react-router'
|
|
import BottomNav from '../components/Layout/BottomNav'
|
|
import ErrorBoundary from '../components/shared/ErrorBoundary'
|
|
import MBottomNav from './components/MBottomNav'
|
|
import MNavRail from './components/MNavRail'
|
|
import { useSizeClass } from '../hooks/sizeClass'
|
|
import MKeyboardHost from './keyboard/MKeyboardHost'
|
|
import MShellShortcuts from './keyboard/MShellShortcuts'
|
|
import MToastHost from './components/MToastHost'
|
|
import MEdgeBack from './components/MEdgeBack'
|
|
import { useNativeCapabilities } from '../native/capabilities'
|
|
import { isNativeIos } from '../native/platform'
|
|
import { useWindowFold } from '../native/windowFold'
|
|
import './mobile.css'
|
|
|
|
interface MobileShellProps {
|
|
isPhone: boolean
|
|
children: ReactNode
|
|
}
|
|
|
|
/**
|
|
* Shell around every protected page. Below the md breakpoint it is the root
|
|
* of the new mobile experience: scopes the --m-* design tokens, paints the
|
|
* screen gradient, hosts the bottom dock, the sheet portal target and the
|
|
* mobile toast presenter. From 768px up it renders the exact legacy wrapper
|
|
* unchanged. Both branches share the div > div > children shape so pages keep
|
|
* their state when the viewport crosses the breakpoint (rotation,
|
|
* split-screen) instead of remounting.
|
|
*
|
|
* On the phone the shell deliberately owns no scroll container (#1809): the
|
|
* document is the scroller, because that is the only movement iOS Safari
|
|
* minimises its address bar for. Screens that want a full-viewport layout
|
|
* (map screens, the trip planner) set their own height instead of borrowing
|
|
* one from here.
|
|
*/
|
|
export default function MobileShell({ isPhone, children }: MobileShellProps) {
|
|
// The trip planner (/trips/:id) is a full-screen takeover that ships its own
|
|
// in-trip dock (MTripShell). The global dock must not render underneath it:
|
|
// on iOS Safari a position:fixed nav paints THROUGH the trip overlay's higher
|
|
// stacking context, so the wrong (global) bar shows on top of the trip screen.
|
|
const inTripPlanner = useMatch('/trips/:id')
|
|
// The app's own edge swipe back, where the binary has turned WebKit's off.
|
|
const { caps } = useNativeCapabilities()
|
|
const pageRef = useRef<HTMLDivElement>(null)
|
|
const edgeBack = isNativeIos() && caps.features.includes('edgeBack')
|
|
// On a wide window (an iPad, a foldable unfolded) the rail on the leading edge
|
|
// replaces the dock, and the page column starts after it. The class goes on the
|
|
// root as data-size: the wide:/split: variants and mobile.css key on it.
|
|
const sizeClass = useSizeClass()
|
|
const wide = sizeClass !== 'compact'
|
|
// A foldable's fold, as the Android app reports it: two-pane screens end their
|
|
// leading pane there (mobile.css, data-fold).
|
|
const fold = useWindowFold()
|
|
const foldStyle = fold
|
|
? ({ '--m-fold-start': `${fold.start}px`, '--m-fold-end': `${fold.end}px` } as CSSProperties)
|
|
: undefined
|
|
|
|
if (!isPhone) {
|
|
return (
|
|
<div className="flex flex-col h-dvh md:block md:h-auto">
|
|
<div className="flex-1 overflow-y-auto md:overflow-visible">{children}</div>
|
|
<ErrorBoundary boundaryId="chrome:bottom-nav" fallback={null}><BottomNav /></ErrorBoundary>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
// min-h-svh, not min-h-dvh: dvh grows while Safari's toolbar retracts, so a
|
|
// dvh page length would chase the animation it just triggered.
|
|
<div className="m-root m-shell flex min-h-svh flex-col text-m-ink" data-size={sizeClass} data-fold={fold ? 'vertical' : undefined} style={foldStyle}>
|
|
{/* Background and screen gradient live on their own viewport-sized layer.
|
|
The shell root is as tall as the document now, and --m-scr is a radial
|
|
gradient measured against its element box, so on it the light cone would
|
|
be stretched over the whole scroll length.
|
|
|
|
It stays on a NEGATIVE z-index, and the content wrapper stays unpositioned
|
|
on purpose. Lifting the layer into the positioned range means the content
|
|
has to be positioned too, which makes it a stacking context and clamps
|
|
every fixed overlay a screen renders inside it: the vacay view/edit FAB
|
|
(z-50) ended up underneath the dock (z-40) that way. For the negative
|
|
layer to be visible, body must not paint over it, which index.css handles
|
|
for this breakpoint. */}
|
|
<div aria-hidden className="pointer-events-none fixed inset-0 -z-10 bg-[color:var(--m-bg)] bg-[image:var(--m-scr)]" />
|
|
{/* The rail comes before the page in the document, as it does on screen,
|
|
so a keyboard (Full Keyboard Access on an iPad) reaches the navigation
|
|
first. It is fixed, so where it sits in the DOM moves nothing. */}
|
|
{!inTripPlanner && wide && (
|
|
<ErrorBoundary boundaryId="chrome:m-nav-rail" fallback={null}><MNavRail /></ErrorBoundary>
|
|
)}
|
|
<div ref={pageRef} className="flex-1 wide:ps-[var(--m-rail-w)]">{children}</div>
|
|
<MEdgeBack target={pageRef} enabled={edgeBack} />
|
|
{!inTripPlanner && !wide && (
|
|
<ErrorBoundary boundaryId="chrome:m-bottom-nav" fallback={null}><MBottomNav /></ErrorBoundary>
|
|
)}
|
|
<ErrorBoundary boundaryId="chrome:m-toast" fallback={null}><MToastHost /></ErrorBoundary>
|
|
{/* A hardware keyboard (an iPad's, mostly) gets the shortcuts an iPad app has. */}
|
|
<ErrorBoundary boundaryId="chrome:m-keyboard" fallback={null}>
|
|
<MKeyboardHost />
|
|
<MShellShortcuts />
|
|
</ErrorBoundary>
|
|
<div id="m-sheet-root" />
|
|
</div>
|
|
)
|
|
}
|