142 lines
7.6 KiB
TypeScript
142 lines
7.6 KiB
TypeScript
// ────────────────────────────────────────────────────────────────────
|
||
// 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;
|