La proyeccion cross-modal (dir · lang · data-motion/sound/haptic sobre <html>)
era cableado manual de cada app: solo 4 de ~13 boots de la demo la tenian, y el
estampado per-componente del valor de prefs actuaba de SUSTITUTO de la
proyeccion que faltaba. Para que el flip de P3 pueda soltar prefs del atributo,
el ambiente tiene que llegar al DOM una vez y siempre:
- createActiveUix la crea por defecto (opt-out `projectPrefs: false` cuando la
app posee <html>: i18n por routing, proyeccion propia). En attach es opt-IN —
el inverso — porque el host puede gobernar el documento.
- Se dispone la PRIMERA en dispose(), antes de que prefs/dom mueran debajo.
- Los 3 cableados manuales commiteables se borran EN ESTE MISMO commit (uix,
blocks, BootUix); el de web/routes/alpha se edita en arbol pero no se
commitea nunca (regla de la rama). Borrarlos junto a la automatica evita el
agujero medido del doble montaje: dispose() BORRA los attrs gestionados sin
restaurar.
Y LA SEMILLA, que es lo que hace la automatica segura: sin ella, el boot
REESCRIBIRIA un <html dir="rtl"> puesto a mano (el escenario 013ceac57) con el
valor derivado del idioma. `readPrefsEnvironmentFromDom` lee <html dir> UNA vez
al boot (SSR-safe: {} sin document; auto/vacio = "la pagina no declaro") y la
dimension direction la honra con la precedencia:
intent del usuario > semilla del entorno > derivacion(idioma) > default
Una declaracion a nivel de pagina es una AFIRMACION explicita; el enlace por
idioma es una heuristica — la afirmacion gana. Lo que pase la app en
options.prefs.environment pisa la semilla.
Un guard viejo fijaba el comportamiento anterior ("does not auto-project…") —
es exactamente lo que la decision D2 (firmada 2026-08-05) revierte. Pasa a
fijar el nuevo contrato: se proyectan SOLO los attrs cross-modales; data-theme/
mode/density siguen prohibidos (son de eidos).
Tests: semilla y precedencia 6/6 (dom-environment.test.ts) · proyeccion
automatica, opt-out, adopcion del dir a mano y limpieza en dispose (4 nuevos en
active-uix.svelte.test.ts) · suites active-uix + prefs 79/79.
Verificado en Chrome por el camino real: el toggle del topbar mueve <html dir>
via prefs->proyeccion AUTOMATICA (ltr -> rtl -> auto) con el cableado manual ya
borrado, en /uix y en /blocks; <html lang> viaja con el.
check 77 = linea base · rtl:check 1 (palabras) · docs:check 0/566.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
parent
9158d07964
commit
973d2c886d
@ -0,0 +1,38 @@
|
||||
import type { PrefsEnvironment } from '$libs/prefs';
|
||||
import { DIRECTIONS, type Direction } from '$libs/direction';
|
||||
|
||||
export interface DomEnvironmentOverrides {
|
||||
/** Injectable for tests / non-global documents. Defaults to `globalThis.document`. */
|
||||
readonly document?: Document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the environment facts the PAGE itself declares — today, the reading
|
||||
* direction of `<html dir>`.
|
||||
*
|
||||
* One-shot, at boot, BEFORE the DOM projection first applies: this is the seed
|
||||
* that stops an automatic projection from rewriting a hand-set
|
||||
* `<html dir="rtl">` with the language-derived value. It is not a live source —
|
||||
* direction stays one-way (prefs → DOM) after boot; re-running the read would
|
||||
* only echo the projection's own write, which is idempotent by construction
|
||||
* (the dimension resolves `env.direction` to the same value it projected).
|
||||
*
|
||||
* `dir="auto"`, empty and missing all read as "the page declared nothing" —
|
||||
* only the two concrete directions are assertions.
|
||||
*
|
||||
* SSR-safe: with no `document` it returns `{}` and the dimension falls through
|
||||
* to language derivation.
|
||||
*/
|
||||
export function readPrefsEnvironmentFromDom(
|
||||
overrides: DomEnvironmentOverrides = {}
|
||||
): PrefsEnvironment {
|
||||
const doc = overrides.document ?? globalThis.document;
|
||||
if (doc === undefined) return {};
|
||||
|
||||
const raw = doc.documentElement?.getAttribute('dir');
|
||||
if (typeof raw !== 'string') return {};
|
||||
const value = raw.toLowerCase();
|
||||
if (!DIRECTIONS.includes(value as Direction)) return {};
|
||||
|
||||
return { direction: value as Direction };
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createActivePrefs, directionDimension, localeDimension } from '$prefs';
|
||||
import { readPrefsEnvironmentFromDom } from '../adapters/dom-environment.ts';
|
||||
|
||||
function fakeDocument(dir: string | null): Document {
|
||||
return {
|
||||
documentElement: {
|
||||
getAttribute: (name: string) => (name === 'dir' ? dir : null)
|
||||
}
|
||||
} as unknown as Document;
|
||||
}
|
||||
|
||||
describe('readPrefsEnvironmentFromDom', () => {
|
||||
it('reads a concrete <html dir> into env.direction', () => {
|
||||
expect(readPrefsEnvironmentFromDom({ document: fakeDocument('rtl') })).toEqual({
|
||||
direction: 'rtl'
|
||||
});
|
||||
expect(readPrefsEnvironmentFromDom({ document: fakeDocument('LTR') })).toEqual({
|
||||
direction: 'ltr'
|
||||
});
|
||||
});
|
||||
|
||||
it('missing, empty and auto all read as "the page declared nothing"', () => {
|
||||
expect(readPrefsEnvironmentFromDom({ document: fakeDocument(null) })).toEqual({});
|
||||
expect(readPrefsEnvironmentFromDom({ document: fakeDocument('') })).toEqual({});
|
||||
expect(readPrefsEnvironmentFromDom({ document: fakeDocument('auto') })).toEqual({});
|
||||
});
|
||||
|
||||
it('is SSR-safe: no document → {}', () => {
|
||||
expect(readPrefsEnvironmentFromDom({ document: undefined })).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('directionDimension — environment seed precedence', () => {
|
||||
const schema = {
|
||||
language: localeDimension({ catalog: ['es-ES', 'ar-EG'], default: 'es-ES' }),
|
||||
direction: directionDimension()
|
||||
};
|
||||
|
||||
it('the seed beats language derivation (the page asserted, the language is a heuristic)', () => {
|
||||
const prefs = createActivePrefs({
|
||||
schema,
|
||||
environment: { direction: 'rtl' }
|
||||
});
|
||||
// language resolves es-ES → derivation would say ltr; the seed wins.
|
||||
expect(prefs.direction.get()).toBe('rtl');
|
||||
prefs.dispose();
|
||||
});
|
||||
|
||||
it('user intent beats the seed', () => {
|
||||
const prefs = createActivePrefs({
|
||||
schema,
|
||||
environment: { direction: 'rtl' }
|
||||
});
|
||||
prefs.direction.set('ltr');
|
||||
expect(prefs.direction.get()).toBe('ltr');
|
||||
// Clearing the intent falls back to the seed, not to the derivation.
|
||||
prefs.direction.clear();
|
||||
expect(prefs.direction.get()).toBe('rtl');
|
||||
prefs.dispose();
|
||||
});
|
||||
|
||||
it('without a seed the derivation still runs', () => {
|
||||
const prefs = createActivePrefs({ schema, environment: {} });
|
||||
expect(prefs.direction.get()).toBe('ltr');
|
||||
prefs.language.set('ar-EG');
|
||||
expect(prefs.direction.get()).toBe('rtl');
|
||||
prefs.dispose();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue