La cadena gana el eslabon que el usuario dejo aparcado y ahora ratifica
(2026-08-05): prop -> afirmacion del ancestro (DirectionContext) -> prefs.
Cada activeDir PUBLICA su afirmacion (prop ?? heredada) y consulta la del
ancestro antes de caer a prefs.
Por que: la cadena por-componente metia prefs en la ruta del ATRIBUTO, y el
boot por defecto SIEMPRE tiene la dimension direction (deriva del idioma,
fallback ltr) — asi que "nadie afirmo" era inalcanzable en la practica y cada
hijo del canon estampaba ltr dentro de un subarbol afirmado rtl, cortando la
herencia. La otra sesion lo midio dos veces en el campo: un menu compuesto en
sitio aterrizaba en la preferencia global (LTR chrome dentro de un player RTL),
y el panel portalizado resolvia por su cuenta aunque arreglaras lo primero. Un
solo eslabon cierra los dos agujeros, porque la capa flotante ya lleva el
opts.dir del dueno — que ahora incorpora el contexto.
Con la fisica, las convenciones se borran:
- los 3 reenvios a mano de 4.3 (natural-time-picker-panel, emoji-picker-content
x3, color-field-format-select) — el contexto los hace
- el enlace manual del submenu (dir ?? parentMenu?.opts.dir.current) — idem
- los 3 canarios dir= de media-player que la otra sesion dejo puestos a
proposito ("si al quitarlos el panel vuelve a salir LTR, el mecanismo no
llego al portal")
El canario canta: su repro exacto (media-player, manual parts, direction rtl,
float de subtitulos) da panel rtl SIN el dir= — y el volume float igual.
Medido ademas en Chrome: islas 0 en emoji-picker, natural-time-picker y
color-picker sin ningun reenvio; sliders de canal en espejo exacto (hue 210 a
0.417 del borde fisico); submenu de dropdown con data-side=left en RTL
derivado del contexto.
Regla de colocacion (documentada en direction.ts y el contrato): activeDir lee
y publica contexto de Svelte, asi que corre SIEMPRE en la init del wrapper —
nunca en constructores de provider. Los tests de provider construyen directo y
no se enteran; los 91 mocks de Soma.require() sobreviven porque getOr(undefined)
cae a traves de prefs.
Test nuevo (direction.svelte.test.ts + harnesses reales, 5/5): el hijo hereda
la afirmacion, su prop la pisa, sin afirmacion queda undefined (nunca un
default), sin ancestro cae a prefs, y la publicacion es REACTIVA.
check 77 = linea base · 71/71 en los 9 ambitos tocados · rtl:check 1
(palabras, preexistente) · docs:check 0/566. El contrato §1 pasa a cuatro
eslabones y §1 "Composition carries the assertion" documenta la fisica.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
parent
1d442c59dc
commit
9158d07964
@ -0,0 +1,128 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { flushSync, mount, unmount } from 'svelte';
|
||||
import type { Active } from '$libs/reactive';
|
||||
import type { Direction } from './types';
|
||||
import DirectionHarness from './test/direction-harness.svelte';
|
||||
import DirectionHarnessChild from './test/direction-harness-child.svelte';
|
||||
|
||||
/**
|
||||
* The context link of the chain (`prop → ancestor assertion → prefs`): a canon
|
||||
* component mounted inside an asserted subtree resolves the ancestor's
|
||||
* assertion without any hand-written `dir=` forward. Ratified 2026-08-05 —
|
||||
* the "implicit inheritance" phase of the direction contract.
|
||||
*
|
||||
* `*.svelte.test.ts` because `activeDir` is rune-backed ($derived via
|
||||
* readableActive); the server vitest project compiles `$state` away and would
|
||||
* pass regardless of the code.
|
||||
*/
|
||||
|
||||
function mountHarness(props: {
|
||||
dir?: Direction;
|
||||
childDir?: Direction;
|
||||
}): {
|
||||
parent: () => Active<Direction | undefined>;
|
||||
child: () => Active<Direction | undefined>;
|
||||
dispose: () => void;
|
||||
} {
|
||||
const target = document.createElement('div');
|
||||
document.body.appendChild(target);
|
||||
let parent: Active<Direction | undefined> | undefined;
|
||||
let child: Active<Direction | undefined> | undefined;
|
||||
const component = mount(DirectionHarness, {
|
||||
target,
|
||||
props: {
|
||||
...props,
|
||||
report: (resolved: Active<Direction | undefined>) => (parent = resolved),
|
||||
reportChild: (resolved: Active<Direction | undefined>) => (child = resolved)
|
||||
}
|
||||
});
|
||||
flushSync();
|
||||
return {
|
||||
parent: () => {
|
||||
if (!parent) throw new Error('parent harness did not report');
|
||||
return parent;
|
||||
},
|
||||
child: () => {
|
||||
if (!child) throw new Error('child harness did not report');
|
||||
return child;
|
||||
},
|
||||
dispose: () => {
|
||||
void unmount(component);
|
||||
target.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('activeDir — the DirectionContext link', () => {
|
||||
it('a child with no prop resolves the ancestor assertion', () => {
|
||||
const h = mountHarness({ dir: 'rtl' });
|
||||
expect(h.parent().current).toBe('rtl');
|
||||
expect(h.child().current).toBe('rtl');
|
||||
h.dispose();
|
||||
});
|
||||
|
||||
it('the child own prop beats the ancestor assertion', () => {
|
||||
const h = mountHarness({ dir: 'rtl', childDir: 'ltr' });
|
||||
expect(h.child().current).toBe('ltr');
|
||||
h.dispose();
|
||||
});
|
||||
|
||||
it('nobody asserted anywhere → undefined, never a default', () => {
|
||||
const h = mountHarness({});
|
||||
expect(h.parent().current).toBeUndefined();
|
||||
expect(h.child().current).toBeUndefined();
|
||||
h.dispose();
|
||||
});
|
||||
|
||||
it('a lone component with no ancestor stays on its own chain', () => {
|
||||
const target = document.createElement('div');
|
||||
document.body.appendChild(target);
|
||||
let resolved: Active<Direction | undefined> | undefined;
|
||||
const component = mount(DirectionHarnessChild, {
|
||||
target,
|
||||
props: {
|
||||
dir: 'rtl',
|
||||
report: (r: Active<Direction | undefined>) => (resolved = r)
|
||||
}
|
||||
});
|
||||
flushSync();
|
||||
expect(resolved?.current).toBe('rtl');
|
||||
void unmount(component);
|
||||
target.remove();
|
||||
});
|
||||
|
||||
it('the assertion is REACTIVE: flipping the ancestor prop reaches the child', () => {
|
||||
const props = $state<{
|
||||
dir?: Direction;
|
||||
childDir?: Direction;
|
||||
}>({ dir: undefined });
|
||||
const target = document.createElement('div');
|
||||
document.body.appendChild(target);
|
||||
let child: Active<Direction | undefined> | undefined;
|
||||
const component = mount(DirectionHarness, {
|
||||
target,
|
||||
props: {
|
||||
get dir() {
|
||||
return props.dir;
|
||||
},
|
||||
report: () => {},
|
||||
reportChild: (r: Active<Direction | undefined>) => (child = r)
|
||||
}
|
||||
});
|
||||
flushSync();
|
||||
expect(child?.current).toBeUndefined();
|
||||
|
||||
props.dir = 'rtl';
|
||||
flushSync();
|
||||
expect(child?.current).toBe('rtl');
|
||||
|
||||
props.dir = undefined;
|
||||
flushSync();
|
||||
expect(child?.current).toBeUndefined();
|
||||
|
||||
void unmount(component);
|
||||
target.remove();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
// Test harness — a bare canon-shaped child: runs `activeDir` at init exactly
|
||||
// like a component wrapper does and hands the resulting Active back to the
|
||||
// test. No soma on purpose: these fixtures exercise the CONTEXT link; the
|
||||
// prefs link is covered by `active-uix/test/prefs-view.svelte.test.ts`.
|
||||
import { activeDir } from '../direction';
|
||||
import type { Active } from '$libs/reactive';
|
||||
import type { Direction } from '../types';
|
||||
|
||||
let {
|
||||
dir = undefined,
|
||||
report
|
||||
}: {
|
||||
dir?: Direction;
|
||||
report: (resolved: Active<Direction | undefined>) => void;
|
||||
} = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally — the harness deliberately hands
|
||||
// the Active out once at init; `report` is not expected to be reactive.
|
||||
report(activeDir(() => dir, undefined));
|
||||
</script>
|
||||
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
// Test harness — an asserting ancestor with a canon-shaped child mounted in
|
||||
// place. The parent runs `activeDir` (publishing its assertion into
|
||||
// DirectionContext) exactly like a component wrapper does; the child runs its
|
||||
// own chain with no prop. What the test asserts: the child resolves the
|
||||
// ancestor's assertion without any hand-written `dir=` forward.
|
||||
import DirectionHarnessChild from './direction-harness-child.svelte';
|
||||
import { activeDir } from '../direction';
|
||||
import type { Active } from '$libs/reactive';
|
||||
import type { Direction } from '../types';
|
||||
|
||||
let {
|
||||
dir = undefined,
|
||||
childDir = undefined,
|
||||
report,
|
||||
reportChild
|
||||
}: {
|
||||
dir?: Direction;
|
||||
childDir?: Direction;
|
||||
report: (resolved: Active<Direction | undefined>) => void;
|
||||
reportChild: (resolved: Active<Direction | undefined>) => void;
|
||||
} = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally — the harness deliberately hands
|
||||
// the Active out once at init; `report` is not expected to be reactive.
|
||||
report(activeDir(() => dir, undefined));
|
||||
</script>
|
||||
|
||||
<DirectionHarnessChild dir={childDir} report={reportChild} />
|
||||
Loading…
Reference in new issue