// ──────────────────────────────────────────────────────────────────── // StreamHeader geometry — shared constants for the curtain layout. // // Mental model: the chats card is a curtain layered ABOVE the header // (z-index higher). The curtain's `top` is the visible part of the // header below the always-pinned tabs row. When the curtain is fully // closed it sits flush under the tabs row (covering chips + form area // beneath). Dragging it DOWN reveals more of the header from underneath. // Dragging UP raises the curtain back over the header. // // Snap stops (curtain.top, px): // pinned = 0 (curtain sits flush at top of the stage, tabs row // covered; the safe-top status-bar strip above the // stage stays painted by the surrounding context — // see «pinned visual contract» below) // closed = TABS_ROW_PX // peek = TABS_ROW_PX + 2·CHIP_ROW_PX + CHIP_GAP_PX // + CURTAIN_BREATHER_PX // form:* = TABS_ROW_PX + formHeight + CURTAIN_BREATHER_PX // // Pinned visual contract: at `pinned` the curtain's top edge lands at // y = safe-top in viewport coords (because the stage starts after the // PageNav / appBody padding-top: var(--vojo-safe-top)). The system tray // strip stays painted by appBody / PageNav-inner / MobileTabsPager's // static header — all of which use `SurfaceVariant.Container` for that // zone, so the colour is continuous across surfaces. The curtain MUST // NOT extend into the safe-top zone (otherwise system text is covered) // and MUST NOT add internal padding-top (otherwise the chat list grows // visually taller). The clamp on the up-drag (= -TABS_ROW_PX) enforces // the first invariant; we deliberately do not add any padding inside // the curtain to enforce the second. // ──────────────────────────────────────────────────────────────────── // Tabs row height. Always visible above the curtain. export const TABS_ROW_PX = 64; // Each peek-chip row. Reveals one chip's pill (h=48) + 8px top breather. export const CHIP_ROW_PX = 56; // Vertical gap BETWEEN two consecutive chip rows. Separate from // `CURTAIN_BREATHER_PX` so the inter-chip spacing can read tighter // than the breather between the last chip and the curtain's rounded // top (the curtain's straight edge against a chip pill needs more // air to avoid feeling «clamped», while two pills sitting in a // vertical stack want to read as a pair). export const CHIP_GAP_PX = 14; // Initial estimate for the search form's outer height. The actual // height is measured at runtime via ResizeObserver and adapts to the // available viewport so the form never overflows the chats card. export const SEARCH_FORM_BASE_PX = 360; // Breathing strip between the bottom of any header content (revealed // chip pill, form's last actionable element) and the top of the // curtain. Painted by the header's `SurfaceVariant.Container` (light- // blue) so the chip / Create button / search results never visually // touch the curtain's rounded top — the user reads chips that sit // flush with the curtain as «зажатые» rather than two separate // affordances. Not applied at `closed` (nothing to breathe to). export const CURTAIN_BREATHER_PX = 20; // Curtain snap transition. Tuned tight for an in-app reveal — // emphasized-decelerate territory. export const CURTAIN_SNAP_MS = 280; export const CURTAIN_SNAP_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)'; // Curtain card top-corner radius. Matches the composer card and the // horseshoe surfaces elsewhere in the app. export const CURTAIN_RADIUS_PX = 24; // Total vertical travel of the curtain between `closed` and `peek` — // the resting-top delta between the two snaps. Used as the basis for // the peek-commit threshold: the user must drag (rubber-banded) at // least COMMIT_THRESHOLD × PEEK_TRAVEL_PX before release for the snap // to flip. Anything shorter reads as accidental and springs back. export const PEEK_TRAVEL_PX = CHIP_ROW_PX + CHIP_GAP_PX + CHIP_ROW_PX + CURTAIN_BREATHER_PX; // Touch gesture tuning. RUBBER_BAND dampens finger→curtain motion so // the chip reveal feels resistive; COMMIT_THRESHOLD is the fraction of // the full peek travel the user must cross on release for the snap to // commit. Tuned high (≈90%) so anything below «дотянул почти до конца» // reads as accidental and snaps back to `closed`. export const RUBBER_BAND = 0.65; export const DIRECTION_DEAD_ZONE_PX = 10; export const COMMIT_THRESHOLD = 0.9; // Pull-up distance (raw finger px) required to close an active form. export const ACTIVE_CLOSE_THRESHOLD_PX = 100; // Total vertical CURTAIN travel for the closed ↔ pinned gesture. // Equals the tabs row height because pinning lifts the curtain by // exactly that distance (from y = TABS_ROW_PX down to y = 0 inside // the stage). export const PIN_TRAVEL_PX = TABS_ROW_PX; // Commit threshold for pin / unpin. Tuned very high (≈95%) so the // user must drag the curtain almost-all-the-way to the cap before // release for the snap to flip. Anything shorter reads as accidental // and springs back to the previous resting snap. // // With 1:1 finger ↔ curtain tracking (no rubber-band on pin / unpin // — see `useCurtainGesture` and `useCurtainHandleGesture`), the // committing finger pull is `PIN_COMMIT_THRESHOLD × PIN_TRAVEL_PX` ≈ // 61 px — essentially «drag the curtain across the full tabs-row // height». The anti-accidental gate that previously came from // rubber-band amplification is now provided by the dedicated handle // hit-zone (intentional surface) plus the list-bound scroll-aware // bail (no list scroll = no scroll-up to confuse with pin). export const PIN_COMMIT_THRESHOLD = 0.95; // Drag-handle hit-zone at the top of the curtain. Hosts the pin / // unpin gesture as a dedicated touch surface so it doesn't compete // with the chat list's vertical scroll. Drag on this handle tracks // the finger 1:1 (no rubber-band) — finger displacement equals // curtain displacement. The list-bound gesture in // `useCurtainGesture` still owns peek / form-close, plus the pin path // when the list has no scrollable content (so single-screen lists // keep the «drag-from-anywhere» pin behaviour). // // Size: 32 px tall — enough touch target to land on comfortably with // a thumb (the visible grabber pill inside is much smaller, see // `StreamHeader.css.ts::handleBar`). The list (or DirectEmpty / the // equivalent placeholder) starts 32 px below the curtain's top edge. export const HANDLE_HEIGHT_PX = 32;