|
|
|
|
# ActiveDom
|
|
|
|
|
|
|
|
|
|
`adom` contiene `ActiveDom`: el servicio DOM reactivo de aplicación.
|
|
|
|
|
|
|
|
|
|
## Handoff 2026-05-14
|
|
|
|
|
|
|
|
|
|
`ActiveDom` es la unica superficie permitida para mutar DOM gestionado desde
|
|
|
|
|
UIX. La decision P1 queda cerrada en `ActiveUix`: `dom:false` inyecta un
|
|
|
|
|
`disabledDom` no-op compartido por Soma/Sema/Eidos. No puede haber fallback
|
|
|
|
|
silencioso a escrituras directas dentro de UIX.
|
|
|
|
|
|
|
|
|
|
## 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 globales (`focus`, `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(...)`, `scrollWindowTo(...)`,
|
|
|
|
|
`scrollWindowBy(...)`, `requestFrame(...)` para acciones imperativas que no
|
|
|
|
|
deben depender del `window/document` global
|
|
|
|
|
- `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;
|
|
|
|
|
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;
|
|
|
|
|
requestFrame(
|
|
|
|
|
callback: FrameRequestCallback,
|
|
|
|
|
node?: Element | Window | Node | Document | null
|
|
|
|
|
): number;
|
|
|
|
|
cancelFrame(handle: number, 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` 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*`
|
|
|
|
|
- hub de `MutationObserver`
|
|
|
|
|
- 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`.
|
|
|
|
|
|
|
|
|
|
Va en `app.presentation`, porque es estado de presentación de aplicación, no una
|
|
|
|
|
primitive DOM.
|
|
|
|
|
|
|
|
|
|
### 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
|
|
|
|
|
|
|
|
|
|
Lo que queda para fases posteriores, si de verdad hace falta:
|
|
|
|
|
|
|
|
|
|
- reflexión de eventos semánticos al DOM
|
|
|
|
|
- observers compartidos
|
|
|
|
|
- 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.
|