|
|
|
|
# ActiveDom
|
|
|
|
|
|
|
|
|
|
`adom` contiene `ActiveDom`: el servicio DOM reactivo de aplicación.
|
|
|
|
|
|
|
|
|
|
## Qué es hoy
|
|
|
|
|
|
|
|
|
|
Ahora mismo `ActiveDom` no es un bus de eventos semánticos ni un reflector de
|
|
|
|
|
`data-event*`.
|
|
|
|
|
|
|
|
|
|
Su responsabilidad actual es más pequeña y más concreta:
|
|
|
|
|
|
|
|
|
|
- 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`,
|
|
|
|
|
`scrollWindowBy`, `requestFrame`)
|
|
|
|
|
- escribir/remover nodos gestionados y texto accesible (`writeNode`,
|
|
|
|
|
`writeText`, `removeNode`)
|
|
|
|
|
|
|
|
|
|
En otras palabras:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
libs/dom -> arts/adom -> App.dom / app.dom
|
|
|
|
|
puro reactivo consumo de app
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Composicion 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.
|
|
|
|
|
|
|
|
|
|
## Qué pertenece a cada capa
|
|
|
|
|
|
|
|
|
|
### `libs/dom`
|
|
|
|
|
|
|
|
|
|
Primitives DOM puras o casi puras:
|
|
|
|
|
|
|
|
|
|
- guards y traversal DOM
|
|
|
|
|
- focus helpers
|
|
|
|
|
- tabbable helpers
|
|
|
|
|
- responsive helpers puros
|
|
|
|
|
|
|
|
|
|
**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.
|
|
|
|
|
|
|
|
|
|
No mantiene estado de aplicación.
|
|
|
|
|
|
|
|
|
|
### `arts/adom`
|
|
|
|
|
|
|
|
|
|
Runtime reactivo de DOM:
|
|
|
|
|
|
|
|
|
|
- `viewport`
|
|
|
|
|
- `breakpoints`
|
|
|
|
|
- `currentBreakpoint`
|
|
|
|
|
- `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
|
|
|
|
|
- `focus(...)`, `scrollIntoView(...)`, `scrollTo(...)`, `scrollWindowTo(...)`,
|
feat(adom): canonize dom.raf() frame scheduler + migrate Words overlays
Add `raf(callback, node?): ActiveDomFrameCleanup` to the ActiveDom surface —
a one-shot animation frame that returns an idempotent **disposer** (the same
`() => void` shape as `listen` / `observe*`), so an `$effect` can
`return dom.raf(...)` and Svelte cancels the pending frame on teardown. It
wraps the existing `requestFrame` / `cancelFrame` (which already resolve the
instance's `targetWindow` — iframe / popup / happy-dom safe), adding no new
scheduling logic. New `ActiveDomFrameCleanup` type exported from the barrel;
`raf` also implemented on the disabled-dom stub (throws, like `requestFrame`).
Why: the doctrine is "all DOM activity via ActiveDom". `listen`/`observe*`
already returned disposers; the animation frame was the gap — `requestFrame`
exposes a raw numeric handle (per-call bookkeeping + leak risk), and layout
components were falling back to the GLOBAL `requestAnimationFrame`, which
targets the wrong window in iframe/popup contexts (the exact bug getWindow/
getDocument fix elsewhere). `raf` closes it.
Migrated the 3 raw `requestAnimationFrame` sites the Words audit surfaced —
words-block-gutter (reposition), words-bubble-menu + words-slash-menu
(overlay position) — to `dom.raf(...)`. Bubble/slash keep their `tick()`
fallback for no-rAF environments.
Documented the decision + rationale as a dated Backlog entry at the end of
`src/arts/adom/README.md` (and listed `raf` in the API + imperative-actions
sections). Notes the kept distinction: `raf` is for layout frames, NOT the
`$timer` lifecycle scheduler; low-level requestFrame/cancelFrame stays for
consumers that already hold the handle (drawer/slider/splitter/floating/
focus-scope). Remaining raw rAF in other eidos components (tabs-indicator…)
left for when those are touched — flagged in the backlog.
Gates: npm run check 1 error (pre-existing grafito, not adom/Words) · soma
words + adom 479/479 · prettier clean · browser smoke: gutter repositions,
bubble menu positions, no console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
|
|
|
`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
|
|
|
|
|
|
|
|
|
|
`ActiveDom` sí mantiene estado reactivo y por eso vive aquí, no en `$libs/dom`.
|
|
|
|
|
|
|
|
|
|
## Posición en App
|
|
|
|
|
|
|
|
|
|
`ActiveDom` vive a nivel de aplicación:
|
|
|
|
|
|
|
|
|
|
```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`.
|
|
|
|
|
|
|
|
|
|
## API actual
|
|
|
|
|
|
|
|
|
|
La API pública real de `ActiveDom` hoy es esta:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
export interface ActiveDom {
|
|
|
|
|
breakpoints: Active<Breakpoints>;
|
|
|
|
|
viewport: { readonly width: number };
|
|
|
|
|
currentBreakpoint: Active<Breakpoint>;
|
|
|
|
|
resolve<T>(value: ResponsiveProp<T> | undefined): T | undefined;
|
|
|
|
|
isAtLeast(breakpoint: Breakpoint): boolean;
|
|
|
|
|
matches(breakpoint: Breakpoint): boolean;
|
|
|
|
|
apply(change: StructuralChange): void;
|
|
|
|
|
remove(target: HTMLElement, names: readonly string[]): void;
|
|
|
|
|
writeStyle(
|
|
|
|
|
id: string,
|
|
|
|
|
css: string,
|
|
|
|
|
options?: ActiveDomWriteStyleOptions
|
|
|
|
|
): HTMLStyleElement | undefined;
|
|
|
|
|
removeStyle(id: string, options?: ActiveDomRemoveStyleOptions): void;
|
|
|
|
|
writeNode(id: string, options?: ActiveDomWriteNodeOptions): HTMLElement | undefined;
|
|
|
|
|
writeText(target: HTMLElement, text: string): void;
|
|
|
|
|
removeNode(id: string, options?: ActiveDomRemoveNodeOptions): void;
|
|
|
|
|
listen(
|
|
|
|
|
target: EventTarget,
|
|
|
|
|
event: string | readonly string[],
|
|
|
|
|
handler: EventListener
|
|
|
|
|
): () => void;
|
|
|
|
|
observeResize(
|
|
|
|
|
target: Element,
|
|
|
|
|
callback: ResizeObserverCallback,
|
|
|
|
|
options?: ResizeObserverOptions
|
|
|
|
|
): () => void;
|
|
|
|
|
observeMutation(
|
|
|
|
|
target: Node,
|
|
|
|
|
callback: MutationCallback,
|
|
|
|
|
options: MutationObserverInit
|
|
|
|
|
): () => void;
|
|
|
|
|
observeIntersection(
|
|
|
|
|
target: Element,
|
|
|
|
|
callback: IntersectionObserverCallback,
|
|
|
|
|
options?: IntersectionObserverInit
|
|
|
|
|
): () => void;
|
|
|
|
|
activeElement(node?: Element | Window | Node | Document | null): Element | null;
|
|
|
|
|
query<T extends Element = Element>(
|
|
|
|
|
selector: string,
|
|
|
|
|
root?: ParentNode | Document | null
|
|
|
|
|
): T | null;
|
|
|
|
|
elementFromPoint(
|
|
|
|
|
x: number,
|
|
|
|
|
y: number,
|
|
|
|
|
node?: Element | Window | Node | Document | null
|
|
|
|
|
): Element | null;
|
|
|
|
|
focus(target: HTMLElement | null | undefined, options?: FocusOptions): void;
|
|
|
|
|
scrollIntoView(target: Element | null | undefined, arg?: boolean | ScrollIntoViewOptions): void;
|
|
|
|
|
scrollTo(target: Element | null | undefined, arg: ScrollToOptions | number, y?: number): void;
|
|
|
|
|
requestFrame(
|
|
|
|
|
callback: FrameRequestCallback,
|
|
|
|
|
node?: Element | Window | Node | Document | null
|
|
|
|
|
): number;
|
|
|
|
|
cancelFrame(handle: number, node?: Element | Window | Node | Document | null): void;
|
feat(adom): canonize dom.raf() frame scheduler + migrate Words overlays
Add `raf(callback, node?): ActiveDomFrameCleanup` to the ActiveDom surface —
a one-shot animation frame that returns an idempotent **disposer** (the same
`() => void` shape as `listen` / `observe*`), so an `$effect` can
`return dom.raf(...)` and Svelte cancels the pending frame on teardown. It
wraps the existing `requestFrame` / `cancelFrame` (which already resolve the
instance's `targetWindow` — iframe / popup / happy-dom safe), adding no new
scheduling logic. New `ActiveDomFrameCleanup` type exported from the barrel;
`raf` also implemented on the disabled-dom stub (throws, like `requestFrame`).
Why: the doctrine is "all DOM activity via ActiveDom". `listen`/`observe*`
already returned disposers; the animation frame was the gap — `requestFrame`
exposes a raw numeric handle (per-call bookkeeping + leak risk), and layout
components were falling back to the GLOBAL `requestAnimationFrame`, which
targets the wrong window in iframe/popup contexts (the exact bug getWindow/
getDocument fix elsewhere). `raf` closes it.
Migrated the 3 raw `requestAnimationFrame` sites the Words audit surfaced —
words-block-gutter (reposition), words-bubble-menu + words-slash-menu
(overlay position) — to `dom.raf(...)`. Bubble/slash keep their `tick()`
fallback for no-rAF environments.
Documented the decision + rationale as a dated Backlog entry at the end of
`src/arts/adom/README.md` (and listed `raf` in the API + imperative-actions
sections). Notes the kept distinction: `raf` is for layout frames, NOT the
`$timer` lifecycle scheduler; low-level requestFrame/cancelFrame stays for
consumers that already hold the handle (drawer/slider/splitter/floating/
focus-scope). Remaining raw rAF in other eidos components (tabs-indicator…)
left for when those are touched — flagged in the backlog.
Gates: npm run check 1 error (pre-existing grafito, not adom/Words) · soma
words + adom 479/479 · prettier clean · browser smoke: gutter repositions,
bubble menu positions, no console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
|
|
|
raf(callback: FrameRequestCallback, node?: Element | Window | Node | Document | null): () => void;
|
|
|
|
|
scrollWindowBy(
|
|
|
|
|
arg: ScrollToOptions | number,
|
|
|
|
|
y?: number,
|
|
|
|
|
node?: Element | Window | Node | Document | null
|
|
|
|
|
): void;
|
|
|
|
|
scrollWindowTo(
|
|
|
|
|
arg: ScrollToOptions | number,
|
|
|
|
|
y?: number,
|
|
|
|
|
node?: Element | Window | Node | Document | null
|
|
|
|
|
): void;
|
|
|
|
|
getDocument(node?: Element | Window | Node | Document | null): Document;
|
|
|
|
|
getWindow(node?: Node | ShadowRoot | Document | Window | null): Window;
|
|
|
|
|
dispose(): void;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Creación:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
const dom = createActiveDom({
|
|
|
|
|
breakpoints: readableActive(() => ({
|
|
|
|
|
lg: 1100
|
|
|
|
|
}))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const domWithDefaults = createActiveDom();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Responsive:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
const columns = dom.resolve({ base: 1, sm: 2, lg: 3 });
|
|
|
|
|
const tone = dom.resolve({ base: 'compact', md: 'normal', xl: 'wide' });
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Mutacion DOM:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
dom.apply({
|
|
|
|
|
target: node,
|
|
|
|
|
attrs: {
|
|
|
|
|
'data-state': 'open',
|
|
|
|
|
'aria-busy': true,
|
|
|
|
|
'data-hidden': false
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dom.remove(node, ['data-state', 'aria-busy']);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Nodos gestionados:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
const region = dom.writeNode('app-live-region', {
|
|
|
|
|
host: dom.getDocument().body,
|
|
|
|
|
attrs: { role: 'status', 'aria-live': 'polite' },
|
|
|
|
|
text: 'Ready'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```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.
|
|
|
|
|
|
|
|
|
|
### Tracking de un window distinto
|
|
|
|
|
|
|
|
|
|
Para iframes, popups o entornos de test:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
const iframeDom = createActiveDom({ targetWindow: iframe.contentWindow! });
|
|
|
|
|
const popupDom = createActiveDom({ targetWindow: popup });
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Ignorado cuando `shareViewport: true` (el singleton siempre rastrea el
|
|
|
|
|
top-level `window`).
|
|
|
|
|
|
|
|
|
|
## Qué no es
|
|
|
|
|
|
|
|
|
|
`ActiveDom` no es:
|
|
|
|
|
|
|
|
|
|
- `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`
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
## Pagina De Prueba
|
|
|
|
|
|
|
|
|
|
La pagina manual esta en `/test/adom` y cubre:
|
|
|
|
|
|
|
|
|
|
- viewport y breakpoint actual
|
|
|
|
|
- `resolve()` responsive
|
|
|
|
|
- `apply()` / `remove()`
|
|
|
|
|
- `BodyScrollLock`
|
|
|
|
|
- `DOMContext`
|
|
|
|
|
- `RovingFocusGroup`
|
|
|
|
|
|
|
|
|
|
## Relación con otras piezas
|
|
|
|
|
|
|
|
|
|
### Semántica
|
|
|
|
|
|
|
|
|
|
La semántica pertenece a `Sema` y a `EngineSemantic`, no a `ActiveDom`.
|
|
|
|
|
|
|
|
|
|
Si mañana `ActiveDom` refleja eventos al DOM, será como consumidor de
|
|
|
|
|
`EngineSemantic`, no como autoridad semántica.
|
|
|
|
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
|
|
|
|
### Air y Terra
|
|
|
|
|
|
|
|
|
|
`air` y `terra` no consumen esta capa nueva.
|
|
|
|
|
|
|
|
|
|
Su código actual sirve como referencia histórica para extraer utilidades hacia
|
|
|
|
|
`$libs/dom`, pero no forman parte del runtime nuevo.
|
|
|
|
|
|
|
|
|
|
## Estado del diseño
|
|
|
|
|
|
|
|
|
|
`ActiveDom` está en fase fundacional.
|
|
|
|
|
|
|
|
|
|
Lo que ya está cerrado:
|
|
|
|
|
|
|
|
|
|
- `app.dom`
|
|
|
|
|
- `App.dom`
|
|
|
|
|
- `viewport`
|
|
|
|
|
- `breakpoints`
|
|
|
|
|
- `currentBreakpoint`
|
|
|
|
|
- resolución responsive
|
|
|
|
|
- listeners, observers y acciones imperativas scoped a la ventana propietaria
|
|
|
|
|
del target
|
|
|
|
|
|
|
|
|
|
Lo que queda para fases posteriores, si de verdad hace falta:
|
|
|
|
|
|
|
|
|
|
- reflexión de eventos semánticos al DOM
|
|
|
|
|
- APIs por `Document` o `ShadowRoot`
|
|
|
|
|
- introspección/diagnóstico de runtime más rica
|
|
|
|
|
|
|
|
|
|
La regla importante por ahora es simple:
|
|
|
|
|
|
|
|
|
|
> `ActiveDom` es el servicio reactivo de DOM de la app; `$libs/dom` es su base pura.
|
feat(adom): canonize dom.raf() frame scheduler + migrate Words overlays
Add `raf(callback, node?): ActiveDomFrameCleanup` to the ActiveDom surface —
a one-shot animation frame that returns an idempotent **disposer** (the same
`() => void` shape as `listen` / `observe*`), so an `$effect` can
`return dom.raf(...)` and Svelte cancels the pending frame on teardown. It
wraps the existing `requestFrame` / `cancelFrame` (which already resolve the
instance's `targetWindow` — iframe / popup / happy-dom safe), adding no new
scheduling logic. New `ActiveDomFrameCleanup` type exported from the barrel;
`raf` also implemented on the disabled-dom stub (throws, like `requestFrame`).
Why: the doctrine is "all DOM activity via ActiveDom". `listen`/`observe*`
already returned disposers; the animation frame was the gap — `requestFrame`
exposes a raw numeric handle (per-call bookkeeping + leak risk), and layout
components were falling back to the GLOBAL `requestAnimationFrame`, which
targets the wrong window in iframe/popup contexts (the exact bug getWindow/
getDocument fix elsewhere). `raf` closes it.
Migrated the 3 raw `requestAnimationFrame` sites the Words audit surfaced —
words-block-gutter (reposition), words-bubble-menu + words-slash-menu
(overlay position) — to `dom.raf(...)`. Bubble/slash keep their `tick()`
fallback for no-rAF environments.
Documented the decision + rationale as a dated Backlog entry at the end of
`src/arts/adom/README.md` (and listed `raf` in the API + imperative-actions
sections). Notes the kept distinction: `raf` is for layout frames, NOT the
`$timer` lifecycle scheduler; low-level requestFrame/cancelFrame stays for
consumers that already hold the handle (drawer/slider/splitter/floating/
focus-scope). Remaining raw rAF in other eidos components (tabs-indicator…)
left for when those are touched — flagged in the backlog.
Gates: npm run check 1 error (pre-existing grafito, not adom/Words) · soma
words + adom 479/479 · prettier clean · browser smoke: gutter repositions,
bubble menu positions, no console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Backlog / Decisiones de evolución
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
### 2026-06-02 — `dom.raf(callback, node?)`: frame de animación con disposer
|
|
|
|
|
|
|
|
|
|
**Qué.** Nuevo método en la superficie `ActiveDom`:
|
|
|
|
|
|
|
|
|
|
```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` /
|
|
|
|
|
`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.
|