vojo/src/app/components/stream-header/geometry.ts

123 lines
6.7 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.

// ────────────────────────────────────────────────────────────────────
// 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;