/** * dsh-widgets — the rail deck's per-card open/close cascade ('stagger' shape). * * WHAT THIS OWNS: every card of the resting deck (and the 设置 tile that shares * its grid) travelling along THE VERY SAME PATH the 'zoom' shape moves the whole * group along — each card simply gets its own timing. The rail's own box does not * move in this shape (see the drawer in rail-view.tsx); only the cards do. * * ── THE PATH IS THE ZOOM SHAPE'S, PER CARD ── * * 'zoom' puts two transforms on two nested `inset: 0` boxes, both anchored at the * WRAPPER's top-right corner `O` — the viewport's own top-right, measured live as * `transform-origin: 1578px 0px` at a 1578px window: * * outer wrapper translateX(travel) on the position curve * inner layer scale(s0) on the zoom curve * * so a point `p` of a card maps to `O + s·(p − O) + (travel, 0)`. `stagger` * reproduces exactly that affine map PER CARD, with `s` and `travel` driven by the * same two curves — the only difference is that each card's progress is its own, * offset by its rank in the fold. The anchor therefore stays the viewport's * top-right corner, which is OFF THE SCREEN: the cards fly in from beyond the * rail's right edge (the rail is pinned to that edge), not out of a corner of the * grid. * * The identity is exact, and worth stating because the implementation does not * literally translate a wrapper: with the card's own top-right corner `Oc` as the * CSS `transform-origin` (which is what `.dsx-stats-card-slot` already carries), * * translate((1−s)·(O − Oc) + (travel, 0)) scale(s) * * denotes the map * * Oc + s·(p − Oc) + (1−s)(O − Oc) + (travel, 0) = O + s·(p − O) + (travel, 0) * * i.e. the group's own. No translation is smuggled in: at any (s, travel) the * card's four edges land exactly where the zoom shape would have put them, which * is what the live probe checks pixel by pixel. * * THE RAIL'S CLIP IS FINE, and is the effect: the rail clips its own subtree, so * a card pushed past its right edge is simply not painted. The cards enter the * rail's strip from off-screen, one after another — hidden until their turn brings * them across the edge, which is exactly what "each component arrives on its own * beat from outside the screen" looks like. * * ── WHY A JS CLOCK, NOT A CSS TRANSITION ── * * The cascade must survive a rapid reversal (a close interrupting an open, and * back) by CONTINUING FROM THE CURRENT PROGRESS. A CSS transition cannot do it: * the per-card `transition-delay` is re-applied the moment the target flips, so * the cards that are already part-way in — the ones with the SMALLEST forward * delay, i.e. exactly the ones a mirror order schedules LAST — freeze in place for * their new delay (measured shape of the bug: the bottom-left card holds a fully * open position for ~570ms before it starts back). One shared clock, advanced * forwards or backwards, makes both directions ONE timeline: the reverse cascade * is the same timeline read backwards, so the mirror order falls out of the rank * table for free, and an interruption is only a change of the clock's sign — no * jump, no freeze, no restart. `prefers-reduced-motion` never reaches this module: * rail-view jumps the drawer instead of playing it. A plan is never swapped under * a running clock, either (see `sync`). * * Per-frame writes are inline `transform` only (no layout property, no React * render): the card list is read once per command and the clock touches nothing * else. */ /** * The timeline the fold is allowed — the GROUP's own duration (fallback). * * Both 'zoom' transforms are declared as `transform var(--ds-transition-duration-slow) * …`, so a fold that took longer than that variable would visibly outlast the shape * it is a version of (measured: 220ms per card + 13 × 30ms of cascade = 610ms * against the shell's 300ms, i.e. TWICE the gesture it is supposed to be a version * of; the timeline is now derived from that variable — see {@link cascadeTiming}). * This constant is only what {@link groupDurationMs} falls back to when the shell * publishes nothing readable. */ export declare const CASCADE_FALLBACK_TOTAL_MS = 300; /** * How much of that timeline ONE card's own travel takes; the rest is spread over * the ranks as the cascade. * * The two are a trade: a longer travel reads slower and smoother but leaves less * room to stagger, and vice versa. 60/40 keeps the visible part of a card's * entrance (it is off-screen for the first ~70% of the group's own path — the rail * clips it) in the same order as the group's, while 14 cells still get 9ms of * separation each, which is a clear beat at 60Hz. */ export declare const CASCADE_TRAVEL_SHARE = 0.6; /** * Re-read the theme token and replace the memo. * * Exported so a caller that knows the document just went quiet can force the refresh * without waiting for an attribute flip. */ export declare function refreshGroupDurationMs(fallback?: number): number; /** The shell's own slow transition, in ms. */ export declare function groupDurationMs(fallback?: number): number; /** * Split the group's timeline into one card's travel and the per-rank step. * * The whole point is that `(participants − 1) · step + duration` IS the group's * duration: the fold is the same gesture, not a longer one, whatever the deck * holds. The step is therefore FRACTIONAL (the stamp rounds it to 0.01ms — see * {@link cascadeDelay}), because rounding it to whole milliseconds would either * overshoot the group or leave the fold short of it by up to one rank's worth. * It is never below 1ms, so every rank keeps a distinct beat even on a 20-cell * deck. */ export declare function cascadeTiming(totalMs: number, participants: number): { duration: number; step: number; }; /** One participant's box inside the deck, in the deck's own coordinates. */ export interface CascadeBox { /** Distance from the deck's TOP edge (`placeCards`'s `top`). */ top: number; /** Distance from the deck's RIGHT edge (`placeCards`'s `right`). */ right: number; } /** * The corner BOTH shapes start from, plus the 'zoom' shape's two start values — * read from the live drawer (its computed `transform-origin`) and from the same * numbers rail-view hands the group transform, so the two shapes cannot drift. */ export interface CascadeAnchor { /** `translateX` the whole-group zoom starts from: `railW + 24`. */ travel: number; /** Scale the whole-group zoom starts from: `prefs.animScale`. */ scale: number; /** The drawer wrapper's `transform-origin` x — the viewport's right edge. */ originX: number; /** …and its y, which is the viewport's top edge (0). */ originY: number; } /** Timing the deck is animated with — the same two curves the group rides. */ export interface CascadeTiming { /** * One card's own travel (ms). Derived so that * `(participants − 1) · step + duration` IS the group's duration — see * {@link cascadeTiming}. */ duration: number; /** The POSITION curve (`prefs.animShiftCurve`) evaluated on [0,1]. */ easeShift: (x: number) => number; /** The ZOOM curve (`prefs.animCurve`) evaluated on [0,1]. */ easeZoom: (x: number) => number; } /** One element's part in the fold. */ export interface CascadeCard { el: HTMLElement; /** `O − Oc` (px): the card's top-right corner to the shared anchor. */ vx: number; vy: number; /** Cascade delay (ms): the card's rank in the fold × the step in `cascadeTiming`. */ delay: number; /** * The 设置 tile carries its own centred `transform-origin`, a hover `transform` * and a `transform` transition: while the fold drives it all three are pinned * and then restored, or the tile would scale about the wrong point and trail the * fold through its own hover transition. */ ownStyle: boolean; } /** The cards of one fold, ready to play. */ export interface CascadeSpec extends CascadeTiming { cards: readonly CascadeCard[]; anchor: CascadeAnchor; } /** Which end of the timeline a command is aiming at. */ export type CascadeEnd = 'open' | 'closed'; /** Where the clock actually is. `to-*` means a fold is in flight. */ export type CascadeState = 'open' | 'closed' | 'to-open' | 'to-closed'; export interface DeckCascade { /** One timeline length (ms): the last rank's delay plus one card's travel. */ readonly total: number; state: () => CascadeState; /** Replace the card list, the anchor and the timing — safe to call every render. */ sync: (spec: CascadeSpec) => void; /** Animate toward `end` from wherever the clock is now. */ play: (end: CascadeEnd) => void; /** Put the clock at `end` WITHOUT animating. */ jump: (end: CascadeEnd) => void; /** Stop and hand the cards back at their final layout position. */ release: () => void; } /** * The fold's order: the deck's BOTTOM-LEFT cell first, its TOP-RIGHT cell last. * * The key is `top · 100000 + right`: a bigger `top` is lower on screen and a bigger * `right` is further LEFT (the deck is right-anchored), so ranking the key * descending walks the deck from its bottom-left corner to its top-right one. * `reverse` is the same order mirrored — the collapse reads it back out, and it is * also what an interruption falls into, because the collapse is the same timeline * played backwards. * * `boxes` carries one entry per participant in DOM order (the cards, then the * settings tile), which is the order of the returned ranks. */ export declare function cascadeRanks(boxes: ReadonlyArray<{ top: number; right: number; }>): { order: number[]; reverse: number[]; }; /** * The cascade delay a rank runs at, as the CSS time the deck stamps per slot. * * Rounded to 0.01ms so the stamp stays readable while the ladder stays exact: the * last rank's stamp plus one travel is the group's duration to within a hundredth * of a millisecond. */ export declare function cascadeDelay(rank: number, step: number): string; /** * Turn the deck's LAYOUT into the fold's start state: one entry per participant, * each carrying the vector from its own top-right corner to the shared anchor. * * The deck's live box is read here (never the cards'): the deck is drawn where the * layout puts it, whereas a card in the middle of a fold is exactly the thing whose * box is not its layout box. A plan is only ever adopted with the deck at rest (see * `sync`), so this read is a rest-state read by construction. * * `boxes` carries one entry per participant in DOM order (the cards, then the * settings tile) — the same order `querySelectorAll` walks, so an element and its * box line up by index. */ export declare function planCascade(deck: HTMLElement, boxes: ReadonlyArray, anchor: CascadeAnchor, timing: CascadeTiming): CascadeSpec; /** * Build the deck's cascade driver. * * `onSettle` fires exactly once per fold that reaches an end — including the no-op * case where the clock is already there — which is what lets the drawer unmount on * the real end of the leave instead of on a timer that the next phase change can * cancel. */ export declare function createDeckCascade(onSettle: (end: CascadeEnd) => void): DeckCascade;