import { style } from '@vanilla-extract/css'; import { color } from 'folds'; // Pager root. Sits inside the authed shell's row-flex slot // (ClientLayout → Box grow=Yes), so `flex: 1 1 0` fills the slot // horizontally; `align-items: stretch` on the parent fills vertically. // // `touch-action: pan-y` lets the browser keep doing native vertical // scroll (DM list virtualizer, curtain peek pull-down) without us // having to call preventDefault on every move — only the pager's own // listener calls preventDefault, and only after axis-resolve commits // to "horizontal". // // `SurfaceVariant.Container` backdrop intentionally shows through // (a) the inter-pane gap during a swipe — the gap colour the user // asked for is "light blue same as the header", which IS this // SurfaceVariant tone — and (b) any sub-pixel rounding seam at rest. // // `color: Background.OnContainer` mirrors what the route-level // `` would have set via its `ContainerColor({ variant: // 'Background' })` wrapper. We mount Direct/Channels/Bots directly // here, bypassing PageRoot for the swipe-pager experience, so without // this declaration the form labels rendered inside the per-pane // StreamHeader curtain (`` for Username/Server/ // Options) had `color: inherit` cascading all the way up to `body`, // which sets no color → browser default black. The labels became // invisible against the dark form background only on native (where the // pager activates). Desktop / web go through PageRoot and inherit the // expected light tone for free. export const pagerRoot = style({ position: 'relative', flex: '1 1 0', minWidth: 0, minHeight: 0, height: '100%', overflow: 'hidden', touchAction: 'pan-y', backgroundColor: color.SurfaceVariant.Container, color: color.Background.OnContainer, }); // Shared static tabs row painted BEHIND the strip in DOM order. // Reserves the status-bar safe-area inset via padding-top so the // segments + icons sit just below the system status bar, and so the // backdrop colour extends through the inset zone (matching the per-pane // PageNav's own `paddingTop: var(--vojo-safe-top)` so there's no // visible band boundary at the inset edge). // // Curtain-overlay invariants (why no z-index here at rest): // // The chats curtain must visually rise ABOVE this header when the // user pulls it up to the «pinned» snap — like a real blind sliding // over the segments rather than the segments moving. The curtain // lives inside each pane > stage with `z: 2` in stage's local // stacking context. The stage / pane stack inside the swipe `strip`, // which creates its own stacking context via `transform`. To let the // curtain visually surface above this static header we: // // (a) leave both elements at `z-index: auto` in pagerRoot's // stacking context (this block has no `zIndex` AT REST), so // painting order falls back to DOM order. `pagerStaticHeader` // is rendered BEFORE `strip` in MobileTabsPager — so the // strip (and everything inside it that paints opaquely) paints // on top. // // (b) tag the strip with `data-pager-pane="true"`. All per-pane // background paints (PageNav-inner surface, MobileSettings/ // ChannelsWorkspace appBody, StreamHeader stage + header) // become transparent under that selector, so the static // header tabs show through every transparent layer of the // strip until the curtain — the only remaining opaque element // — covers them by being positioned at top: 0 (pinned snap). // // Breaking (a) or (b) re-introduces the «paravozik» regression where // the tabs visually slide with the curtain. See git history for the // user-feedback trail. // // Conditional z-elevation (horseshoe-active override, suppressed by pin): // // When a horseshoe sheet (Settings or workspace switcher) is // geometrically active — i.e. `expandedPx > 0`, which covers both // the in-flight drag and the committed-open state — the wrapping // container paints `VOJO_HORSESHOE_VOID_COLOR` (= #000 in dark // theme) across the entire pane so the carve at the sheet's top // reads as a dark seam. With the transparent strip stack from (b), // that void would bleed up through the safe-top + tabsRow zone, // turning the system-tray strip + tabs solid black. // // `MobileTabsPagerHeader.tsx` bumps this element to a positive // `zIndex` (inline style, driven by `mobileHorseshoeActiveAtom`) // from the first frame of drag. Positive z beats the strip's // `z: auto` stacking context, putting the static header back on // top in the safe-top + tabsRow band — the void is contained to // the carve area, tabs stay visible. The horseshoe's `appBody` // flips back to opaque on the same signal so the void doesn't // bleed into the chip-area band between the static header and // the curtain top either. The curtain pin gesture is gated off // in the same state (see `StreamHeader.gestureDisabled`) so no // pin can race the elevation flip. // // Pinned-override: when the active pane's curtain is pinned, the // curtain itself sits at the top of the stage (z:2 inside the // strip's stacking ctx) and covers everything from `y = safe-top` // downward — including the tabsRow band. Above the curtain // (y=0..safe-top) the opaque appBody contains the void. Elevating // the static header in that state would visibly slice the pinned // curtain in the tabsRow band, popping tabs over what the user // explicitly pulled up to cover. So `MobileTabsPagerHeader` // suppresses elevation whenever `curtainPinnedByTabAtom[activeTab]` // is true — preserves the «pinned hides tabs» invariant across // sheet open/drag without re-introducing the void leak. export const pagerStaticHeader = style({ position: 'absolute', top: 0, left: 0, right: 0, paddingTop: 'var(--vojo-safe-top, 0px)', // The wrapped tabsRow has its own height of TABS_ROW_PX via the // stream-header recipe; we don't set a fixed height here so the // status-bar inset adds on top naturally. backgroundColor: color.SurfaceVariant.Container, }); // Horizontal strip carrying all three panes side-by-side. Width & // transform are computed inline in the JSX (they depend on tabs.length // and visualIdx + visualDragPx, and the gap math couples to them). // // `gap: PANE_GAP_PX` is what makes the inter-pane void visible during // a swipe — the pagerRoot's SurfaceVariant.Container colour shows // through the gap, matching the static header tone exactly. export const strip = style({ display: 'flex', flexDirection: 'row', height: '100%', willChange: 'transform', }); // Each pane is exactly one viewport wide. CRITICALLY `display: flex; // flex-direction: row` so the nested Folds PageNav (which is a flex // child with `flex-grow: 1` on mobile to override its 256px recipe // width) expands to fill the pane. A column-flex parent here would // leave PageNav at 256px — the bug that ate the previous attempt. // // No paddingTop here: the per-pane StreamHeader still renders its // own tabs row (kept for the curtain's TABS_ROW_PX snap math, just // painted invisible via visibility:hidden), and PageNav's inner // column reserves the status-bar safe-area inset via its own // `paddingTop: var(--vojo-safe-top)`. The static header overlay at // the pager root simply paints OVER the same screen zone, so the // underlying geometry stays identical to non-pager mode. export const pane = style({ display: 'flex', flexDirection: 'row', flexShrink: 0, width: '100vw', height: '100%', minWidth: 0, });