203 lines
8.8 KiB
TypeScript
203 lines
8.8 KiB
TypeScript
import { style } from '@vanilla-extract/css';
|
||
import { color, toRem } from 'folds';
|
||
import { VOJO_HORSESHOE_GAP_PX, VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe';
|
||
|
||
// Re-exported so the TSX can pick up the constants without crossing
|
||
// the vanilla-extract / runtime boundary twice.
|
||
export const HORSESHOE_RADIUS_PX = VOJO_HORSESHOE_RADIUS_PX;
|
||
export const HORSESHOE_GAP_PX = VOJO_HORSESHOE_GAP_PX;
|
||
|
||
// Outer container — `position: relative` anchor for the two absolutely-
|
||
// positioned panes (`appBody` and `silhouette`). `overflow: hidden`
|
||
// clips anything that overflows the wrapper's bounds and crops the
|
||
// rounded carves on both panes against the container's bg (which is
|
||
// painted with `VOJO_HORSESHOE_VOID_COLOR` inline when the sheet is
|
||
// active, so the carved-out areas read as the same near-black seam
|
||
// used everywhere else in the app).
|
||
//
|
||
// `flex: 1` so the container fills whatever flex slot it's mounted in
|
||
// (PageNav's inner column for the Direct route).
|
||
//
|
||
// `marginTop: -var(--vojo-safe-top)` extends the container UP over the
|
||
// status-bar safe-top zone reserved by `PageNav` via `padding-top`,
|
||
// and the compensating `paddingTop: var(--vojo-safe-top)` on `appBody`
|
||
// keeps the wrapped DM list anchored at the same visual Y as before
|
||
// the shift. The combination has two load-bearing effects:
|
||
//
|
||
// (1) The settings-sheet clip-path mask on `appBody` carves rounded
|
||
// BL/BR into an opaque surface that already paints THROUGH the
|
||
// status-bar strip — without the upward extension the carve
|
||
// would visibly stop at the bottom of the system-tray strip.
|
||
// (2) `appBody`'s bg paints the safe-top strip itself in the same
|
||
// `SurfaceVariant.Container` tone as `PageNav-inner` / the
|
||
// pager's static header, giving the system-tray text a
|
||
// consistent backdrop across surfaces.
|
||
//
|
||
// Note: the curtain itself NEVER paints into the safe-top zone — the
|
||
// pin gesture's hard clamp at `-PIN_TRAVEL_PX = -TABS_ROW_PX` stops
|
||
// the curtain at `top: 0` of the stage (= `y = safe-top` in viewport),
|
||
// preserving the system-tray strip. See
|
||
// `components/stream-header/geometry.ts::PIN_TRAVEL_PX` and the
|
||
// «pinned visual contract» comment for the full invariant.
|
||
export const container = style({
|
||
position: 'relative',
|
||
display: 'flex',
|
||
flex: 1,
|
||
flexDirection: 'column',
|
||
minWidth: 0,
|
||
minHeight: 0,
|
||
overflow: 'hidden',
|
||
marginTop: 'calc(-1 * var(--vojo-safe-top, 0px))',
|
||
});
|
||
|
||
// === App body === Holds the wrapped children (the DM list — header,
|
||
// scroll content, DirectSelfRow). Fills the container via `inset: 0`.
|
||
// Does NOT translate or shrink — the DM list stays exactly where it
|
||
// was in the closed state. Instead, the bottom of the pane is masked
|
||
// away by an animated `clip-path:
|
||
// inset(...)` with rounded BL/BR corners — the user sees the visible
|
||
// top portion of the DM list with a rounded carve at the new bottom
|
||
// edge, exactly like the profile horseshoe shows the chat with a
|
||
// rounded TOP carve as the panel masks it from above.
|
||
//
|
||
// Why clip-path rather than flex-shrink + margin-bottom: a flex-
|
||
// shrink approach changes the scroll-container's height every render,
|
||
// and the DM list's `@tanstack/react-virtual` re-measures items mid-
|
||
// gesture. Why not `transform: translateY` either: translating moves
|
||
// the whole pane up off the top of the viewport, including the
|
||
// StreamHeader the user wants to keep visible. Clip-path leaves
|
||
// layout unchanged — the DM list keeps its scroll position, its
|
||
// measured heights, and its top items in place; only the bottom edge
|
||
// of what's visible gets carved into the void below.
|
||
//
|
||
// `backgroundColor: SurfaceVariant.Container` is load-bearing on two
|
||
// counts: (1) it must be OPAQUE so the container's void colour
|
||
// (painted inline when the sheet is active) doesn't bleed through gaps
|
||
// between DM list rows; (2) the safe-top padding region of THIS
|
||
// element is what paints the system-tray strip when the wrapper
|
||
// container is extended up over it (see `container.marginTop` above).
|
||
// Picking `SurfaceVariant.Container` (not `Background.Container`)
|
||
// matches the Bots / ChannelsRoot status-bar tone exactly — Bots
|
||
// renders `PageNav-inner.bg = SurfaceVariant.Container` in the safe-
|
||
// top zone, and the StreamHeader curtain (`Background.Container`)
|
||
// overpaints that lighter strip with the darker tone as it's dragged
|
||
// up. Mirroring the same two tones here gives Direct the same visible
|
||
// «curtain darkens the strip» transition the user expects.
|
||
//
|
||
// `flex: column` so the children (which expect a flex column parent
|
||
// — PageNav uses it) still stack naturally. `paddingTop:
|
||
// var(--vojo-safe-top)` reserves status-bar space INSIDE appBody so
|
||
// the wrapped StreamHeader.stage starts at the same Y as before the
|
||
// `container.marginTop` shift extended us upward.
|
||
export const appBody = style({
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
minWidth: 0,
|
||
minHeight: 0,
|
||
backgroundColor: color.SurfaceVariant.Container,
|
||
paddingTop: 'var(--vojo-safe-top, 0px)',
|
||
willChange: 'clip-path',
|
||
});
|
||
|
||
// === Silhouette === The Settings sheet's surface. Anchored at the
|
||
// bottom of the container; its height animates 0 → railHeight as the
|
||
// user drags up. Rounded TL/TR carve the top edge against the void
|
||
// gap between it and the translated-up appBody.
|
||
//
|
||
// `overflow: hidden` clips `panelContent` (which is railHeight tall,
|
||
// top-anchored) so the visible portion of the panel is just the
|
||
// silhouette's current height — the user sees more of the panel
|
||
// content reveal from the top as silhouette grows.
|
||
//
|
||
// Background: `SurfaceVariant.Container` (Dawn bg = #181a20) — the
|
||
// chat-pane tone, same as the Settings PageNav inside (set via
|
||
// `surface="surfaceVariant"`). Load-bearing: the silhouette's bg
|
||
// shows through the 20px drag-handle band at the top so the seam
|
||
// matches the PageNav tone. With `Background.Container` (#0d0e11)
|
||
// here, the user saw a dark stripe at that seam on Samsung S24
|
||
// edge-to-edge; matching silhouette to the PageNav tone closes it.
|
||
// Same idea as commit 77bb72d which dynamically retunes
|
||
// `--vojo-safe-area-bg` while a Room is mounted to keep the
|
||
// system-bar strips and the chat surface in lockstep.
|
||
export const silhouette = style({
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
overflow: 'hidden',
|
||
backgroundColor: color.SurfaceVariant.Container,
|
||
willChange: 'height, border-top-left-radius, border-top-right-radius',
|
||
});
|
||
|
||
// Anchored at the TOP of `silhouette` so as silhouette grows from 0
|
||
// upward, more of the panel content reveals from the top down. The
|
||
// handle and Settings title are at the top of `panelContent`, so they
|
||
// are visible at the very first pixel of drag — gives the user
|
||
// immediate "I'm opening Settings" feedback.
|
||
//
|
||
// Android nav-bar clearance is owned by `MainActivity.java`'s
|
||
// WindowInsets listener (the WebView is already sized above the nav
|
||
// bar). No `padding-bottom: env(...)` here — that would stack on top
|
||
// of the native padding and clip the Settings menu's bottom rows.
|
||
export const panelContent = style({
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
});
|
||
|
||
// Drag handle band — sits at the TOP of `panelContent` so it appears
|
||
// at the top edge of the emerging sheet as the user drags up. Mirror
|
||
// of the profile horseshoe handle (20px tall + 36×4 grabber). The
|
||
// whole band is the ONLY drag-to-close target — touches on the
|
||
// panel body below this strip do NOT initiate drag, so Settings's
|
||
// internal scroll works without conflict.
|
||
//
|
||
// `touchAction: none` ensures the browser doesn't try to scroll
|
||
// when the user drags on the handle (the whole gesture is ours).
|
||
export const panelHandle = style({
|
||
flexShrink: 0,
|
||
height: toRem(20),
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
cursor: 'grab',
|
||
touchAction: 'none',
|
||
userSelect: 'none',
|
||
selectors: {
|
||
'&:active': { cursor: 'grabbing' },
|
||
},
|
||
});
|
||
|
||
export const panelHandleBar = style({
|
||
width: toRem(36),
|
||
height: toRem(4),
|
||
borderRadius: toRem(4),
|
||
// Darker than the silhouette's SurfaceVariant bg so the grabber
|
||
// reads against the panel without competing with content. The
|
||
// Dawn `Background.Container` (#0d0e11) is one notch deeper than
|
||
// `SurfaceVariant.Container` (#181a20) — same step-down used by
|
||
// chat bubbles vs the chat surface.
|
||
backgroundColor: color.Background.Container,
|
||
});
|
||
|
||
// Holds the Settings tree. `flex: 1` so it grows to fill the
|
||
// remaining `panelContent` height below the handle. No drag listeners
|
||
// here — Settings's own internal scroll handles touch events; the
|
||
// drag-to-close gesture is reserved for `panelHandle`.
|
||
export const panelBody = style({
|
||
flex: 1,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
minHeight: 0,
|
||
minWidth: 0,
|
||
});
|