vojo/src/app/components/mobile-tabs-pager/style.css.ts

158 lines
7.5 KiB
TypeScript

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
// `<PageRoot>` 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 (`<Text size="L400">` 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,
});