feat(direction): el estampado pertenece al runtime — y olvidarlo ya no compila

Era la ultima convencion manual del eje: una linea por proveedor, 49 copias de
ella, y 20 de 55 componentes que simplemente la habian olvidado. Una convencion
que hay que repetir 49 veces y que un tercio del catalogo incumple no es una
convencion — es una abstraccion que falta.

VEHICULO A, y el handoff tenia razon desde el principio. Yo habia recomendado B
(un AttrPlan del compilador de morfo) con el argumento de que A deja `dir`
invisible para `contracts.cssSelectors` y `eidos-lint`. Medido: FALSO. El linter
sólo clasifica selectores `[data-*]`, y las recetas leen la direccion con
`:dir()`, una pseudo-clase que no toca ninguno de los dos guards. El coste que
le achacaba a A no existe.

Y B no llegaba donde yo decia. Reparto real de los 49 sitios segun la bolsa que
expanden:

  renderProps()          21   un AttrPlan les llegaria
  runtimePart.props      27   NO les llegaria
  ninguna                 1

O sea B aterriza en 21 de 49 y exige migrar 28 componentes de `.props` a
`renderProps()` — que no es cosmetico: `renderProps()` emite TODOS los attrs del
morfo de esa parte. A, en cambio, inyecta en `partPropsForRegistration`, que
incluyen LAS DOS bolsas, y llega a 48 de 49 desde un solo punto. Su precedente
es literal: `id`, el marcador, `data-archetype` y `reg.attachment` ya se
inyectan ahi sin declaracion en morfo.

EL GATE ES DE TIPOS, que es el premio de verdad:

    dir: SomaRuntimeDir | null    // OBLIGATORIO en SomaRuntimeSources

`null` es la unica salida y dice algo cierto: este runtime no estampa direccion.
Olvidarlo es un error de compilacion, no un defecto que nadie ve hasta que
alguien abre el componente en RTL.

`dir.parts` responde la otra mitad de la pregunta del contrato — QUE elemento
lleva la pintura — por defecto `['provider']`. El censo dice que 44 de 49 son la
raiz; los cinco que no: dropdown-menu ['trigger'] (su raiz no renderiza
elemento), drawer y float-panel ['content'] (pintan en el portal), number-field
y css-field ['provider','input'].

VERIFICADO en Chrome real, pagina en LTR y direccion movida por el control de la
demo:

  tabs           auto -> ltr   prop rtl -> rtl   vuelta a auto -> ltr
  dropdown-menu  el TRIGGER pasa a rtl y el panel portalizado tambien
  number-field   raiz e input, los dos a rtl
  drawer         el content portalizado a rtl por la cadena de prefs
  accordion      sigue la cadena y vuelve, sin una sola linea en su proveedor

check: 77 con mis cambios vs 79 sin ellos (A/B con stash). El -2 esta explicado:
uno era un error preexistente de `date-range-picker-provider.svelte.test.ts` que
indexaba el tipo `{ readonly dir }` de unos props que ahora son mas anchos; el
otro es que `media-player` ya no puede compilar sin el campo (su unica linea
tocada, +1/-1, es mecanica).

Tests: 1271/1278 en `src/uix/soma` + contracts. Los 7 fallos son el conjunto
preexistente, A/B contra arbol limpio: los 6 de `contracts.test.ts` salen
identicos sin mis cambios y `soma-attr-audit` pasa aislado (flaky bajo carga).

⚠️ Un fallo mio que cazaron los tests y conviene no repetir: los proveedores con
la forma de una linea `soma.runtime(morfo, {})` recibieron `dir: null` en vez del
getter, y 12 componentes con direccion se quedaron sin estampar. Lo vieron cinco
tests de "exposes root props". Un barrido con dos formas sintacticas necesita
las dos en el mapa, no una.

Estampados a mano restantes en el catalogo: 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent 482cea3bee
commit 5983b00d5f

@ -196,23 +196,55 @@ Which is to say: in practice a component with any horizontal layout at all is
direction-dependent, and the honest reading of the rule is that accepting `dir`
implies stamping it.
**The open question is WHICH element, not whether.** It is the one the paint
sits on:
### The runtime stamps it, and the type makes you say so
- Ordinary components — the provider root.
The rule above used to be a convention: one hand-written line per provider, 49
copies of it, and 20 of 55 components that had simply forgotten. A convention
repeated 49 times that a third of the catalogue breaks is not a convention — it
is a missing abstraction.
So the stamp is the runtime's. A provider hands `soma.runtime(morfo, …)` its
direction and the runtime writes the attribute:
```ts
this.runtime = this.soma.runtime(xMorfo, {
dir: { get: () => this.opts.dir.current }, // RAW, never `resolvedDir`
// …
});
```
`SomaRuntimeSources.dir` is **required**, so a component that forgets does not
compile. `null` is the only way out, and it says something true: this runtime
stamps no direction. A provider must not write `dir` into its render props any
more — the runtime already put it there.
**The open question is still WHICH element, not whether**, and now it is
answered in the same place: `dir.parts` names the parts that carry the stamp,
defaulting to `['provider']`. It is the element the paint sits on:
- Ordinary components — the provider root, which is the default.
- A root that **renders no element** — the stamp goes to the part that IS the
in-place half (`dropdown-menu` → `parts: ['trigger']`).
- A field that paints on **both** its shell and its input —
`parts: ['provider', 'input']`.
- **Portalled surfaces** — the floating wrapper, which the floating layer
stamps from `assertedDir` (§1). It has to: portalled to `<body>`, it is
outside the component's subtree and cannot inherit from the trigger. A
component whose only direction-dependent paint is portalled is already
covered, and so is one that merely composes an overlay — provided it hands
that overlay its `dir`.
component whose only direction-dependent paint is portalled names that part
(`drawer` / `float-panel` → `parts: ['content']`); one that merely composes
an overlay is covered provided it hands that overlay its `dir`.
- A component with **both** halves — a trigger in place and a menu portalled —
needs both. The two agree today only because both descend from `<html dir>`;
assert by prop and they part company.
- **Nothing painted from CSS at all** — geometry computed and applied by JS —
needs no attribute, and adding one only enlarges the DOM.
needs no attribute, and adding one only enlarges the DOM: `dir: null`.
**Ask what the paint reads and where it lives**, then name that part.
**Ask what the paint reads and where it lives**, then stamp there.
> ⚠️ One consequence worth knowing: the stamp ships through the runtime's part
> props, so it is present at runtime but **absent from the static type** of a
> provider's `props`. A test that asserts on it reads through
> `(props as Record<string, unknown>).dir`.
### The exception: `:dir()` translating an already-logical prop
@ -388,10 +420,12 @@ When you touch a component's direction behaviour:
1. Is `dir?: Direction` declared, using the alias?
2. Does the **wrapper** run `activeDir(() => dir, soma)`? (A portalled
`Content` is the exception — it passes the prop raw; §1.)
3. Does the provider default once, in `resolvedDir`, and never elsewhere?
And does every canon component it MOUNTS receive the raw assertion — the
composed overlay's provider, and every in-place child? Otherwise the prop
moves one half and leaves the other behind.
3. Does the provider hand the runtime `dir: { get: () => this.opts.dir.current }`
— raw, with `parts` when the paint is not on the provider root? (It will not
compile otherwise.) Does it default once, in `resolvedDir`, and never
elsewhere? And does every canon component it MOUNTS receive the raw
assertion — the composed overlay's provider, and every in-place child?
Otherwise the prop moves one half and leaves the other behind.
4. Does the recipe branch with `:dir()`? Then is the raw `dir` stamped?
5. Does any block pair a logical anchor with a physical displacement?
(`npm run rtl:check`)

