From 7acb197daf1e822ff305e07da9c5c662f5435cec Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 12 Aug 2026 01:18:34 -0400 Subject: [PATCH] feat(frontend): give a cached view a lifecycle and a keyboard owner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `index.ts` caches primary views and hides them with a class so scrollTop survives navigation. Nothing else was told: `disconnectedCallback` never fires for one, so everything written to clean up there never cleans up. The worst case was not a leak — pressing `s` on Settings skipped two albums out of the Autotag queue, and `a` on that same live handler rewrites tags on disk. - `utils/view-lifecycle.ts` is the missing half: `viewActivated` / `viewDeactivated`, with `listenWhileActive`, `intervalWhileActive` and `whileActive` torn down on the way out, and an off-screen view that does not render. `registerViewAware` gives a shared reactive controller the same treatment, because a controller cannot know whether its host is a cached view — `ContextMenuController` bound three document listeners in `hostConnected`, which for a cached host is "forever". - `services/shortcut-scope.ts` publishes the ambient scope. Resolving scope from focus alone was not enough: this app is driven with the mouse, focus sits on ``, and a focus-only rule would have made the panel keys work only after a click landed inside the panel. - Global bindings yield to a focused control that owns the key — button, select, slider, checkbox, menu, grid row, or anything inside an open dialog — so the unmodified single-key bindings stop stealing Space and the arrows. - `utils/roving-grid.ts` gives a card grid one tab stop moved with the arrows, since a card per tab stop makes a library-length tab sequence. --- .../src/components/sidebar/app-sidebar.ts | 67 +++-- .../src/services/keyboard-shortcut-service.ts | 152 ++++++++++- frontend/src/services/shortcut-scope.ts | 45 ++++ frontend/src/store/shortcuts-store.ts | 6 +- frontend/src/utils/context-menu-controller.ts | 32 ++- frontend/src/utils/roving-grid.ts | 133 ++++++++++ frontend/src/utils/view-lifecycle.ts | 251 ++++++++++++++++++ 7 files changed, 659 insertions(+), 27 deletions(-) create mode 100644 frontend/src/services/shortcut-scope.ts create mode 100644 frontend/src/utils/roving-grid.ts create mode 100644 frontend/src/utils/view-lifecycle.ts diff --git a/frontend/src/components/sidebar/app-sidebar.ts b/frontend/src/components/sidebar/app-sidebar.ts index 0940d3c..a051fca 100644 --- a/frontend/src/components/sidebar/app-sidebar.ts +++ b/frontend/src/components/sidebar/app-sidebar.ts @@ -53,39 +53,57 @@ export class AppSidebar extends LitElement { padding: 16px; } + /* The nav item is a real `; })} + `; } diff --git a/frontend/src/services/keyboard-shortcut-service.ts b/frontend/src/services/keyboard-shortcut-service.ts index 6478d5f..e271274 100644 --- a/frontend/src/services/keyboard-shortcut-service.ts +++ b/frontend/src/services/keyboard-shortcut-service.ts @@ -11,6 +11,7 @@ * - Action dispatch to player/queue/nav stores */ import { shortcutsStore } from '@store/shortcuts-store'; +import { ambientShortcutScope } from './shortcut-scope'; import { playerStore } from '@store/player-store'; import { queueStore } from '@store/queue-store'; import * as Player from '@go/player/Player'; @@ -131,6 +132,128 @@ type ShortcutScope = | `panel:${string}` | 'global'; +/** + * Elements that own particular keys themselves. + * + * The global bindings are unmodified single keys (Space, arrows, letters) + * — see Decision 1 in plan 007 — so without this a focused button cannot + * be activated with Space and a `