vojo/src/app/features/settings/MobileSettingsHorseshoe.css.ts

203 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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