11 KiB
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/windowpropietario 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:
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:
viewportbreakpointscurrentBreakpointresolve(...)isAtLeast(...)matches(...)apply(...)/remove(...)como superficie unica de mutacion de atributoslisten(...)como superficie unica para listeners globales o transversalesquery(...),elementFromPoint(...),activeElement(...)para consultas contra el documento propietariofocus(...),scrollIntoView(...),scrollTo(...),scrollWindowTo(...),scrollWindowBy(...),requestFrame(...)para acciones imperativas que no deben depender delwindow/documentglobalBodyScrollLockcomo helper global de body scroll lock, sin bloquear eventos de punteroDOMContextcomo helper scoped paraDocument/ShadowRootRovingFocusGroupcomo 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:
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:
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;
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:
const dom = createActiveDom({
breakpoints: readableActive(() => ({
lg: 1100
}))
});
const domWithDefaults = createActiveDom();
Responsive:
const columns = dom.resolve({ base: 1, sm: 2, lg: 3 });
const tone = dom.resolve({ base: 'compact', md: 'normal', xl: 'wide' });
Mutacion DOM:
dom.apply({
target: node,
attrs: {
'data-state': 'open',
'aria-busy': true,
'data-hidden': false
}
});
dom.remove(node, ['data-state', 'aria-busy']);
Nodos gestionados:
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 breakpointses opcional; si no se pasa, usaBREAKPOINTS_DEFAULT- no hay herencia de
dompadre ActiveDomes servicio de app, no scope anidadoActiveDomregistra el listener deresizeal construirse (por instancia). El singleton compartido (shareViewport: true) pospone la asignación del listener a la primera lectura del viewport — importar$adomno aloca estado reactivo si nadie consume el viewport.applysolo escribe atributos; no interpreta semantica ni eventos- listeners de
document/window, observers (ResizeObserver,MutationObserver,IntersectionObserver) y acciones imperativas transversales van porActiveDom - lecturas locales de un elemento propio (
getBoundingClientRect,contains,closest,clientWidth,scrollTop) siguen siendo responsabilidad del componente; envolverlas enActiveDomseria ruido writeNode/writeTextson para nodos owned por servicios UIX (live regions, descripciones ocultas, style hosts auxiliares), no para saltarse el render de Svelte en componentes normalesfalse | null | undefinedremueven atributosviewportes per-instancia por defecto: cadaActiveDomposee su propio listener deresize, scoped altargetWindow(defaultwindow). Esto evita filtraciones entre tests, iframes, popups y entornos happy-dom. Llamardispose()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:
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:
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()responsiveapply()/remove()BodyScrollLockDOMContextRovingFocusGroup
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.domApp.domviewportbreakpointscurrentBreakpoint- 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
DocumentoShadowRoot - introspección/diagnóstico de runtime más rica
La regla importante por ahora es simple:
ActiveDomes el servicio reactivo de DOM de la app;$libs/domes su base pura.