/** * `scrollElementIntoView` — canonical "scroll an element to the top of the * viewport, account for sticky chrome, survive layout shifts" helper. * * One shared implementation so every caller (the ticket drawer expand, the * hub's `useUnifiedNav` / `use-nav-link` hash scroll, doc-tree, delivery * `?focus=`, sticky-section-nav, …) inherits the SAME cancellation-proof * motion. * * WHY A SELF-DRIVEN rAF TWEEN INSTEAD OF `window.scrollTo({behavior:'smooth'})`: * the native smooth scroll is CANCELLABLE, and in real pages it gets cancelled * constantly: * * - Browser SCROLL ANCHORING: when content is inserted/removed above or * around the target (a collapsible drawer expanding, an async image * loading, a list re-rendering) the browser issues a synchronous scrollTop * correction to keep the anchored element stable. Per CSSOM-View "perform a * scroll" step 1 ("abort any ongoing smooth scroll"), that correction * ABORTS an in-flight native smooth scroll — so it lands as an instant jump. * Anchoring is suppressed when the scroll offset is 0, which is exactly why * a native smooth scroll appears to work the FIRST time (page at top) and * jumps on every repeat (page already scrolled). This was a multi-day * "smooth only works once" bug on the /tickets drawer. * - A second programmatic scroll on the same frame, or a `focus()` without * `{preventScroll:true}`, cancels it the same way. * * A tween that re-asserts the position with INSTANT writes every frame is * immune: there is no "ongoing native smooth scroll" for anchoring/focus to * abort, and any correction that lands between our frames is overwritten on the * next frame. We also RECOMPUTE the target each frame, so an element whose * final position is still settling (drawer still expanding, images loading) * is tracked to its resting place instead of animating to a stale pixel. * * Honors `prefers-reduced-motion` (jumps instantly) and cancels on genuine user * scroll intent (wheel / touch) so we never fight the user. * * WINDOW *OR* A SCROLLABLE ANCESTOR: the helper is not hard-wired to the window * scroller. It walks up from the target to the nearest ancestor that is an * actual scroll container (`overflow-y: auto | scroll | overlay` AND * `scrollHeight > clientHeight`) and drives THAT element; only when none exists * does it fall back to `window`. This is what makes it work inside app shells * that put page content in a fixed-height `
` * (e.g. OpenFrame's `AppLayout`) where the document/window never scrolls — the * old window-only version was a silent no-op there. Note `overflow: clip` / * `hidden` are deliberately NOT treated as scroll containers, so a list wrapper * that uses `overflow-clip` only to round its corners still bubbles the scroll * up to the real container (matches the `` list intent). */ export interface ScrollElementIntoViewOptions { /** Pixels to subtract from the target element's `top` so it lands BELOW * sticky chrome. Defaults to 0. Pass `96` for the standard hub header. */ headerOffset?: number; /** `'smooth'` (default) runs the self-driven tween; `'instant'` / `'auto'` * jump in one synchronous write (deep-link land, programmatic focus moves). */ behavior?: ScrollBehavior; /** Optional adjustment applied to the computed pixel target each frame. The * callback receives the "raw" Y (`element.top + scrollY - headerOffset`) and * returns the FINAL target. Use when the caller knows about a layout shift * (e.g. a sibling drawer collapsing) the geometry can't yet reflect. */ adjustTargetY?: (rawTargetY: number) => number; /** Tween duration in ms (smooth only). Default 320. */ durationMs?: number; } /** * Scroll the page so `target` lands at the top of the viewport (below sticky * chrome via `headerOffset`). SSR-safe; `null`/`undefined` target is a no-op so * callers can pass refs without defensive branching. */ export declare function scrollElementIntoView(target: HTMLElement | null | undefined, options?: ScrollElementIntoViewOptions): void; //# sourceMappingURL=scroll-into-view.d.ts.map