// ──────────────────────────────────────────────────────────────────── // 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. // // On the handle the up direction is 1:1 with no upper clamp (the // «closed-free» transition spans the full pin↔closed↔peek range in // one gesture and the curtain follows the finger off-screen freely); // the committing curtain DISPLACEMENT is still // `PIN_COMMIT_THRESHOLD × PIN_TRAVEL_PX` ≈ 61 px — essentially «drag // the curtain across the full tabs-row height». On the body the same // displacement is reached with a longer finger pull because the body // path is rubber-banded (×0.65). // // Unpin's clamp is asymmetric — `pinned-free` lower-bounds the live // delta at 0 (no destination above pinned) but leaves the upper // direction unclamped so the same gesture can carry the curtain // through closed into peek territory in one motion. The handle-only // contract on unpin means the body never resolves to `pinned-free`, // so the no-upper-clamp tolerance only applies on the dedicated // drag-handle. export const PIN_COMMIT_THRESHOLD = 0.95; // Drag-handle hit-zone at the top of the curtain. NATIVE-ONLY: the // handle is rendered only when `isNativePlatform()` is true (see // StreamHeader.tsx) — on web (desktop / Electron) the curtain has // no interactive snap states, so the handle would be pure // decoration and is omitted entirely. // // On native the handle is the AUTHORITATIVE gesture surface — // closed-free / unpin / close-peek / form-close all bind here with // 1:1 finger ↔ curtain tracking, no matter whether the chat list // inside the curtain is scrollable. See `useCurtainHandleGesture` // for the full state machine. // // A parallel `useCurtainBodyGesture` bound to the curtain's body // handles drag from anywhere on the card, but only when the inner // chat list has no scrollable content AND the curtain isn't pinned // (unpin is handle-only). Its dynamics are rubber-banded so the // body drag reads as physically «heavier» than the handle's crisp // pull. // // 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 // on native; on web the list sits flush at the curtain's top. export const HANDLE_HEIGHT_PX = 32;