@ -527,7 +527,7 @@ describe('UIX layer contracts', () => {
it('SomaRuntime has one public part API and only depends on injected services', async () => {
const dom = createActiveDom();
try {
const runtime = createSomaRuntime(toggleMorfo, { dom });
const runtime = createSomaRuntime(toggleMorfo, { dom, dir: null });
for (const key of UIX_LAYER_CONTRACTS.somaRuntime.publicSurface) {
expect(key in runtime, key).toBe(true);
}

@ -111,6 +111,7 @@
// ── Soma behaviour bridge ────────────────────────────────────────────────
const runtime = createMenuDialRuntime({
dir: null,
dom: eidos.dom,
eventEngine: eidos.uix?.events,
announce: eidos.uix ? (m, p) => eidos.uix!.announce(m, p) : undefined,

@ -38,6 +38,7 @@
// change. The runtime resolves the signal's intent from the `intent` source.
let currentIntent = $state<MetricsIntent>('neutral');
const runtime = createMetricsRuntime({
dir: null,
dom: eidos.dom,
eventEngine: eidos.uix?.events,
announce: eidos.uix ? (m, p) => eidos.uix!.announce(m, p) : undefined,

@ -69,6 +69,7 @@
// (morfo's `texts.label`). No hardcoded English.
const surfaceLabel = $derived(eidos.langs.ts('#?components.onion-menu.label|Onion menu'));
const runtime = createOnionMenuRuntime({
dir: null,
dom: eidos.dom,
eventEngine: eidos.uix?.events,
announce: eidos.uix ? (m, p) => eidos.uix!.announce(m, p) : undefined,

@ -62,6 +62,7 @@ export class AccordionProvider {
private constructor(opts: AccordionOpts) {
this.opts = opts;
this.runtime = this.soma.runtime(accordionMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
orientation: () => this.opts.orientation.current,
disabled: () => this.opts.disabled.current
@ -141,8 +142,7 @@ export class AccordionProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current
...this.runtimePart.props
} as const)
);
}
@ -187,6 +187,7 @@ export class AccordionItemProvider {
this.opts = opts;
this.provider = AccordionProvider.require();
this.runtime = this.provider.soma.runtime(accordionMorfo, {
dir: null,
states: {
open: () => this.isOpen
},

@ -41,6 +41,7 @@ export class AlertDialogActionProvider {
this.dialog = DialogProvider.require();
this.soma = Soma.require();
this.runtime = this.soma.runtime(alertDialogMorfo, {
dir: null,
props: {
ariaLabel: () => this.resolvedAriaLabel
}
@ -100,6 +101,7 @@ export class AlertDialogCancelProvider {
this.dialog = DialogProvider.require();
this.soma = Soma.require();
this.runtime = this.soma.runtime(alertDialogMorfo, {
dir: null,
props: {
ariaLabel: () => this.resolvedAriaLabel
}

@ -81,6 +81,7 @@ export class AnchorNavProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(anchorNavMorfo, {
dir: null,
props: {
ariaLabel: () => this.resolvedAriaLabel.current
}
@ -215,7 +216,7 @@ export class AnchorNavLinkProvider {
// A30: register with the parent from the constructor, not a reactive effect.
this.parent?.registerLink(this.targetId);
this.runtime = this.parent?.runtime ?? this.soma.runtime(anchorNavMorfo, {});
this.runtime = this.parent?.runtime ?? this.soma.runtime(anchorNavMorfo, { dir: null });
this.linkPart = this.runtime.part('link', {
id: opts.id,
ref: opts.ref,

@ -64,7 +64,7 @@ export class AnnounceProvider {
private constructor(opts: AnnounceOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(announceMorfo, {});
this.runtime = this.soma.runtime(announceMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -220,7 +220,7 @@ export class AnnounceRegionProvider {
this.opts = opts;
this.soma = Soma.require();
this.provider = AnnounceProvider.get();
this.runtime = this.provider?.runtime ?? this.soma.runtime(announceMorfo, {});
this.runtime = this.provider?.runtime ?? this.soma.runtime(announceMorfo, { dir: null });
this.runtimePart = this.runtime.part('region', {
id: opts.id,
ref: opts.ref,

@ -81,6 +81,7 @@ export class AuraProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(auraMorfo, {
dir: null,
states: {
idle: () => this.state
}

@ -39,6 +39,7 @@ export class BreadcrumbProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(breadcrumbMorfo, {
dir: null,
props: {
ariaLabel: () => this.resolvedAriaLabel.current,
ariaLabelledby: () => this.opts.ariaLabelledby.current

@ -52,6 +52,7 @@ export class ButtonProvider {
this.soma = Soma.require();
this.runtime = this.soma.runtime(buttonMorfo, {
dir: null,
states: {
loading: () => opts.loading.current ?? false
},

@ -157,7 +157,7 @@ export class CalendarProvider implements CalendarNav {
private constructor(opts: CalendarOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(calendarMorfo, {});
this.runtime = this.soma.runtime(calendarMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -566,7 +566,6 @@ export class CalendarProvider implements CalendarNav {
this.runtimePart.assert({
// role + aria-disabled are morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
dir: this.opts.dir.current,
// Computed accessible name (calendarLabel + heading) — overrides the
// morfo's raw `ariaLabel` propRef (which the provider doesn't source).
'aria-label': this.fullCalendarLabel,

@ -102,7 +102,7 @@ export class CarouselProvider {
private constructor(opts: CarouselOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(carouselMorfo, {});
this.runtime = this.soma.runtime(carouselMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -462,7 +462,6 @@ export class CarouselProvider {
// computed value wins.
'aria-label': this.resolvedAriaLabel.current,
tabindex: this.isDisabled ? -1 : 0,
dir: this.opts.dir.current,
lang: this.opts.locale.current || undefined,
// data-* below are contract-only in the morfo (declared with `values`,
// no resolvable `value`), so renderProps doesn't emit them — soma owns them.

@ -53,6 +53,7 @@ export class ChatComposerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(chatComposerMorfo, {
dir: null,
props: {
contextKind: () => this.opts.context.current,
disabled: () => this.opts.disabled.current,

@ -53,7 +53,7 @@ export class ChatListProvider {
private constructor(opts: ChatListOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(chatListMorfo, {});
this.runtime = this.soma.runtime(chatListMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -131,6 +131,7 @@ export class ChatListItemProvider {
// Per-instance runtime — `unread` differs per row (AccordionItem
// pattern; a shared prop source cannot carry per-instance state).
this.runtime = this.provider.soma.runtime(chatListMorfo, {
dir: null,
props: {
unread: () => this.opts.unread.current
}

@ -68,6 +68,7 @@ export class ChatLogProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(chatLogMorfo, {
dir: null,
props: {
atLatest: () => this.atLatest,
hasNew: () => this.unseenCount > 0

@ -50,6 +50,7 @@ export class ChatMessageProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(chatMessageMorfo, {
dir: null,
props: {
delivery: () => this.opts.delivery.current,
ariaLabel: () => this.opts.ariaLabel.current,
@ -389,6 +390,7 @@ export class ChatMessageReactionProvider {
this.opts = opts;
this.provider = ChatMessageProvider.require();
this.runtime = this.provider.soma.runtime(chatMessageMorfo, {
dir: null,
states: {
pressed: () => this.opts.pressed.current
}

@ -39,6 +39,7 @@ export class ChatTypingProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(chatTypingMorfo, {
dir: null,
props: {
state: () => this.state,
// aria attrs are string-valued; absent unless the app opts in.

@ -55,6 +55,7 @@ export class CheckboxProvider {
this.soma = Soma.require();
this.runtime = this.soma.runtime(checkboxMorfo, {
dir: null,
states: {
checked: () => this.dataState
},
@ -293,6 +294,7 @@ export class CheckboxGroupProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(checkboxMorfo, {
dir: null,
props: {
disabled: () => this.opts.disabled.current,
orientation: () => this.opts.orientation.current

@ -150,7 +150,7 @@ export class ChronosProvider {
// Roving focus starts on the anchor day (opts.date ?? today).
this.focusedDate = this.calendar.anchor;
this.runtime = this.soma.runtime(chronosMorfo, {});
this.runtime = this.soma.runtime(chronosMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,

@ -43,6 +43,7 @@ export class ClipboardProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(clipboardMorfo, {
dir: null,
props: {
copied: () => this.copied
}

@ -50,6 +50,7 @@ export class CollapsibleProvider {
this.soma = Soma.require();
this.runtime = this.soma.runtime(collapsibleMorfo, {
dir: null,
states: {
open: () => opts.open.current
},

@ -153,7 +153,7 @@ export class ColorFieldProvider {
private constructor(opts: ColorFieldOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(colorFieldMorfo, {});
this.runtime = this.soma.runtime(colorFieldMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -433,7 +433,6 @@ export class ColorFieldProvider {
// Morfo supplies `aria-invalid` (literal 'true' when `invalid` truthy),
// resolved against the per-part `invalid` source registered above.
...this.runtimePart.renderProps(),
dir: this.opts.dir.current,
// Soma-owned data-* — the morfo declares these without a value source.
'data-format': this.opts.format.current,
'data-invalid': boolToEmptyStrOrUndef(this.isInvalid),

@ -194,6 +194,7 @@ export class ColorPickerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(colorPickerMorfo, {
dir: { get: () => this.opts.dir.current },
states: { open: () => this.opts.open.current },
props: {
disabled: () => this.opts.disabled.current,
@ -469,7 +470,6 @@ export class ColorPickerProvider {
// Morfo-declared `data-state` (← stateRef `open`) is resolved by
// renderProps against the component-level `open` state source.
...this.runtimePart.renderProps(),
dir: this.opts.dir.current,
'aria-invalid': this.isInvalid || undefined,
// The morfo declares these data-* WITHOUT a value (presence /
// soma-owned), so soma still supplies them.

@ -117,6 +117,7 @@ export class ComboboxProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(comboboxMorfo, {
dir: { get: () => this.opts.dir.current },
states: {
open: () => this.opts.open.current
},
@ -350,7 +351,6 @@ export class ComboboxProvider {
// from it: the control's flex row (input / trigger / clear-button order) and
// the chip strip's asymmetric `padding-inline` + `margin-inline-start`. The
// portalled half is the floating wrapper's, which stamps its own.
dir: this.opts.dir.current
} as const)
);
}

@ -139,7 +139,7 @@ export class CommandProvider {
private constructor(opts: CommandOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(commandMorfo, {});
this.runtime = this.soma.runtime(commandMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,

@ -102,7 +102,7 @@ export class ContextMenuProvider {
meta: { component: 'context-menu', action: 'typeahead-reset' },
getText: (el) => el.getAttribute('data-text-value') || el.textContent || ''
});
this.runtime = this.soma.runtime(contextMenuMorfo, {});
this.runtime = this.soma.runtime(contextMenuMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,

@ -159,6 +159,7 @@ export class CropperProvider {
maxScale: opts.maxScale.current
});
this.runtime = this.soma.runtime(cropperMorfo, {
dir: null,
props: {
disabled: () => this.opts.disabled.current,
ariaLabel: () => this.resolvedAriaLabel

@ -178,6 +178,7 @@ export class CssFieldProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(cssFieldMorfo, {
dir: { get: () => this.opts.dir.current, parts: ['provider', 'input'] },
props: {
disabled: () => this.isDisabled,
readonly: () => this.isReadonly,
@ -500,7 +501,6 @@ export class CssFieldProvider {
// The attribute carries the RAW assertion, never the resolved value:
// omitted when nobody asserted a direction, so the element inherits
// instead of forcing `ltr`. `this.dir` stays for the maths.
dir: this.opts.dir.current
} as const)
);
}
@ -553,7 +553,6 @@ export class CssFieldInputProvider {
autocorrect: 'off' as const,
spellcheck: false,
// Raw assertion again — with none, the input inherits from the root.
dir: this.provider.opts.dir.current,
value: this.provider.inputValue,
'aria-valuenow': p && p.kind === 'dimension' ? p.value : undefined,
'aria-valuemin': this.provider.opts.min.current,

@ -269,6 +269,7 @@ export class DateFieldProvider {
private constructor(opts: DateFieldOpts) {
this.opts = opts;
this.runtime = this.soma.runtime(dateFieldMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
disabled: () => this.opts.disabled.current,
readonly: () => this.opts.readonly.current,
@ -897,8 +898,7 @@ export class DateFieldProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current
...this.runtimePart.props
} as const)
);
}

@ -155,6 +155,7 @@ export class DatePickerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(datePickerMorfo, {
dir: { get: () => this.opts.dir.current },
states: { open: () => this.opts.open.current },
props: {
disabled: () => this.opts.disabled.current,
@ -281,7 +282,6 @@ export class DatePickerProvider {
// it: the root IS the field, and its segment row (`[data-field-control]`,
// `flex-direction: row`) reverses in RTL. The portalled calendar reads the
// floating wrapper's own stamp.
dir: this.opts.dir.current
} as const)
);
}

@ -106,6 +106,7 @@ export class DateRangeFieldProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(dateRangeFieldMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
disabled: () => this.isDisabled,
readonly: () => this.isReadonly,
@ -176,7 +177,6 @@ export class DateRangeFieldProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current,
'aria-invalid': this.isInvalid || undefined
} as const)
);

@ -142,6 +142,7 @@ export class DateRangePickerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(dateRangePickerMorfo, {
dir: { get: () => this.opts.dir.current },
states: { open: () => this.opts.open.current },
props: {
disabled: () => this.opts.disabled.current,
@ -289,7 +290,6 @@ export class DateRangePickerProvider {
// endpoint + trigger order reverses in RTL (each endpoint's own composed
// DateField stamps its segments). The portalled calendar reads the floating
// wrapper's own stamp.
dir: this.opts.dir.current
} as const)
);
}

@ -174,6 +174,7 @@ export class DialogProvider {
const onClose = () => this.handleClose();
this.runtime = this.soma.runtime(dialogMorfo, {
dir: null,
states: { open: () => opts.open.current },
props: {
disabled: () => opts.disabled.current,

@ -60,7 +60,7 @@ export class DragDropProvider {
private constructor(opts: DragDropOpts) {
this.opts = opts;
this.globalAnnounce = createAnnouncer(this.soma.dom, 1500, this.soma.uix.timers);
this.runtime = this.soma.runtime(dragDropMorfo, {});
this.runtime = this.soma.runtime(dragDropMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,

@ -246,6 +246,7 @@ export class DrawerProvider {
const noop = () => {};
this.runtime = this.soma.runtime(drawerMorfo, {
dir: { get: () => this.opts.dir.current, parts: ['content'] },
// 'persistent' forces open=true so eidos can render the layout
// drawer without the consumer needing to bind `open`. The morfo
// state is read once per tick so the override is reactive.
@ -1050,7 +1051,6 @@ export class DrawerContentProvider {
// `[data-drawer-header]`'s flex row and `[data-drawer-footer]`'s
// `justify-content: end`. Raw, not `resolvedSide`'s `resolvedDir`: no
// assertion means no attribute (direction contract).
dir: this.provider.opts.dir.current,
role: 'dialog' as const,
'aria-modal': this.provider.isOverlay ? true : undefined,
'aria-describedby': this.provider.descriptionId.current || undefined,

@ -107,7 +107,7 @@ export class MenuProvider {
meta: { component: 'dropdown-menu', action: 'typeahead-reset' },
getText: (el) => el.getAttribute('data-text-value') || el.textContent || ''
});
this.runtime = this.soma.runtime(dropdownMenuMorfo, {});
this.runtime = this.soma.runtime(dropdownMenuMorfo, { dir: { get: () => this.opts.dir.current, parts: ['trigger'] } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -264,7 +264,6 @@ export class MenuTriggerProvider {
// carries the stamp. Raw, so no assertion leaves it inheriting. It paints
// from direction through Button's `display: inline-flex` (icon / endIcon
// order). The portalled panel is the floating wrapper's, which stamps its own.
dir: this.provider.opts.dir.current,
onclick: this.onclick,
onkeydown: this.onkeydown
} as const)

@ -64,6 +64,7 @@ export class EditableProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(editableMorfo, {
dir: null,
props: {
editing: () => this.isEditing,
empty: () => this.isEmpty,

@ -77,6 +77,7 @@ export class EmojiPickerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(emojiPickerMorfo, {
dir: { get: () => this.opts.dir.current },
states: { open: () => this.opts.open.current },
props: { disabled: () => this.opts.disabled.current }
});
@ -183,7 +184,6 @@ export class EmojiPickerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
dir: this.opts.dir.current,
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current)
} as const)
);
@ -317,6 +317,7 @@ export class EmojiPickerOptionProvider {
this.provider = EmojiPickerProvider.require();
// Per-instance runtime — `data-tone-capable` differs per cell.
this.runtime = this.provider.soma.runtime(emojiPickerMorfo, {
dir: null,
props: { toneCapable: () => this.opts.toneCapable.current }
});
this.runtimePart = this.runtime.part('option', {

@ -46,6 +46,7 @@ export class FeedProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(feedMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
busy: () => this.opts.busy.current,
ariaLabel: () => this.resolvedAriaLabel.current,
@ -152,7 +153,6 @@ export class FeedProvider {
// The recipe selects with `:dir(rtl)`, which reads the RESOLVED direction,
// so an asserted `dir` must reach the DOM or the paint would lag the prop.
// Raw, not `resolvedDir`: no assertion means no attribute (direction contract).
dir: this.opts.dir.current,
...this.runtimePart.props
} as const)
);
@ -427,7 +427,7 @@ export class FeedSentinelProvider {
this.opts = opts;
this.soma = Soma.require();
this.feed = FeedProvider.get();
this.runtime = this.feed?.runtime ?? this.soma.runtime(feedMorfo, {});
this.runtime = this.feed?.runtime ?? this.soma.runtime(feedMorfo, { dir: null });
this.runtimePart = this.runtime.part('sentinel', {
id: opts.id,
ref: opts.ref,

@ -61,6 +61,7 @@ export class FieldLangsProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(fieldLangsMorfo, {
dir: null,
props: {
activeLang: () => this.activeLang,
switcherSide: () => this.switcherSide,

@ -55,6 +55,7 @@ export class FieldProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(fieldMorfo, {
dir: null,
states: {
state: () => this.effectiveState
},

@ -79,7 +79,7 @@ export class FileUploadProvider {
private constructor(opts: FileUploadOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(fileUploadMorfo, {});
this.runtime = this.soma.runtime(fileUploadMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,

@ -174,6 +174,7 @@ export class FloatPanelProvider {
});
this.runtime = this.soma.runtime(floatPanelMorfo, {
dir: { get: () => this.opts.dir.current, parts: ['content'] },
states: { open: () => opts.open.current },
props: {
modal: () => opts.modal.current,
@ -1167,7 +1168,6 @@ export class FloatPanelContentProvider {
// rule selects against, and `:dir()` reads the EFFECTIVE direction: a
// consumer asserting `dir` by prop would otherwise flip the resize
// maths and leave the cursor and chevrons pointing the old way.
dir: this.provider.opts.dir.current,
// NOTE: geometry + stacking (`position`, `translate`, `width`, `height`,
// `z-index`, `will-change`) are deliberately NOT in this reactive style.
// Soma serializes `style` to a full string and reassigns the element's

@ -46,6 +46,7 @@ export class FormProvider<TForm extends Form = Form> {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(formMorfo, {
dir: null,
props: {
isPending: () => this.isPending,
dirty: () => this.isDirty,

@ -88,6 +88,7 @@ export class GradientBuilderProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(gradientBuilderMorfo, {
dir: null,
props: {
disabled: () => this.isDisabled
},

@ -144,6 +144,7 @@ export class GradientPickerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(gradientPickerMorfo, {
dir: null,
states: { open: () => this.opts.open.current },
props: { disabled: () => this.isDisabled }
});

@ -69,7 +69,7 @@ export class GridListProvider {
timeout: () => this.opts.typeaheadTimeout.current,
meta: { component: 'grid-list', action: 'typeahead-reset' }
});
this.runtime = this.soma.runtime(gridListMorfo, {});
this.runtime = this.soma.runtime(gridListMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -456,7 +456,6 @@ export class GridListProvider {
...this.runtimePart.renderProps(),
// Raw, never `resolvedDir`: the rows are flex ROWS and the trailing cell is
// pushed out with `margin-inline-start: auto` — both read this root's direction.
dir: this.opts.dir.current,
// Soma-owned: aria-label honours Field/labelledby (overrides the
// morfo's translationRef default); aria-labelledby + the data-* flags
// are not value-backed in the morfo, so soma supplies them.

@ -161,6 +161,7 @@ export class ImageAdjustmentsProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(imageAdjustmentsMorfo, {
dir: null,
props: {
disabled: () => this.opts.disabled.current,
ariaLabel: () => this.resolvedAriaLabel

@ -58,6 +58,7 @@ export class ImagePickerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(imagePickerMorfo, {
dir: null,
props: {
state: () => this.state,
disabled: () => this.opts.disabled.current,

@ -75,6 +75,7 @@ export class KnobProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(knobMorfo, {
dir: null,
states: {},
props: {
value: () => this.opts.value.current,

@ -81,7 +81,7 @@ export class LinkPreviewProvider {
private constructor(opts: LinkPreviewOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(linkPreviewMorfo, {});
this.runtime = this.soma.runtime(linkPreviewMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,

@ -138,9 +138,9 @@ describe('ListboxProvider', () => {
// Nobody asserted a direction, so the attribute is OMITTED — the element
// inherits instead of forcing `ltr`. The chain lives in the wrapper.
expect(provider.props.dir).toBeUndefined();
expect((provider.props as Record<string, unknown>).dir).toBeUndefined();
opts.dir.current = 'rtl';
expect(provider.props.dir).toBe('rtl');
expect((provider.props as Record<string, unknown>).dir).toBe('rtl');
opts.dir.current = undefined;
expect(provider.props).toMatchObject({

@ -76,7 +76,7 @@ export class ListboxProvider {
timeout: () => this.opts.typeaheadTimeout.current,
meta: { component: 'listbox', action: 'typeahead-reset' }
});
this.runtime = this.soma.runtime(listboxMorfo, {});
this.runtime = this.soma.runtime(listboxMorfo, { dir: { get: () => this.assertedDir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -367,7 +367,6 @@ export class ListboxProvider {
// readonly/required/invalid, default aria-label) resolved against this
// part's registered sources — no longer re-declared here.
...this.runtimePart.renderProps(),
dir: this.assertedDir.current,
// Soma override: honours explicit ariaLabel prop + suppresses when
// aria-labelledby is set (overrides the morfo's default translation;
// undefined correctly removes it). aria-labelledby is not morfo-modeled.

@ -86,6 +86,7 @@ export class MaskFieldProvider {
this.committedValue = opts.value.current;
this.soma = Soma.require();
this.runtime = this.soma.runtime(maskFieldMorfo, {
dir: null,
props: {
disabled: () => this.isDisabled,
readonly: () => this.isReadonly,

@ -200,6 +200,7 @@ export class MediaPlayerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(mediaPlayerMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
media: () => this.opts.media.current,
controls: () => (this.controlsVisible ? 'visible' : 'hidden'),
@ -784,7 +785,6 @@ export class MediaPlayerProvider {
// Raw, so the attribute is omitted when nobody asserted a direction and
// the chrome inherits. Without it the `dir` PROP reached only the composed
// Sliders and the player laid its own controls out the other way round.
dir: this.opts.dir.current,
// data-dir is contract-only in the morfo (declared with `values`, no
// resolvable `value`), so renderProps doesn't emit it — soma owns it.
'data-dir': this.resolvedDir,

@ -82,7 +82,7 @@ export class MenubarProvider {
private constructor(opts: MenubarOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(menubarMorfo, {});
this.runtime = this.soma.runtime(menubarMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -218,7 +218,6 @@ export class MenubarProvider {
// role + aria-orientation are morfo-declared (resolved by renderProps).
...this.runtimePart.renderProps(),
// Soma-only: writing direction the morfo does not model.
dir: this.opts.dir.current
} as const)
);
}

@ -52,6 +52,7 @@ export class MeterProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(meterMorfo, {
dir: null,
states: {
optimum: () => this.state
},

@ -19,6 +19,7 @@ describe('createMetricsRuntime — the live-update signal bridge', () => {
const dom = createActiveDom();
const emit = vi.fn(async () => 'sig-test');
const runtime = createMetricsRuntime({
dir: null,
dom,
eventEngine: { emit },
translate: (key: string) => key,

@ -99,7 +99,7 @@ export class MonthGridProvider {
private constructor(opts: MonthGridOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(monthGridMorfo, {});
this.runtime = this.soma.runtime(monthGridMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -288,7 +288,6 @@ export class MonthGridProvider {
this.runtimePart.assert({
// role + aria-disabled are morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
dir: this.opts.dir.current,
// Computed accessible name (ariaLabel + heading year) — overrides the
// morfo's raw `ariaLabel` propRef (which the provider doesn't source).
'aria-label': this.fullLabel,

@ -93,6 +93,7 @@ export class NaturalTimePickerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(naturalTimePickerMorfo, {
dir: { get: () => this.opts.dir.current },
states: { open: () => this.opts.open.current },
props: {
disabled: () => this.opts.disabled.current,
@ -248,7 +249,6 @@ export class NaturalTimePickerProvider {
// and its icon's `margin-inline-start: auto`, plus the inline Panel's own
// auto inline-start margins. Raw, per the note on `resolvedDir` above —
// no assertion means no attribute, so the subtree inherits.
dir: this.opts.dir.current
} as const)
);
}

@ -88,6 +88,7 @@ export class NavTreeProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(navTreeMorfo, {
dir: { get: () => this.opts.dir.current },
props: { ariaLabel: () => this.resolvedAriaLabel.current }
});
this.providerPart = this.runtime.part('provider', {
@ -217,7 +218,6 @@ export class NavTreeProvider {
// The recipe selects with `:dir(rtl)`, which reads the RESOLVED direction,
// so an asserted `dir` must reach the DOM or the paint would lag the prop.
// Raw, not `resolvedDir`: no assertion means no attribute (direction contract).
dir: this.opts.dir.current
} as const)
);
readonly listProps = $derived.by(() => this.listPart.renderProps());
@ -302,6 +302,7 @@ export class NavTreeItemProvider {
this.soma = Soma.require();
this.parent = NavTreeProvider.require();
this.runtime = this.soma.runtime(navTreeMorfo, {
dir: null,
states: { open: () => this.expanded },
props: {
active: () => this.active,

@ -292,7 +292,7 @@ describe('NavigationMenuProvider', () => {
// Nobody asserted a direction, so the attribute is OMITTED — the element
// inherits instead of forcing `ltr`. The chain lives in the wrapper.
expect(result.provider.props.dir).toBeUndefined();
expect((result.provider.props as Record<string, unknown>).dir).toBeUndefined();
expect(result.provider.props).toMatchObject({
'data-orientation': 'horizontal'
});

@ -84,7 +84,7 @@ export class NavigationMenuProvider {
private constructor(opts: NavigationMenuOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(navigationMenuMorfo, {});
this.runtime = this.soma.runtime(navigationMenuMorfo, { dir: { get: () => this.assertedDir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -381,7 +381,6 @@ export class NavigationMenuProvider {
// dir is soma-resolved (prop → prefs → 'ltr'); the morfo doesn't model
// it. data-orientation is a morfo enum without a value source, so soma
// supplies the value.
dir: this.assertedDir.current,
'data-orientation': this.opts.orientation.current
} as const)
);

@ -201,6 +201,7 @@ export class NumberFieldProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(numberFieldMorfo, {
dir: { get: () => this.opts.dir.current, parts: ['provider', 'input'] },
props: {
disabled: () => this.isDisabled,
readonly: () => this.isReadonly,
@ -556,7 +557,6 @@ export class NumberFieldProvider {
// The attribute carries the RAW assertion, never the resolved value:
// omitted when nobody asserted a direction, so the element inherits
// instead of forcing `ltr`. `this.dir` stays for the maths.
dir: this.opts.dir.current
} as const)
);
}
@ -621,7 +621,6 @@ export class NumberFieldInputProvider {
autocorrect: 'off' as const,
spellcheck: false,
// Raw assertion again — with none, the input inherits from the root.
dir: this.provider.opts.dir.current,
value: this.provider.inputValue,
// Morfo declares the raw value for aria-valuetext; soma supplies the
// locale-formatted text.

@ -97,6 +97,7 @@ export class PaginationProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(paginationMorfo, {
dir: null,
props: {
disabled: () => this.opts.disabled.current
},

@ -391,6 +391,7 @@ export class PalabrasProvider {
this.opts.selection.current = initialState.selection;
this.runtime = this.soma.runtime(palabrasMorfo, {
dir: null,
props: {
focused: () => this.focused,
empty: () => this.isEmpty,

@ -119,6 +119,7 @@ export class PasswordFieldProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(passwordFieldMorfo, {
dir: null,
props: {
disabled: () => this.isDisabled,
readonly: () => this.isReadonly,

@ -39,7 +39,7 @@ function installSomaHarness() {
langs: { ts: vi.fn((key: string) => key), t: vi.fn((key: string) => key) },
uix: { timers },
runtime: (morfo: Morfo, sources?: Omit<SomaRuntimeSources, 'dom' | 'eventEngine'>) =>
createSomaRuntime(morfo, { dom, translate: (key) => key, ...(sources ?? {}) })
createSomaRuntime(morfo, { dom, dir: null, translate: (key) => key, ...(sources ?? {}) })
} as unknown as Soma;
vi.spyOn(Soma, 'require').mockReturnValue(soma);
vi.spyOn(PathTraceProvider.ctx, 'set').mockImplementation((value) => value);

@ -129,7 +129,7 @@ export class PathTraceProvider {
private constructor(opts: PathTraceOpts) {
this.opts = opts;
this.announcer = createAnnouncer(this.soma.dom, 1500, this.soma.uix.timers);
this.runtime = this.soma.runtime(pathTraceMorfo, {});
this.runtime = this.soma.runtime(pathTraceMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,

@ -92,6 +92,7 @@ export class PickerProvider<TValue> {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(pickerMorfo, {
dir: null,
states: { open: () => this.opts.open.current },
props: {
disabled: () => this.opts.disabled.current,

@ -107,7 +107,7 @@ export class PinInputProvider {
private constructor(opts: PinInputOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(pinInputMorfo, {});
this.runtime = this.soma.runtime(pinInputMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,

@ -163,6 +163,7 @@ export class PopoverProvider {
const onClose = () => this.handleClose();
this.runtime = this.soma.runtime(popoverMorfo, {
dir: null,
states: { open: () => opts.open.current },
props: {
modal: () => opts.modal.current,

@ -53,6 +53,7 @@ export class ProgressProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(progressMorfo, {
dir: null,
states: {
loading: () => this.state
},

@ -62,6 +62,7 @@ export class ProofOfHumanProvider implements ProofOfHumanChallengeHost {
this.opts = opts;
this.announcer = createAnnouncer(this.soma.dom, 1500, this.soma.uix.timers);
this.runtime = this.soma.runtime(proofOfHumanMorfo, {
dir: null,
props: { ariaLabel: () => this.opts.ariaLabel.current }
});
this.runtimePart = this.runtime.part('provider', {

@ -72,6 +72,7 @@ export class RadioGroupProvider {
// would introduce a 180ms lag in held-key auto-select. The
// `data-event-*` hold still occurs in parallel so eidos reacts.
this.runtime = this.soma.runtime(radioGroupMorfo, {
dir: { get: () => this.opts.dir.current },
states: {
value: () => this.opts.value.current
},
@ -174,8 +175,7 @@ export class RadioGroupProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current
...this.runtimePart.props
} as const)
);
}

@ -148,7 +148,7 @@ export class RangeCalendarProvider implements CalendarNav {
private constructor(opts: RangeCalendarOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(rangeCalendarMorfo, {});
this.runtime = this.soma.runtime(rangeCalendarMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -649,7 +649,6 @@ export class RangeCalendarProvider implements CalendarNav {
this.runtimePart.assert({
// role + aria-disabled come from the morfo via renderProps.
...this.runtimePart.renderProps(),
dir: this.opts.dir.current,
// Computed label (base + heading) — overrides the morfo's raw
// propRef('ariaLabel') default.
'aria-label': this.fullCalendarLabel,

@ -89,6 +89,7 @@ export class RatingGroupProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(ratingGroupMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
value: () => this.opts.value.current,
min: () => this.opts.min.current,
@ -318,7 +319,6 @@ export class RatingGroupProvider {
...this.runtimePart.props,
// Raw, never `resolvedDir`: the recipe lays the items out as a flex ROW, which
// reverses against this root's direction — the same axis `calcFromPointer` flips.
dir: this.opts.dir.current,
tabindex: this.isDisabled ? -1 : 0,
onkeydown: this.onkeydown,
onpointerleave: this.onpointerleave,

@ -41,7 +41,7 @@ function installSomaHarness() {
langs: { ts: vi.fn((key: string) => key), t: vi.fn((key: string) => key) },
uix: { timers },
runtime: (morfo: Morfo, sources?: Omit<SomaRuntimeSources, 'dom' | 'eventEngine'>) =>
createSomaRuntime(morfo, { dom, translate: (key) => key, ...(sources ?? {}) })
createSomaRuntime(morfo, { dom, dir: null, translate: (key) => key, ...(sources ?? {}) })
} as unknown as Soma;
vi.spyOn(Soma, 'require').mockReturnValue(soma);
vi.spyOn(RotateAlignProvider.ctx, 'set').mockImplementation((value) => value);

@ -143,7 +143,7 @@ export class RotateAlignProvider {
private constructor(opts: RotateAlignOpts) {
this.opts = opts;
this.announcer = createAnnouncer(this.soma.dom, 1500, this.soma.uix.timers);
this.runtime = this.soma.runtime(rotateAlignMorfo, {});
this.runtime = this.soma.runtime(rotateAlignMorfo, { dir: null });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,

@ -61,7 +61,7 @@ export class ScrollAreaProvider {
private constructor(opts: ScrollAreaOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(scrollAreaMorfo, {});
this.runtime = this.soma.runtime(scrollAreaMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -178,7 +178,6 @@ export class ScrollAreaProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current,
style: {
position: 'relative',
'--scroll-area-corner-width':

@ -76,6 +76,7 @@ export class SearchFieldProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(searchFieldMorfo, {
dir: null,
props: {
disabled: () => this.isDisabled,
readonly: () => this.isReadonly,

@ -127,6 +127,7 @@ export class SelectProvider {
getText: (el) => el.getAttribute('data-label') || el.textContent || ''
});
this.runtime = this.soma.runtime(selectMorfo, {
dir: { get: () => this.opts.dir.current },
states: {
open: () => this.opts.open.current
},
@ -280,7 +281,6 @@ export class SelectProvider {
// from it: the trigger's flex row (the indicator swaps sides) and
// `[data-select-value] { text-align: start }`. The portalled half is the
// floating wrapper's, which stamps its own.
dir: this.opts.dir.current
} as const)
);
}

@ -97,6 +97,7 @@ export class SidebarProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(sidebarMorfo, {
dir: { get: () => this.opts.dir.current },
states: { expanded: () => this.open },
props: {
collapsible: () => this.collapsible,
@ -216,7 +217,6 @@ export class SidebarProvider {
// The recipe selects with `:dir(rtl)`, which reads the RESOLVED direction,
// so an asserted `dir` must reach the DOM or the paint would lag the prop.
// Raw, not `resolvedDir`: no assertion means no attribute (direction contract).
dir: this.opts.dir.current
} as const)
);
}

@ -118,6 +118,7 @@ export class SliderProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(sliderMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
orientation: () => this.opts.orientation.current,
disabled: () => this.isDisabled,
@ -395,7 +396,6 @@ export class SliderProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current,
style: {
position: 'relative',
'touch-action': this.opts.orientation.current === 'horizontal' ? 'pan-y' : 'pan-x'

@ -98,6 +98,7 @@ export class SplitterProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(splitterMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
orientation: () => this.opts.orientation.current,
disabled: () => this.opts.disabled.current,
@ -313,7 +314,6 @@ export class SplitterProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current,
style: {
display: 'flex',
'flex-direction': this.opts.orientation.current === 'vertical' ? 'column' : 'row',

@ -83,6 +83,7 @@ export class StepperProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(stepperMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
orientation: () => this.opts.orientation.current,
linear: () => this.opts.linear.current
@ -143,8 +144,7 @@ export class StepperProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current
...this.runtimePart.props
} as const)
);
}

@ -75,6 +75,7 @@ export class StickyProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(stickyMorfo, {
dir: null,
props: {
edge: () => this.opts.edge.current,
stuck: () => this.stuck

@ -72,6 +72,7 @@ export class SwitchProvider {
this.soma = Soma.require();
this.runtime = this.soma.runtime(switchMorfo, {
dir: { get: () => this.opts.dir.current },
states: {
checked: () => opts.checked.current
},
@ -170,7 +171,6 @@ export class SwitchProvider {
// The recipe selects with `:dir(rtl)`, which reads the RESOLVED direction,
// so an asserted `dir` must reach the DOM or the paint would lag the prop.
// Raw, not `resolvedDir`: no assertion means no attribute (direction contract).
dir: this.opts.dir.current,
disabled: this.isDisabled || undefined,
onclick: this.onclick,
onkeydown: this.onkeydown

@ -78,7 +78,7 @@ export class TableProvider {
private constructor(opts: TableProviderOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(tableMorfo, {});
this.runtime = this.soma.runtime(tableMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -102,7 +102,6 @@ export class TableProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current,
'aria-label': this.resolvedLabel
} as const)
);

@ -81,6 +81,7 @@ export class TabsProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(tabsMorfo, {
dir: { get: () => this.opts.dir.current },
states: {
value: () => this.opts.value.current
},
@ -166,8 +167,7 @@ export class TabsProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current
...this.runtimePart.props
} as const)
);
}

@ -60,6 +60,7 @@ export class TagGroupProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(tagGroupMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
ariaLabel: () => this.resolvedAriaLabel.current,
ariaLabelledby: () => this.resolvedAriaLabelledby,
@ -250,7 +251,6 @@ export class TagGroupProvider {
// The RAW assertion, never the resolved value: omitted when nobody
// asserted a direction, so the element inherits instead of forcing
// `ltr`. `resolvedDir` is the other half, for the maths.
dir: this.opts.dir.current
} as const)
);
}

@ -62,6 +62,7 @@ export class TagsInputProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(tagsInputMorfo, {
dir: null,
props: {
empty: () => this.isEmpty,
disabled: () => this.opts.disabled.current,

@ -77,6 +77,7 @@ export class TextAreaProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(textareaMorfo, {
dir: null,
props: {
disabled: () => this.isDisabled,
readonly: () => this.isReadonly,

@ -166,6 +166,7 @@ export class TimeFieldProvider {
private constructor(opts: TimeFieldOpts) {
this.opts = opts;
this.runtime = this.soma.runtime(timeFieldMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
disabled: () => this.isDisabled,
readonly: () => this.isReadonly,
@ -497,7 +498,6 @@ export class TimeFieldProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current,
'aria-invalid': this.isInvalid || undefined
} as const)
);

@ -100,6 +100,7 @@ export class TimePickerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(timePickerMorfo, {
dir: { get: () => this.opts.dir.current },
states: { open: () => this.opts.open.current },
props: {
disabled: () => this.opts.disabled.current,
@ -259,7 +260,6 @@ export class TimePickerProvider {
// `flex-direction: row`) reverses in RTL, and the embedded trigger rides
// `margin-inline-start: auto`. The portalled clock reads the floating
// wrapper's own stamp.
dir: this.opts.dir.current
} as const)
);
}

@ -90,6 +90,7 @@ export class TimeRangeFieldProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(timeRangeFieldMorfo, {
dir: { get: () => this.opts.dir.current },
props: {
disabled: () => this.isDisabled,
readonly: () => this.isReadonly,
@ -173,7 +174,6 @@ export class TimeRangeFieldProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
dir: this.opts.dir.current,
'aria-invalid': this.isInvalid || undefined
} as const)
);

@ -106,6 +106,7 @@ export class TimeRangePickerProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(timeRangePickerMorfo, {
dir: { get: () => this.opts.dir.current },
states: { open: () => this.opts.open.current },
props: {
disabled: () => this.opts.disabled.current,
@ -306,7 +307,6 @@ export class TimeRangePickerProvider {
// endpoint + trigger order reverses in RTL, and the embedded trigger rides
// `margin-inline-start: auto` (each endpoint's own composed TimeField stamps
// its segments). The portalled clock reads the floating wrapper's own stamp.
dir: this.opts.dir.current
} as const)
);
}

@ -52,6 +52,7 @@ export class TimelineProvider {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(timelineMorfo, {
dir: null,
props: { live: () => this.opts.live.current },
translate: (key) => this.soma.langs.ts(key)
});
@ -144,6 +145,7 @@ export class TimelineItemProvider {
// Per-item runtime so the `emerge-reveal` event resolves THIS item's
// `intent` (a shared runtime can't disambiguate one item from another).
this.runtime = this.provider.soma.runtime(timelineMorfo, {
dir: null,
props: {
status: () => this.effectiveStatus,
intent: () => this.opts.intent.current ?? undefined

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save

Powered by TurnKey Linux.