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, });