You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/arts/adom
dev a4c04a00b3
Inject ActiveDom into Soma scroll lock
5 months ago
..
test Route Soma viewport scroll through ActiveDom 5 months ago
README.md Route Soma viewport scroll through ActiveDom 5 months ago
active-dom.svelte.ts Route Soma viewport scroll through ActiveDom 5 months ago
body-scroll-lock.svelte.ts Inject ActiveDom into Soma scroll lock 5 months ago
dom-context.svelte.ts eidos: pilot wrapper pattern + doctrinal API conventions 5 months ago
index.ts Route Soma DOM actions through ActiveDom 5 months ago
roving-focus-group.svelte.ts eidos: pilot wrapper pattern + doctrinal API conventions 5 months ago
viewport.svelte.ts eidos: pilot wrapper pattern + doctrinal API conventions 5 months ago

README.md

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 (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:

  • 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(...), 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:

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
  • 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:

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() 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.

Powered by TurnKey Linux.