docs(arts): A2 ES->EN — adom (full translation)

Full Spanish -> English translation of adom/README.md (faithful; code blocks and
the ActiveDom interface kept verbatim, incl. the Backlog chronicle entries).
Carries the A1 additions (prefersReducedMotion/writeProperty/removeProperty,
listen 4 overloads, standalone rune-helpers table, /active/docs/adom).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent dd1abac629
commit 50dce1ea1b

@ -1,66 +1,63 @@
# ActiveDom
`adom` contiene `ActiveDom`: el servicio DOM reactivo de aplicación.
`adom` contains `ActiveDom`: the application's reactive DOM service.
## Qué es hoy
## What it is today
Ahora mismo `ActiveDom` no es un bus de eventos semánticos ni un reflector de
`data-event*`.
Right now `ActiveDom` is not a semantic event bus nor a `data-event*` reflector.
Su responsabilidad actual es más pequeña y más concreta:
Its current responsibility is smaller and more concrete:
- exponer el ancho de viewport de forma reactiva
- resolver el breakpoint actual
- mantener la definición de breakpoints de la app
- resolver valores responsive
- ofrecer helpers de consulta (`isAtLeast`, `matches`)
- aplicar/remover atributos DOM de forma controlada (`apply`, `remove`)
- registrar listeners globales o transversales con cleanup (`listen`)
- resolver `document` / `window` propietario para iframes, popups y tests
- ejecutar acciones imperativas (`focus`, `scrollTo`, `scrollWindowTo`,
- expose the viewport width reactively
- resolve the current breakpoint
- keep the app's breakpoint definition
- resolve responsive values
- offer query helpers (`isAtLeast`, `matches`)
- apply/remove DOM attributes in a controlled way (`apply`, `remove`)
- register global or cross-cutting listeners with cleanup (`listen`)
- resolve the owning `document` / `window` for iframes, popups and tests
- run imperative actions (`focus`, `scrollTo`, `scrollWindowTo`,
`scrollWindowBy`, `requestFrame`)
- escribir/remover nodos gestionados y texto accesible (`writeNode`,
`writeText`, `removeNode`)
- write/remove managed nodes and accessible text (`writeNode`, `writeText`,
`removeNode`)
En otras palabras:
In other words:
```text
libs/dom -> arts/adom -> App.dom / app.dom
puro reactivo consumo de app
pure reactive app consumption
```
## Composicion via active-app
## Composition via active-app
`createActiveApp(...)` puede construir `App.dom` como servicio cuando la app lo
declara con `defineActiveDom()`. `ActiveUix` lo usa como unico escritor de
attrs cuando sus capas reciben una superficie DOM. Las preferencias
transversales se proyectan mediante `createActivePrefsDomProjection(...)`; las
visuales mediante `ActiveEidos`. Si no existe `dom` porque el integrador pidio
`dom:false`, UIX usa `disabledDom` en modo standalone. Construir
`createActiveDom()` directamente solo es necesario en tests aislados o en
consumidores fuera de la composicion estandar.
`createActiveApp(...)` can build `App.dom` as a service when the app declares it
with `defineActiveDom()`. `ActiveUix` uses it as the sole attribute writer when
its layers receive a DOM surface. Cross-cutting preferences are projected via
`createActivePrefsDomProjection(...)`; visual ones via `ActiveEidos`. If `dom`
does not exist because the integrator asked for `dom:false`, UIX uses
`disabledDom` in standalone mode. Building `createActiveDom()` directly is only
needed in isolated tests or in consumers outside the standard composition.
## Qué pertenece a cada capa
## What belongs to each layer
### `libs/dom`
Primitives DOM puras o casi puras:
Pure or nearly-pure DOM primitives:
- guards y traversal DOM
- DOM guards and traversal
- focus helpers
- tabbable helpers
- responsive helpers puros
- pure responsive helpers
**Capa de implementación.** Componentes y soma NO importan de aquí
directamente — todo se reexporta a través de `$adom` (ver más abajo).
Sólo `arts/adom/*` y los tests de `libs/dom` pueden importar
`$libs/dom` directo.
**Implementation layer.** Components and soma do NOT import from here directly —
everything is re-exported through `$adom` (see below). Only `arts/adom/*` and
the `libs/dom` tests may import `$libs/dom` directly.
No mantiene estado de aplicación.
It keeps no application state.
### `arts/adom`
Runtime reactivo de DOM:
Reactive DOM runtime:
- `viewport`
- `breakpoints`
@ -68,36 +65,38 @@ Runtime reactivo de DOM:
- `resolve(...)`
- `isAtLeast(...)`
- `matches(...)`
- `apply(...)` / `remove(...)` como superficie unica de mutacion de atributos
- `listen(...)` como superficie unica para listeners globales o transversales
- `query(...)`, `elementFromPoint(...)`, `activeElement(...)` para consultas
contra el documento propietario
- `apply(...)` / `remove(...)` as the sole attribute-mutation surface
- `listen(...)` as the sole surface for global or cross-cutting listeners
- `query(...)`, `elementFromPoint(...)`, `activeElement(...)` for queries against
the owning document
- `focus(...)`, `scrollIntoView(...)`, `scrollTo(...)`, `scrollWindowTo(...)`,
`scrollWindowBy(...)`, `requestFrame(...)` / `raf(...)` para acciones
imperativas que no deben depender del `window/document` global (`raf`
devuelve un disposer — ver Backlog)
- `BodyScrollLock` como helper global de body scroll lock, sin bloquear eventos de puntero
- `DOMContext` como helper scoped para `Document` / `ShadowRoot`
- `RovingFocusGroup` como helper runtime para navegación compuesta por teclado
`scrollWindowBy(...)`, `requestFrame(...)` / `raf(...)` for imperative actions
that must not depend on the global `window/document` (`raf` returns a disposer
— see Backlog)
- `BodyScrollLock` as a global body-scroll-lock helper, without blocking pointer
events
- `DOMContext` as a scoped helper for `Document` / `ShadowRoot`
- `RovingFocusGroup` as a runtime helper for composite keyboard navigation
`ActiveDom` sí mantiene estado reactivo y por eso vive aquí, no en `$libs/dom`.
`ActiveDom` does keep reactive state, which is why it lives here, not in
`$libs/dom`.
## Posición en App
## Position in App
`ActiveDom` vive a nivel de aplicación:
`ActiveDom` lives at the application level:
```ts
App.dom;
app.dom;
```
La implementación se consume desde la capa de aplicación y desde artefactos que
necesitan escribir atributos finales sobre un target DOM, por ejemplo
`ActivePrefsDomProjection`, `SomaRuntime`, `VisualChannel` o `ActiveEidos`.
The implementation is consumed from the application layer and from artifacts that
need to write final attributes onto a DOM target, for example
`ActivePrefsDomProjection`, `SomaRuntime`, `VisualChannel` or `ActiveEidos`.
## API actual
## Current API
La API pública real de `ActiveDom` hoy es esta:
`ActiveDom`'s real public API today is this:
```ts
export interface ActiveDom {
@ -202,7 +201,7 @@ export interface ActiveDom {
}
```
Creación:
Creation:
```ts
const dom = createActiveDom({
@ -221,7 +220,7 @@ const columns = dom.resolve({ base: 1, sm: 2, lg: 3 });
const tone = dom.resolve({ base: 'compact', md: 'normal', xl: 'wide' });
```
Mutacion DOM:
DOM mutation:
```ts
dom.apply({
@ -236,7 +235,7 @@ dom.apply({
dom.remove(node, ['data-state', 'aria-busy']);
```
Nodos gestionados:
Managed nodes:
```ts
const region = dom.writeNode('app-live-region', {
@ -249,247 +248,246 @@ if (region) dom.writeText(region, 'Saved');
dom.removeNode('app-live-region', { host: dom.getDocument().body });
```
Reglas:
- los breakpoints se definen en la creación del `dom`
- `breakpoints` es opcional; si no se pasa, usa `BREAKPOINTS_DEFAULT`
- no hay herencia de `dom` padre
- `ActiveDom` es servicio de app, no scope anidado
- `ActiveDom` registra el listener de `resize` al construirse (por instancia).
El singleton compartido (`shareViewport: true`) pospone la asignación del
listener a la primera lectura del viewport — importar `$adom` no aloca
estado reactivo si nadie consume el viewport.
- `apply` solo escribe atributos; no interpreta semantica ni eventos
- listeners de `document` / `window`, observers (`ResizeObserver`,
`MutationObserver`, `IntersectionObserver`) y acciones imperativas
transversales van por `ActiveDom`
- lecturas locales de un elemento propio (`getBoundingClientRect`, `contains`,
`closest`, `clientWidth`, `scrollTop`) siguen siendo responsabilidad del
componente; envolverlas en `ActiveDom` seria ruido
- `writeNode`/`writeText` son para nodos owned por servicios UIX
(live regions, descripciones ocultas, style hosts auxiliares), no para
saltarse el render de Svelte en componentes normales
- `false | null | undefined` remueven atributos
- `viewport` es **per-instancia por defecto**: cada `ActiveDom` posee su
propio listener de `resize`, scoped al `targetWindow` (default `window`).
Esto evita filtraciones entre tests, iframes, popups y entornos happy-dom.
Llamar `dispose()` desadjunta ese listener.
### Compartir el viewport entre instancias
Para apps "single window" donde toda la composición vive en el mismo
documento, opt-in al singleton evita N listeners para el mismo evento:
Rules:
- breakpoints are defined at `dom` creation
- `breakpoints` is optional; if not passed, it uses `BREAKPOINTS_DEFAULT`
- there is no parent-`dom` inheritance
- `ActiveDom` is an app service, not a nested scope
- `ActiveDom` registers the `resize` listener on construction (per instance).
The shared singleton (`shareViewport: true`) defers assigning the listener
until the first viewport read — importing `$adom` allocates no reactive state
if nobody consumes the viewport.
- `apply` only writes attributes; it does not interpret semantics or events
- `document` / `window` listeners, observers (`ResizeObserver`,
`MutationObserver`, `IntersectionObserver`) and cross-cutting imperative
actions go through `ActiveDom`
- local reads of an element's own node (`getBoundingClientRect`, `contains`,
`closest`, `clientWidth`, `scrollTop`) remain the component's responsibility;
wrapping them in `ActiveDom` would be noise
- `writeNode`/`writeText` are for nodes owned by UIX services (live regions,
hidden descriptions, auxiliary style hosts), not for bypassing Svelte's render
in normal components
- `false | null | undefined` remove attributes
- `viewport` is **per-instance by default**: each `ActiveDom` owns its own
`resize` listener, scoped to the `targetWindow` (default `window`). This
prevents leaks across tests, iframes, popups and happy-dom environments.
Calling `dispose()` detaches that listener.
### Sharing the viewport across instances
For "single window" apps where the whole composition lives in the same document,
opting into the singleton avoids N listeners for the same event:
```ts
const dom = createActiveDom({ shareViewport: true });
```
Bajo esta opción, `dispose()` es no-op para el viewport (el singleton vive
toda la vida del proceso). Lo que sí se libera es `breakpoints`,
`currentBreakpoint` y los demás reactivos por-instancia.
Under this option, `dispose()` is a no-op for the viewport (the singleton lives
for the whole process lifetime). What is released is `breakpoints`,
`currentBreakpoint` and the other per-instance reactives.
### Tracking de un window distinto
### Tracking a different window
Para iframes, popups o entornos de test:
For iframes, popups or test environments:
```ts
const iframeDom = createActiveDom({ targetWindow: iframe.contentWindow! });
const popupDom = createActiveDom({ targetWindow: popup });
```
Ignorado cuando `shareViewport: true` (el singleton siempre rastrea el
top-level `window`).
Ignored when `shareViewport: true` (the singleton always tracks the top-level
`window`).
## Helpers rune standalone
Además del runtime `createActiveDom`, `$adom` exporta primitivas reactivas
autónomas (ports 0-dep de `runed`, corregidas para respetar el `targetWindow` vía
`getWindow(node)`). Se importan directamente del barrel `$adom`:
| Helper | Qué expone |
| --------------------------------- | ----------------------------------------------------------------- |
| `ActiveElement` / `activeElement` | valor reactivo = `document.activeElement` |
| `IsDocumentVisible` | ¿la pestaña está visible? (`visibilitychange`) |
| `IsFocusWithin` | ¿el foco está dentro de un elemento? |
| `IsInViewport` | ¿un nodo intersecta el viewport? (`IntersectionObserver`) |
| `IsIdle` | inactividad del usuario tras N ms sin interacción |
| `PressedKeys` | conjunto reactivo de teclas pulsadas ahora mismo |
| `ElementRect` | `DOMRect` reactivo de un elemento |
| `ElementSize` | tamaño reactivo de un elemento (`ResizeObserver`) |
| `ScrollState` | posición, dirección y bordes de scroll |
| `TextareaAutosize` | crece un `<textarea>` para ajustarse a su contenido |
| `AnimationFrames` | bucle `requestAnimationFrame` gestionado (fps / delta) |
| `onClickOutside` | callback al hacer click / foco fuera de un elemento |
| `BodyScrollLock` | bloquea el scroll del `body` |
| `DOMContext` | resuelve `window` / `document` del contexto (iframe / popup safe) |
| `RovingFocusGroup` | grupo de foco con roving tabindex |
## Qué no es
`ActiveDom` no es:
Besides the `createActiveDom` runtime, `$adom` exports standalone reactive
primitives (0-dep ports of `runed`, fixed to respect the `targetWindow` via
`getWindow(node)`). They are imported directly from the `$adom` barrel:
| Helper | What it exposes |
| --------------------------------- | ------------------------------------------------------------------ |
| `ActiveElement` / `activeElement` | reactive value = `document.activeElement` |
| `IsDocumentVisible` | is the tab visible? (`visibilitychange`) |
| `IsFocusWithin` | is focus inside an element? |
| `IsInViewport` | does a node intersect the viewport? (`IntersectionObserver`) |
| `IsIdle` | user inactivity after N ms without interaction |
| `PressedKeys` | reactive set of currently-pressed keys |
| `ElementRect` | reactive `DOMRect` of an element |
| `ElementSize` | reactive size of an element (`ResizeObserver`) |
| `ScrollState` | scroll position, direction and edges |
| `TextareaAutosize` | grows a `<textarea>` to fit its content |
| `AnimationFrames` | managed `requestAnimationFrame` loop (fps / delta) |
| `onClickOutside` | callback on click / focus outside an element |
| `BodyScrollLock` | locks `body` scroll |
| `DOMContext` | resolves the context's `window` / `document` (iframe / popup safe) |
| `RovingFocusGroup` | roving-tabindex focus group |
## What it is not
`ActiveDom` is not:
- `EngineSemantic`
- broker de eventos
- reflector de `data-event*`
- scheduler de observers compartidos con cache global; solo expone factories
scoped a la ventana propietaria del target
- sistema de theme
- sistema de modal, backdrop o inert
- reemplazo de `$libs/dom`
- an event broker
- a `data-event*` reflector
- a scheduler of observers shared with a global cache; it only exposes factories
scoped to the target's owning window
- a theme system
- a modal, backdrop or inert system
- a replacement for `$libs/dom`
Además, `uix/adom` puede alojar helpers DOM con estado global real, como
`BodyScrollLock`, o helpers scoped de runtime como `DOMContext`, cuando ya no
son primitives puras de `$libs/dom` pero tampoco pertenecen a un componente UI concreto.
In addition, `uix/adom` can host DOM helpers with real global state, like
`BodyScrollLock`, or scoped runtime helpers like `DOMContext`, once they are no
longer pure `$libs/dom` primitives but do not belong to a concrete UI component
either.
También caben aquí helpers runtime de foco con estado propio, como
`RovingFocusGroup`, que reutilizan `$libs/dom` por debajo pero ya no son
solo utilidades puras.
Focus runtime helpers with their own state, like `RovingFocusGroup`, also fit
here: they reuse `$libs/dom` underneath but are no longer just pure utilities.
## Pagina De Prueba
## Test Page
La pagina manual esta en `/active/docs/adom` y cubre:
The manual page is at `/active/docs/adom` and covers:
- viewport y breakpoint actual
- `resolve()` responsive
- viewport and current breakpoint
- responsive `resolve()`
- `apply()` / `remove()`
- `BodyScrollLock`
- `DOMContext`
- `RovingFocusGroup`
## Relación con otras piezas
## Relationship with other pieces
### Semántica
### Semantics
La semántica pertenece a `Sema` y a `EngineSemantic`, no a `ActiveDom`.
Semantics belong to `Sema` and `EngineSemantic`, not to `ActiveDom`.
Si mañana `ActiveDom` refleja eventos al DOM, será como consumidor de
`EngineSemantic`, no como autoridad semántica.
If `ActiveDom` ever reflects events to the DOM, it will do so as a consumer of
`EngineSemantic`, not as a semantic authority.
### Theme
El theme no pertenece a `dom`. En UIX, `ActiveEidos` posee `data-theme`,
`data-mode` y `data-density`; `ActiveDom` solo recibe la mutación ya resuelta.
Theme does not belong to `dom`. In UIX, `ActiveEidos` owns `data-theme`,
`data-mode` and `data-density`; `ActiveDom` only receives the already-resolved
mutation.
### Air y Terra
### Air and Terra
`air` y `terra` no consumen esta capa nueva.
`air` and `terra` do not consume this new layer.
Su código actual sirve como referencia histórica para extraer utilidades hacia
`$libs/dom`, pero no forman parte del runtime nuevo.
Their current code serves as historical reference for extracting utilities into
`$libs/dom`, but they are not part of the new runtime.
## Estado del diseño
## Design status
`ActiveDom` está en fase fundacional.
`ActiveDom` is in a foundational phase.
Lo que ya está cerrado:
What is already closed:
- `app.dom`
- `App.dom`
- `viewport`
- `breakpoints`
- `currentBreakpoint`
- resolución responsive
- listeners, observers y acciones imperativas scoped a la ventana propietaria
del target
- responsive resolution
- listeners, observers and imperative actions scoped to the target's owning
window
Lo que queda para fases posteriores, si de verdad hace falta:
What is left for later phases, if it is really needed:
- reflexión de eventos semánticos al DOM
- APIs por `Document` o `ShadowRoot`
- introspección/diagnóstico de runtime más rica
- reflecting semantic events to the DOM
- per-`Document` or `ShadowRoot` APIs
- richer runtime introspection/diagnostics
La regla importante por ahora es simple:
The important rule for now is simple:
> `ActiveDom` es el servicio reactivo de DOM de la app; `$libs/dom` es su base pura.
> `ActiveDom` is the app's reactive DOM service; `$libs/dom` is its pure base.
---
## Backlog / Decisiones de evolución
## Backlog / Evolution decisions
Registro de cambios de superficie posteriores a la fase fundacional. Cada
entrada documenta **qué** se añadió y, sobre todo, **por qué** — para que la
decisión no se pierda y futuros consumidores entiendan el patrón canónico.
Record of surface changes after the foundational phase. Each entry documents
**what** was added and, above all, **why** — so the decision is not lost and
future consumers understand the canonical pattern.
### 2026-06-29 — `dom.measure(read, node?)`: lectura de layout coalescida post-layout
### 2026-06-29 — `dom.measure(read, node?)`: coalesced post-layout read
**Qué.** Nuevo método en `ActiveDom`:
**What.** New method on `ActiveDom`:
```ts
measure(read: () => void, node?: …): ActiveDomFrameCleanup
```
Agenda una lectura que fuerza layout (`getBoundingClientRect`, `getComputedStyle`,
`offset*`, `scroll*`) en un **rAF coalescido por ventana** en vez de síncronamente.
Todas las lecturas encoladas en un mismo turno corren juntas en un único frame, así
una lectura nunca fuerza un reflow EN MEDIO de un turno de escritura — el origen de
`[Violation] Forced reflow while executing JavaScript`. Devuelve el mismo disposer
idempotente que `raf`; `dispose()` cancela los frames pendientes.
Schedules a layout-forcing read (`getBoundingClientRect`, `getComputedStyle`,
`offset*`, `scroll*`) in a **per-window coalesced rAF** instead of synchronously.
Every read queued in one turn runs together in a single frame, so a read never
forces a reflow IN THE MIDDLE of a write turn — the source of
`[Violation] Forced reflow while executing JavaScript`. It returns the same
idempotent disposer as `raf`; `dispose()` cancels pending frames.
**Por qué.** El framework gobierna las ESCRITURAS (`apply`) y el TIEMPO
(`uix.timers`) pero no tenía superficie para el _timing_ de las LECTURAS de layout,
que solo son seguras post-layout. `measure` es el hogar sancionado: posee _cuándo_
corre la lectura (post-turno, coalescida), no _qué_ elemento. **Solo lecturas** — las
escrituras ya secuencian por `apply` + el runtime; un segundo eje `mutate`/dos-fases
duplicaría lo que `apply` + Svelte ya hacen (sobre-ingeniería descartada).
**Why.** The framework governs the WRITES (`apply`) and the TIME (`uix.timers`)
but had no surface for the _timing_ of layout READS, which are only safe
post-layout. `measure` is the sanctioned home: it owns _when_ the read runs
(post-turn, coalesced), not _which_ element. **Reads only** — writes already
sequence through `apply` + the runtime; a second `mutate`/two-phase axis would
duplicate what `apply` + Svelte already do (over-engineering rejected).
**Disciplina.** Está prohibido leer layout síncronamente justo tras una escritura de
DOM/estilo. Un `raf` que haga la lectura cumple igual; `measure` añade el coalescing
y la intención semántica. Patrón canónico ya seguido por `Tabs.Indicator` (mide vía
rAF diferido + guard de coalescing).
**Discipline.** Reading layout synchronously right after a DOM/style write is
forbidden. A `raf` that performs the read complies just as well; `measure` adds
the coalescing and the semantic intent. Canonical pattern already followed by
`Tabs.Indicator` (measures via a deferred rAF + coalescing guard).
### 2026-06-02 — `dom.raf(callback, node?)`: frame de animación con disposer
### 2026-06-02 — `dom.raf(callback, node?)`: animation frame with a disposer
**Qué.** Nuevo método en la superficie `ActiveDom`:
**What.** New method on the `ActiveDom` surface:
```ts
raf(callback: FrameRequestCallback, node?: …): ActiveDomFrameCleanup
```
Agenda un `requestAnimationFrame` de una sola pasada contra la ventana que
posee `node` (o el `targetWindow` de la instancia) y devuelve un **disposer**
`() => void` idempotente que cancela el frame pendiente. Tipo
`ActiveDomFrameCleanup` exportado junto a `ActiveDomListenerCleanup` /
Schedules a single-pass `requestAnimationFrame` against the window that owns
`node` (or the instance's `targetWindow`) and returns an idempotent
`() => void` **disposer** that cancels the pending frame. The
`ActiveDomFrameCleanup` type is exported alongside `ActiveDomListenerCleanup` /
`ActiveDomObserverCleanup`.
**Por qué.** La doctrina del proyecto es _"toda actividad de DOM pasa por
ActiveDom"_ (sin `window.addEventListener`, sin `new ResizeObserver`, sin
`document.querySelector` crudos en componentes). `listen` y `observe*` ya
cumplían esa regla devolviendo un **disposer** — la forma exacta que un
`$effect` de Svelte puede `return` para que el framework limpie en el
teardown. Pero el frame de animación quedaba fuera:
- `ActiveDom` exponía `requestFrame` / `cancelFrame` (resuelven bien la
ventana iframe/popup), pero devuelven el **handle numérico crudo**. Eso
obliga a cada consumidor a guardar el número, gestionar el guard de
"pendiente" y cancelar a mano en el teardown — bookkeeping repetido y
propenso a fugas.
- Los componentes que necesitaban un frame de _layout_ (medir y reposicionar
un overlay) caían en `requestAnimationFrame` / `cancelAnimationFrame`
**globales**. Eso apunta al `window` global, que es **incorrecto** en
contextos iframe / popup / happy-dom — el mismo bug que `getWindow` /
`getDocument` resuelven para el resto de la API.
`raf` cierra ese hueco: envuelve `requestFrame`/`cancelFrame` (cero lógica
nueva de scheduling) y devuelve el disposer simétrico. Un consumidor escribe
ahora `return dom.raf(reposition, node)` y Svelte cancela el frame solo.
**Detonante.** La auditoría del editor **Words** (2026-06-02) encontró 3
`requestAnimationFrame` crudos —`words-block-gutter`, `words-bubble-menu`,
`words-slash-menu`— que reposicionaban overlays apuntando al `window` global.
Migrados a `dom.raf(...)`. El precedente de rotura ya existía en otros sitios
del eidos (p. ej. `tabs-indicator`), así que esto canoniza el patrón, no es
un parche puntual de Words.
**Distinción que se mantiene.** `dom.raf` es para frames de **layout**
(medir/posicionar, ligados al ciclo de pintado). NO sustituye al servicio
`$timer` (`App.timers`), que es para _lifecycle timers con clave_
(debounce / heartbeat / intervalos / one-shots con delay). Son dominios
distintos: el timer no tiene primitiva de frame y `raf` no tiene clave ni
cancelación por scope. El `requestFrame`/`cancelFrame` de bajo nivel se
conserva para los pocos consumidores que ya guardan el handle (drawer,
slider, splitter, floating, focus-scope…) y aún no migran.
**Pendiente (no bloqueante).** Migrar los `requestAnimationFrame` crudos
restantes del eidos (`tabs-indicator`, etc.) a `dom.raf` cuando se toquen
esos componentes; no se hizo en barrido para no ensanchar el diff de la
auditoría de Words.
**Why.** The project doctrine is _"all DOM activity goes through ActiveDom"_ (no
raw `window.addEventListener`, no `new ResizeObserver`, no
`document.querySelector` in components). `listen` and `observe*` already followed
that rule by returning a **disposer** — the exact shape a Svelte `$effect` can
`return` so the framework cleans up on teardown. But the animation frame was left
out:
- `ActiveDom` exposed `requestFrame` / `cancelFrame` (they resolve the
iframe/popup window correctly), but they return the **raw numeric handle**.
That forces every consumer to store the number, manage the "pending" guard and
cancel by hand on teardown — repeated, leak-prone bookkeeping.
- Components that needed a _layout_ frame (measure and reposition an overlay)
fell back to **global** `requestAnimationFrame` / `cancelAnimationFrame`. That
points at the global `window`, which is **wrong** in iframe / popup / happy-dom
contexts — the same bug `getWindow` / `getDocument` solve for the rest of the
API.
`raf` closes that gap: it wraps `requestFrame`/`cancelFrame` (zero new scheduling
logic) and returns the symmetric disposer. A consumer now writes
`return dom.raf(reposition, node)` and Svelte cancels the frame by itself.
**Trigger.** The **Words** editor audit (2026-06-02) found 3 raw
`requestAnimationFrame` calls —`words-block-gutter`, `words-bubble-menu`,
`words-slash-menu`— that repositioned overlays pointing at the global `window`.
Migrated to `dom.raf(...)`. The breakage precedent already existed elsewhere in
eidos (e.g. `tabs-indicator`), so this canonizes the pattern, it is not a
one-off Words patch.
**Distinction that stays.** `dom.raf` is for **layout** frames (measure/position,
tied to the paint cycle). It does NOT replace the `$timer` service
(`App.timers`), which is for _keyed lifecycle timers_ (debounce / heartbeat /
intervals / delayed one-shots). They are distinct domains: the timer has no frame
primitive and `raf` has no key or scope-based cancellation. The low-level
`requestFrame`/`cancelFrame` is kept for the few consumers that already store the
handle (drawer, slider, splitter, floating, focus-scope…) and have not migrated
yet.
**Pending (non-blocking).** Migrate the remaining raw `requestAnimationFrame`
calls in eidos (`tabs-indicator`, etc.) to `dom.raf` when those components are
touched; not done in a sweep, to avoid widening the Words audit diff.

Loading…
Cancel
Save

Powered by TurnKey Linux.