The overlay history stack let a navigation land ABOVE open overlays and "buried" their entries with a counter. Measured in Chrome: Shift+W over About navigated under the still-open dialog, the next Back both undid the navigation and closed About, and the Back after that did nothing; Shift+W on Home with the player open did nothing at all. The counter could also be spent by a different overlay closing first, and callers that could not report whether they pushed were assumed to have. setPage and openChannel now close every open overlay and push only once the history is back in sync (one entry per live overlay, no programmatic back() in flight, no reconcile queued), so nothing is ever buried and the whole mechanism goes: navigateOverOverlays, closeThenNavigate, buryUnder, setPage's boolean, and the three test files that pinned them. The palette closes only itself before running a row, so a theme picked over the player keeps the player. Shift+W on Home with the player open now closes it (owner decision 2026-09-09). The wait is on state, not on "a popstate": the old 250 ms fallback could run with back()s still in flight. An overlay that refuses to close within 1.5 s drops the command instead of being navigated out from under.
36 lines
1.6 KiB
TypeScript
36 lines
1.6 KiB
TypeScript
import type { Page } from "./urlState";
|
|
|
|
// Tiny decoupled navigator so deep components (a feed card's download toast, the Download
|
|
// Center) can switch pages without threading setPage through the whole tree. NavigationProvider
|
|
// registers its setPage on mount; navigateTo is a no-op until then.
|
|
let _navigate: ((p: Page) => void) | null = null;
|
|
|
|
export function setNavigator(fn: (p: Page) => void): void {
|
|
_navigate = fn;
|
|
}
|
|
|
|
/** The "View" on a "Download added" toast is clicked INSIDE the player (its toast host). That is
|
|
* safe because `setPage` closes every open overlay before it pushes (see
|
|
* `lib/history.closeOverlaysThenNavigate`) — without it the player's Back-to-close cleanup popped the
|
|
* entry the navigation had just pushed (deep-review round 4). */
|
|
export function navigateTo(p: Page): void {
|
|
_navigate?.(p);
|
|
}
|
|
|
|
/** Should this click be handled IN-APP, or left to the browser?
|
|
*
|
|
* An anchor that ALSO navigates in-app has to keep the browser's own gestures working — Ctrl/Cmd-click
|
|
* and Shift-click open a tab/window, and middle-click arrives as `auxclick` and never reaches React's
|
|
* `onClick` at all (the `button !== 0` arm is belt-and-braces for synthetic events). Otherwise the
|
|
* thing looks like a link and behaves like a button, which is what the channel name in the player did.
|
|
* One predicate, so a second such link cannot forget an arm. */
|
|
export function isPlainLeftClick(e: {
|
|
metaKey: boolean;
|
|
ctrlKey: boolean;
|
|
shiftKey: boolean;
|
|
altKey: boolean;
|
|
button: number;
|
|
}): boolean {
|
|
return !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey && e.button === 0;
|
|
}
|