From 928b3c15b68e35e3608578b6e851947727175dee Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 25 May 2026 14:23:22 +0200 Subject: [PATCH] fix(eidos/command): route shortcut keyboard listener via ActiveDom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cierra audit Codex P1 #2: «command-shortcut.svelte.ts registra window.addEventListener('keydown', ...) directamente». Eidos no debe gestionar DOM activity por su cuenta — todo binding pasa por ActiveDom (regla: «toda DOM activity via ActiveDom»). Cambios: - `command-shortcut.svelte.ts`: signature ahora es `bindCommandShortcut(dom: ActiveDom, shortcut, onTrigger)`. Usa `dom.getWindow()` + `dom.listen(win, 'keydown', handler)` en vez de `window.addEventListener('keydown', handler)`. Retorna la cleanup function de dom.listen (sigue siendo el contrato esperado por el $effect del consumer). - `command.svelte`: pasa `eidos.dom` como primer arg (ya tenia `const eidos = ActiveEidos.require()`). - `command-dialog.svelte`: anadido `import { ActiveEidos }` + `const eidos = ActiveEidos.require()` (no lo tenia). Mismo patron. Beneficios: - ActiveDom owns el lifecycle (cleanup tracked, pause-on-dispose, iframe/popup-aware via targetWindow). - Eidos visual layer queda sin DOM API directo (regla cumplida). - Tests pueden mockear `dom.listen` sin parchear window globalmente. Verificacion: - `grep window.addEventListener src/uix/eidos/components/`: cero matches (solo la mencion en JSDoc del cambio explicativo). - `npm run check`: 0 errors mantenido. - HTTP 200 en `/uix/components/command`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/command/command-dialog.svelte | 8 ++++++-- .../command/command-shortcut.svelte.ts | 19 ++++++++++++++++--- .../eidos/components/command/command.svelte | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/uix/eidos/components/command/command-dialog.svelte b/src/uix/eidos/components/command/command-dialog.svelte index fbde21f86..d4ba969ec 100644 --- a/src/uix/eidos/components/command/command-dialog.svelte +++ b/src/uix/eidos/components/command/command-dialog.svelte @@ -13,6 +13,7 @@ * Default `shortcut='mod+k'` opens the dialog on the canonical * keyboard binding. Set `shortcut={null}` to disable. */ + import { ActiveEidos } from '$uix/eidos'; import { Dialog } from '$uix/eidos/components/dialog'; import CommandRoot from './command.svelte'; import type { CommandDialogProps } from './types'; @@ -42,11 +43,14 @@ children: bodyContent }: CommandDialogProps = $props(); + const eidos = ActiveEidos.require(); + // Uses `$effect` (see command.svelte — visual layer can't import - // Svelte 4 lifecycle hooks). + // Svelte 4 lifecycle hooks). DOM binding via `eidos.dom.listen(...)` + // inside `bindCommandShortcut` so the ActiveDom layer owns cleanup. $effect(() => { if (!shortcut) return undefined; - return bindCommandShortcut(shortcut, () => { + return bindCommandShortcut(eidos.dom, shortcut, () => { const next = !open; open = next; onOpenChange?.(next); diff --git a/src/uix/eidos/components/command/command-shortcut.svelte.ts b/src/uix/eidos/components/command/command-shortcut.svelte.ts index acfc957d6..d3821128f 100644 --- a/src/uix/eidos/components/command/command-shortcut.svelte.ts +++ b/src/uix/eidos/components/command/command-shortcut.svelte.ts @@ -4,7 +4,13 @@ * Cmd on macOS / Ctrl elsewhere. * * Returns a cleanup function. Caller owns the open-state binding. + * + * DOM binding goes through `ActiveDom` (per the canonical rule «toda + * actividad DOM por ActiveDom»). Eidos visual layer never calls + * `window.addEventListener` directly — the runtime owns lifecycle so + * cleanup is tracked and listeners can be paused / inspected. */ +import type { ActiveDom } from '$adom'; import type { CommandShortcut } from './types'; function isMac(): boolean { @@ -52,12 +58,20 @@ function matches(e: KeyboardEvent, parsed: ParsedKey): boolean { /** * Bind a window-level keydown listener that calls `onTrigger` when any of * the configured shortcuts fires. Pass `null`/`undefined` shortcut to no-op. + * + * The window listener is registered via `dom.listen(...)` so the + * ActiveDom layer owns the cleanup contract (paused on dispose, tracked + * for tests). The caller — an Eidos `$effect` — wires the return into + * its effect cleanup as usual. */ export function bindCommandShortcut( + dom: ActiveDom, shortcut: CommandShortcut | null | undefined, onTrigger: (e: KeyboardEvent) => void ): () => void { - if (!shortcut || typeof window === 'undefined') return () => {}; + if (!shortcut) return () => {}; + const win = dom.getWindow(); + if (!win) return () => {}; const specs = Array.isArray(shortcut) ? shortcut : [shortcut]; const parsed = specs.map(parseShortcut); const handler = (e: KeyboardEvent) => { @@ -69,6 +83,5 @@ export function bindCommandShortcut( } } }; - window.addEventListener('keydown', handler); - return () => window.removeEventListener('keydown', handler); + return dom.listen(win, 'keydown', handler); } diff --git a/src/uix/eidos/components/command/command.svelte b/src/uix/eidos/components/command/command.svelte index dce16c2d9..554a43446 100644 --- a/src/uix/eidos/components/command/command.svelte +++ b/src/uix/eidos/components/command/command.svelte @@ -59,7 +59,7 @@ // registration. $effect(() => { if (!shortcut) return undefined; - return bindCommandShortcut(shortcut, () => { + return bindCommandShortcut(eidos.dom, shortcut, () => { const next = !open; open = next; onOpenChange?.(next);