/** * dsh-widgets — rail geometry: the space the rail may claim, how cards are sized * inside it, and the anchors both the deck and the magnification wave read. * * Pure reads of the shell's own measurements (`--dsh-chat-*` custom properties, the * frame's inline grid template) plus the sizing maths — no React, no listeners, and * no state beyond `engagedPanelW`, which only `resolveRailSpace` touches. */ import type { Prefs } from '../runtime/prefs'; /** * Whether the rail can follow the shell's column track NATIVELY through CSS * anchor positioning (`right: anchor(--dsx-center right)`) instead of being * repositioned from JS every frame. Both the rail and its magnify overlay are * `position: fixed` and the anchor is the AppFrame's center column * (`[class$='_centerCol']`, declared in widgets.module.css), so the browser * resolves the rail's right edge inside the very layout pass that animates * `grid-template-columns`: the rail and the conversation column move as ONE * surface —no tween, no frame lag, and none of the per-frame * `--dsx-rightbar-w` writes (a :root style invalidation, i.e. a full-document * style recalc every animation frame) the fallback path needs. */ export declare const ANCHOR_FOLLOW: boolean; /** * Throw away every memoised shell read. Called by the observer paths in `measure.ts` * (a resize, a track transition, a drag) and by the frame boundary below. */ export declare function invalidateRailMetrics(): void; /** The epoch the memo is currently serving — a cheap cache key for derived values. */ export declare function railMetricsEpoch(): number; /** Publish the rail's `top` offset (see `notedRailTop`). Path is derived, never measured. */ export declare function noteRailTop(top: number): void; /** Forget the published `top`, so `rowFitSide` goes back to reading the DOM. */ export declare function clearRailTop(): void; /** * The published rail `top`, or `-1` when it was never published. * * Callers that need the rail's HEIGHT derive it with `window.innerHeight` (see * `notedRailTop`): the rail's box is `top: var(--dsx-rail-top); bottom: 0`, so * the subtraction is exact and costs no layout read. The rail's own pane-height * observer used `.dsx-stats-rail.clientHeight` instead, and that read landed * inside the shell's React commit — a whole-document forced layout per commit * while the sidebar animated (measured 2026-10-03, E2 profile: 130.9ms of * self-time, the largest own-plugin item of the frame). */ export declare function getNotedRailTop(): number; export interface WavePlace { s: number; top: number; right: number; w: number; h: number; } /** * Where row 0's cards sit inside the rail's scroll content (`placeCards` seats the * first row at 2px). Detents are therefore `RAIL_ROW_SEAT + row · pitch`: this is the * ONE formula the wheel, the row guard and the verification probes share, so a row can * never rest 2px away from where another code path expects it. */ export declare const RAIL_ROW_SEAT = 2; export declare const BASE_SIDE = 150; /** * The transcript sits inside the scroll box with ~36px of side padding, and the * scroller itself is ~2px narrower than the track (measured 2026-09-17: at a * 1280 viewport the box was 800px and the prose 728px, i.e. 72px of inset; at * 1578 the prose stayed capped at its 748px measure). Without this term the * rail still shaved 20-50px off the measure. */ export declare const RAIL_BOX_INSET = 74; /** * Conversation column width (px), 0 while the shell has not mounted it. * * Geometry first: the shell publishes --dsh-conversation-column-width a beat * AFTER a track transition settles, so a variable-only read can still see the * mid-flight column (measured 2026-09-17: closing the right panel left the * budget stuck at 0 because the read happened during that window). */ export declare function readColumnWidth(): number; /** Read the official transcript measure / column width off the conversation root. */ export declare function readRailBudget(): number; /** * Root custom-property write that skips identical values. * * Writing a custom property on `document.documentElement` invalidates every * declaration that depends on it, so an unconditional write buys a full-document * style recalc even when the value did not move. The rail's geometry is written * from two paths that both run on the toggle's hottest beats — the render body * (rail-view.tsx) and the direct-DOM yield beat (`updateRailBudget` in * measure.ts) — and each of them re-derives the SAME value on most passes, so an * unguarded write there is a per-pass document-wide invalidation for no change. * * Measured 2026-10-03 at the owner's stage (1707×1067 @ DSF 1.5, two open/close * cycles, `.tmp-fold-filmstrip.cjs` instrumenting `setProperty`): the rail's own * writes landed 20 times, of which 18 carried the value already in the style * declaration (`--dsx-rail-pad` 6/6 identical, `--dsx-rail-overshoot` 6/6, * `--dsx-rail-w` 6 of 8). Every one of those 18 was a whole-document style * invalidation priced into the fold's frame budget. * * Identical-value writes are what this skips; it never reorders or batches, so a * caller can keep writing from wherever the value is known (including a render * body, where React has not committed yet — see the rail's own note). */ export declare function setRailVar(name: string, value: string): void; /** * Right inset every rail-owned fixed layer reads. Normal mode follows the column * (anchor positioning); while a panel is present the rail is pinned to the * viewport's right edge —the same value whenever no panel is open —so the * panel, which paints ABOVE the rail's conversation-scoped slot, covers it. */ export declare function applyRailRight(swallowed: boolean): void; /** Everything the rail's fixed layers need to know for the current space. */ export interface RailSpace { /** The transcript must yield its margin (claim 0). */ yielded: boolean; /** A panel is present and will cover the rail (paint order: panel above rail). */ swallowed: boolean; /** No room and nothing to cover it: hide. */ hidden: boolean; /** Right inset for the rail / magnify / add panel. */ right: string; /** Width the rail draws. */ drawW: number; /** * Extra rightward shift (px) so a panel NARROWER than the rail can still cover * it completely: the rail's right edge is pinned to the viewport, so shifting * it right by (rail –抪anel) lands its left edge exactly on the panel's left * edge. Zero whenever the panel is at least as wide as the rail. */ shiftX: number; /** Space the transcript yields to the rail. */ claimW: number; side: number; columns: number; pad: number; } /** * Decide how the rail coexists with the right panel and the transcript measure. * * Resolution order: the preferred deck —what still fits the transcript's * leftover margin —if nothing does, the panel takes the space and the rail is * COVERED by it (the rail's slot is inside the conversation, which paints below * the right column), or hidden when there is no panel to do the covering. */ export declare function resolveRailSpace(prefs: Prefs, budget: number): RailSpace; /** Budget implied by the track the frame is animating toward (null = unknown). */ export declare function predictRailBudget(): number | null; /** * Smallest card side the rail will shrink to on its own. * * In the fluid layout (see resolveRailLayout) this floor only applies to the * NARROW case —a single column whose `1fr` share falls below the base size — * and it is what separates "shrink and stay" from "yield entirely". It is * derived from the rail's own box via `rowFitSide`, not guessed; see * RAIL_MIN_ROWS for the derivation. The card-size slider goes down to * RAIL_MIN_SIDE (100), so a smaller explicit preference always wins. */ export declare function readMinCardSide(pad: number): number; /** * The largest card side the rail may grow a card to on its own (see * RAIL_MAX_ROWS). An explicit base size larger than this is still honoured: the * ceiling only limits the AUTOMATIC growth, never the user's own preference. */ export declare function readMaxCardSide(pad: number, base: number): number; /** * The rail geometry that fits a budget —a fluid grid: the industry-standard * `grid-template-columns: repeat(auto-fill, minmax(base, 1fr))`, capped by the * user's column count and by the five-row ceiling (see readMaxCardSide). * * columns = min(pref.columns, the most columns whose cells still hold `base`) * side = the leftover width shared by those columns, SNAPPED to * CARD_SIZE_STEP tiers at or above `base`, then clamped to the * ceiling (itself snapped onto the same tier grid) * * The two settings + the gap therefore compose into ONE rule: * * side = clamp(autoFloor, tier((room –(n+1)·gap) / n), fiveRowCeiling) * * with `pref.columns` an UPPER BOUND (a wider window never opens more columns * than the user asked for —their decision 2026-09-19: "设置成 2 列,即便对话区 * 域有多余的空间,也只排放 2 列) and `pref.cardSide` the BASE side, i.e. the * minimum track the deck is willing to call a card. Because `room` is read live * from the transcript measure, dragging the conversation width moves `side` on * the very same frame —as a tier hop, which is what the spring on the card * slots animates. * * Only when even a single column cannot hold the base size does the card shrink, * and only below the automatic floor does the rail collapse (prefs untouched, so * widening the window brings it straight back). * * Why the older rule went: "keep the column count, shrink the card to fit" pinned * every card at exactly the base size across wide budget ranges (measured * 2026-09-19 at 1578×1000: `side` stayed 150px while the budget moved * 372−89px), which is the reported "the widgets do not resize while I drag the * conversation width". */ export declare function resolveRailLayout(prefs: Prefs, budget: number, minSide: number, maxSide: number): { side: number; columns: number; railW: number; constrained: boolean; };