feat(direction): el flip — el atributo es la afirmacion, y undefined ACTUA

La cadena se parte en la afirmacion. `activeDir` devuelve ahora SOLO lo que
alguien eligio (prop -> contexto del ancestro) y ahi se detiene: eso es lo que
el runtime estampa y lo que cruza portales. La preferencia de la app ya no
viaja en el atributo de cada componente — llega a la pagina UNA vez, por la
proyeccion automatica de P2, y todo lo demas HEREDA.

Por que: con prefs dentro de la cadena por-componente, "nadie afirmo" era
inalcanzable — el boot por defecto siempre tiene la dimension direction, asi
que getDir() casi nunca era undefined y cada raiz del canon estampaba
dir="ltr" en una pagina sin afirmaciones. El atributo nunca llegaba a estar
AUSENTE, la herencia del DOM nunca llegaba a trabajar, y de ahi la clase
entera de islas y reenvios que este eje llevo meses parcheando.

La matematica conserva la preferencia como VALOR (no puede leer el DOM) en un
solo helper:

    afirmacion  ->  soma.prefs.getDir()  ->  'ltr'        resolveDir()

- activeDir(dir) pierde el parametro soma: 57 wrappers, y 49 quedan sin
  `const soma = Soma.get()` huerfano (limpiados con deteccion de identificador,
  no de ruta — la primera pasada se tropezaba con 'core/soma.svelte').
- 45 colas `opts.dir.current ?? 'ltr'` -> resolveDir(this.opts.dir, this.soma).
- Los 4 getDirectionalKeys de submenus que leian opts.dir.current directo
  pasan a resolvedDir — con undefined degradaban a ltr EN SILENCIO bajo rtl
  ambiental.
- command: 4.1 le habia dejado `dir: null` en el runtime y el estampado manual
  inline en el assert (la linea no estaba sola y el barrido no la vio). Ahora
  el runtime estampa y el inline se va.
- listbox / navigation-menu: el alias assertedDir muere — tras el flip,
  opts.dir ES la afirmacion.
- date-range-field-input / time-range-field-input: el reenvio dir= al Field
  compuesto en sitio sobra — DirectionContext lo lleva.
- 7 wrappers renombran `resolvedDir` -> `assertedDir`: el nombre mentia.
- picker / gradient-picker: activeDir(() => undefined) = la afirmacion del
  ancestro o nada — mejor que el prefs-siempre-concreto de antes.

MEDIDO en Chrome, el modelo entero:

  auto              UN solo [dir] en toda la pagina: el <html> de la proyeccion
                    (antes lo llevaba casi cada raiz del canon)
  prefs rtl         select sin atributo, computa rtl por herencia; el panel
                    portalizado SIN atributo y computa rtl (heredo del html a
                    traves del body)
  prop rtl          trigger del dropdown estampa rtl, el panel portalizado
                    estampa rtl (la afirmacion cruza), submenu data-side=left,
                    cero islas dentro
  matematica        slider SIN atributo con pagina rtl: click al 25% fisico da
                    75 y ArrowRight BAJA — resolveDir lee prefs sin que el
                    atributo lo haga
  SSR               curl de select y accordion: cero dir= en el payload

El contrato: §1 documenta la cadena partida (activeDir -> afirmacion;
resolveDir -> matematica), §6 la proyeccion automatica + la semilla y el
parrafo de SSR — el <html dir> del primer render pertenece a app.html o al
servidor (modelo react-aria).

check 77 = linea base · suites soma+active-uix+prefs+contracts 1362 tests:
los 6 de contracts y soma-attr-audit son el conjunto preexistente (A/B dos
veces en fases previas; attr-audit pasa aislado) · media-player 17/17 tras
hacer resolveDir defensivo con mocks sin prefs · rtl:check 1 (palabras) ·
docs:check 0/566.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent 973d2c886d
commit 01cc13d412

@ -52,21 +52,27 @@ site.
What remains true: a component still does **not read the DOM**. The DOM `dir`
attribute is a _projection_ of the assertion/preference, never a source.
| Link | Who runs it | Where |
| --------------- | ------------------------- | -------------------------------------------------- |
| `prop → ctx` | `activeDir(getter, soma)` | the **wrapper** `.svelte`, at `Provider.create(…)` |
| `ctx → prefs` | `activeDir(getter, soma)` | same call — the read must run at component init |
| `→ 'ltr'` | `resolvedDir` | the **provider**, once |
**The chain is split at the assertion.** `activeDir` returns the ASSERTION
(`prop → ctx`) and stops there — that is what the attribute stamps and what
crosses portals. The preference is deliberately NOT part of it: prefs reach
the page once, through the boot's automatic DOM projection on `<html>`, and
everything below inherits. Only the MATHS still needs the preference as a
value (it cannot read the DOM), and that tail lives in one helper:
| Link | Who runs it | Where |
| ---------------- | ------------------------ | -------------------------------------------------- |
| `prop → ctx` | `activeDir(getter)` | the **wrapper** `.svelte`, at `Provider.create(…)` |
| `→ prefs → 'ltr'`| `resolveDir(dir, soma)` | the **provider**, once, in `resolvedDir` |
A component with no soma provider runs the same first links through
`activeEidosDir` — see §7.
`activeDir` (`src/uix/soma/direction.ts`) returns
`Active<Direction | undefined>` — it deliberately stops before the default.
The `'ltr'` tail lives per-provider:
`Active<Direction | undefined>` — the raw assertion. The maths tail lives
per-provider:
```ts
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
```
**Why the chain is split.** `undefined` means _nobody asserted a direction_ —
@ -345,10 +351,20 @@ both.
## 6. The app-global half
`prefs.direction` is the single source of the _app's_ effective direction: it
derives from `prefs.language` unless an explicit intent overrides it.
`ActivePrefsDomProjection` writes that value to `<html dir>` — together with
`<html lang>`, which travels with it because the browser reads both from the DOM
for font selection, hyphenation and screen-reader announcement.
derives from `prefs.language` unless an explicit intent — or the environment
seed — overrides it. `ActivePrefsDomProjection` writes that value to
`<html dir>` — together with `<html lang>`, which travels with it because the
browser reads both from the DOM for font selection, hyphenation and
screen-reader announcement.
**The projection is automatic in standalone boot** (ratified 2026-08-05):
components stamp only ASSERTED directions, so the page must reflect the
ambient preference or nothing does. `projectPrefs: false` opts out when the
app owns `<html>` (i18n by routing, its own projection); in `attachActiveUix`
it is opt-IN for the same reason. And the boot SEEDS the environment from a
hand-set `<html dir>` (`readPrefsEnvironmentFromDom`) so the projection adopts
the page's own assertion instead of rewriting it with the language-derived
value — precedence `intent > environment seed > derive(language) > default`.
That projection is the reason the common case needs no per-component
assertion at all: the page declares its direction once at the root, every
@ -356,6 +372,15 @@ component inherits it, and `:dir()` resolves correctly with no attribute in
between. The per-component prop exists for the other case — asserting a
direction for **one subtree** against the page's.
### Server rendering owns the first paint
The projection runs in the client. The `<html dir>` of the FIRST render
therefore belongs to `app.html` or the server (the react-aria model): an app
whose default direction is RTL sets `<html dir="rtl" lang="ar">` at the
document template — the seed adopts it, the projection keeps it, and there is
no LTR flash. Components ship no `dir` in SSR output unless the consumer
asserted one, which is the same rule they follow in the client.
Detail of the preference and its projection: [`src/arts/prefs/README.md`](../../src/arts/prefs/README.md).
---

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { tick } from 'svelte';
import { readableActive, state, type ActiveProps, type StateProps } from '$libs/reactive';
import type { Orientation, Direction, SomaMouseEvent, SomaKeyboardEvent } from '../../types';
@ -39,7 +40,7 @@ export class AccordionProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by<Direction>(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by<Direction>(() => resolveDir(this.opts.dir, this.soma));
readonly opts: AccordionOpts;
readonly runtimePart: SomaRuntimePart;

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { AccordionProvider } from '../accordion-provider.svelte';
import type { AccordionProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -45,7 +43,7 @@
disabled: readableActive(() => disabled),
orientation: readableActive(() => orientation),
loop: readableActive(() => loop),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
region: readableActive(() => region)
});

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import type { Direction } from '../../types';
import { createAttrs } from '$uix/morfo';
import { boolToEmptyStrOrUndef } from '$adom';
@ -124,7 +125,7 @@ export class CalendarProvider implements CalendarNav {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: CalendarOpts;
readonly runtimePart: SomaRuntimePart;

@ -5,10 +5,8 @@
import { createId } from '$active-uix/id';
import { CalendarProvider, resolveInitialCalendarPlaceholder } from '../calendar-provider.svelte';
import type { CalendarProps } from '../types';
import { Soma } from '../../../core/soma.svelte';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -94,7 +92,7 @@
showWeekNumbers: readableActive(() => showWeekNumbers),
weekNumberFormat: readableActive(() => weekNumberFormat),
announceNavigation: readableActive(() => announceNavigation),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
locale: readableActive(() => locale ?? '')
});

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { boolToEmptyStrOrUndef } from '$adom';
import { readableActive, type Active, type ActiveProps, type StateProps } from '$libs/reactive';
@ -56,7 +57,7 @@ export class CarouselProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: CarouselOpts;
readonly runtimePart: SomaRuntimePart;

@ -5,10 +5,8 @@
import { createId } from '$active-uix/id';
import { CarouselProvider } from '../carousel-provider.svelte';
import type { CarouselProps } from '../types';
import { Soma } from '../../../core/soma.svelte';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -70,7 +68,7 @@
stopOnMouseEnter: readableActive(() => stopOnMouseEnter),
stopOnLastSnap: readableActive(() => stopOnLastSnap),
disabled: readableActive(() => disabled),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
locale: readableActive(() => locale),
ariaLabel: readableActive(() => ariaLabel)
});

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { watch } from '$libs/reactive';
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
@ -125,7 +126,7 @@ export class ColorFieldProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: ColorFieldOpts;
readonly runtimePart: SomaRuntimePart;

@ -4,13 +4,11 @@
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { DEFAULT_COLOR, type ColorValue } from '$libs/color';
import { ColorFieldProvider } from '../color-field-provider.svelte';
import type { ColorFieldProviderProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -90,7 +88,7 @@
required: readableActive(() => required),
invalid: readableActive(() => invalid),
errorMessageId: readableActive(() => errorMessageId),
dir: activeDir(() => _dir, soma)
dir: activeDir(() => _dir)
});
const mergedProps = $derived(mergeProps(restProps, state.props));

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { watch } from '$libs/reactive';
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
@ -167,7 +168,7 @@ export class ColorPickerProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: ColorPickerOpts;
readonly runtimePart: SomaRuntimePart;

@ -4,7 +4,6 @@
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { DEFAULT_COLOR, type ColorValue } from '$libs/color';
import { ColorPickerProvider } from '../color-picker-provider.svelte';
import { PopoverProvider } from '../../popover/popover-provider.svelte';
@ -12,7 +11,6 @@
import type { ColorPickerProviderProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -142,7 +140,7 @@
}
);
const resolvedDir = activeDir(() => _dir, soma);
const assertedDir = activeDir(() => _dir);
// ── ColorPicker provider (shared state + channel setters) ────────────────
const picker = ColorPickerProvider.create({
@ -177,7 +175,7 @@
invalid: readableActive(() => invalid),
errorMessageId: readableActive(() => errorMessageId),
locale: readableActive(() => locale),
dir: resolvedDir
dir: assertedDir
});
// ── Popover provider (skipped when inline) ────────────────────────────────
@ -191,7 +189,7 @@
modal: readableActive(() => mode === 'modal'),
// The panel is portalled out of this subtree, so the popover is the only
// route this component's `dir` has into its own chrome.
dir: resolvedDir
dir: assertedDir
});
}
@ -216,7 +214,7 @@
required: readableActive(() => required),
invalid: readableActive(() => invalid),
errorMessageId: readableActive(() => errorMessageId),
dir: resolvedDir
dir: assertedDir
});
const mergedProps = $derived(mergeProps(restProps, picker.props));

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { SvelteMap } from 'svelte/reactivity';
import { createAttrs } from '$uix/morfo';
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
@ -65,7 +66,7 @@ export class ComboboxProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: ComboboxOpts;
readonly runtimePart: SomaRuntimePart;

@ -6,10 +6,8 @@
import { ComboboxProvider } from '../combobox-provider.svelte';
import type { ComboboxProps } from '../types';
import { Soma } from '../../../core/soma.svelte';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -68,7 +66,7 @@
loop: readableActive(() => loop),
deselectable: readableActive(() => deselectable),
allowCustomValue: readableActive(() => allowCustomValue),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
onOpenChangeComplete: readableActive(() => onOpenChangeComplete)
});

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import type { Direction } from '../../types';
import { boolToEmptyStrOrUndef } from '$adom';
import {
@ -101,7 +102,7 @@ export class CommandProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: CommandOpts;
readonly runtimePart: SomaRuntimePart;
@ -139,7 +140,7 @@ export class CommandProvider {
private constructor(opts: CommandOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(commandMorfo, { dir: null });
this.runtime = this.soma.runtime(commandMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -426,7 +427,7 @@ export class CommandProvider {
readonly snippetProps: CommandSnippetProps;
readonly props = $derived.by(() =>
this.runtimePart.assert({ ...this.runtimePart.props, dir: this.opts.dir.current } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}

@ -6,10 +6,8 @@
import { CommandProvider } from '../command-provider.svelte';
import type { CommandProps } from '../types';
import { computeFuzzyScore } from '$libs/strings';
import { Soma } from '../../../core/soma.svelte';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -59,7 +57,7 @@
vimBindings: readableActive(() => vimBindings),
columns: readableActive(() => columns),
disablePointerSelection: readableActive(() => disablePointerSelection),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
onSelect: readableActive(() => onSelect)
});

@ -4,10 +4,8 @@
import { createId } from '$active-uix/id';
import { ContextMenuProvider } from '../context-menu-provider.svelte';
import type { ContextMenuProps } from '../types';
import { Soma } from '../../../core/soma.svelte';
const uid = $props.id();
const soma = Soma.get();
let {
id = createId(uid, 'context-menu'),
@ -28,7 +26,7 @@
}
),
onOpenChangeComplete: readableActive(() => onOpenChangeComplete),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
</script>

@ -1,4 +1,5 @@
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { boolToEmptyStrOrUndef, getDataOpenClosed } from '$adom';
import { readableActive, state, type ActiveProps, type StateProps } from '$libs/reactive';
@ -58,7 +59,7 @@ export class ContextMenuProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: ContextMenuOpts;
readonly runtimePart: SomaRuntimePart;
@ -933,7 +934,7 @@ export class ContextMenuSubTriggerProvider {
readonly onkeydown = (e: SomaKeyboardEvent<HTMLElement>) => {
if (this.opts.disabled.current) return;
const dir = this.sub.provider.opts.dir.current;
const dir = this.sub.provider.resolvedDir;
const { nextKey } = getDirectionalKeys(dir, 'horizontal');
if (e.key === nextKey || e.key === KEYS.ENTER || e.key === KEYS.SPACE) {
e.preventDefault();
@ -1034,7 +1035,7 @@ export class ContextMenuSubContentProvider {
readonly wrapperProps = $derived.by(() => buildFloatingShellWrapperProps(this.floating));
readonly onkeydown = (e: KeyboardEvent) => {
const dir = this.sub.provider.opts.dir.current;
const dir = this.sub.provider.resolvedDir;
const { prevKey } = getDirectionalKeys(dir, 'horizontal');
const { nextKey: downKey, prevKey: upKey } = getDirectionalKeys(dir, 'vertical');

@ -1,14 +1,12 @@
<script lang="ts">
import { readableActive, writableActive } from '$libs/reactive';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { CssFieldProvider } from '../css-field-provider.svelte';
import type { CssFieldProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -59,7 +57,7 @@
max: readableActive(() => max),
step: readableActive(() => step),
largeStep: readableActive(() => largeStep),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
clampOnBlur: readableActive(() => clampOnBlur),
allowMouseWheel: readableActive(() => allowMouseWheel),
disabled: readableActive(() => disabled),

@ -1,4 +1,5 @@
import { watch } from '$libs/reactive';
import { resolveDir } from '../../direction';
import { context, type WithRefOpts } from '../../provider';
import { boolToEmptyStrOrUndef, isHTMLElement } from '$adom';
import { readableActive, type Active, type ActiveProps, type StateProps } from '$libs/reactive';
@ -199,7 +200,7 @@ export class CssFieldProvider {
syncAttrs: true
});
this.resolvedDir = readableActive(() => opts.dir.current ?? 'ltr');
this.resolvedDir = readableActive(() => resolveDir(opts.dir, this.soma));
// Sync the visible text with external value changes while not editing.
watch.pre(

@ -97,7 +97,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: readableActive(() => locale ?? soma?.langs.getLocale() ?? ''),
dir: activeDir(() => _dir, soma),
dir: activeDir(() => _dir),
minValue: readableActive(() => minValue),
maxValue: readableActive(() => maxValue),
validate: readableActive(() => validate),

@ -1,4 +1,5 @@
import type { Updater } from 'svelte/store';
import { resolveDir } from '../../direction';
import { untrack } from 'svelte';
import { watch } from '$libs/reactive';
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
@ -208,7 +209,7 @@ export class DateFieldProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: DateFieldOpts;
readonly runtimePart: SomaRuntimePart;

@ -111,7 +111,7 @@
);
const resolvedLocale = readableActive(() => locale ?? soma?.langs.getLocale() ?? '');
const resolvedDir = activeDir(() => dir, soma);
const assertedDir = activeDir(() => dir);
// ── DatePicker context (shared source of truth) ───────────────────────────
const state = DatePickerProvider.create({
@ -140,7 +140,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: resolvedLocale,
dir: resolvedDir,
dir: assertedDir,
pagedNavigation: readableActive(() => pagedNavigation),
weekStartsOn: readableActive(() => weekStartsOn),
weekdayFormat: readableActive(() => weekdayFormat),
@ -170,7 +170,7 @@
modal: readableActive(() => mode === 'modal'),
// The panel is portalled out of this subtree, so the popover is the only
// route this component's `dir` has into its own chrome.
dir: resolvedDir
dir: assertedDir
});
// ── DateField provider wired to the shared value / placeholder / config ───
@ -195,7 +195,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: resolvedLocale,
dir: resolvedDir,
dir: assertedDir,
errorMessageId: readableActive(() => errorMessageId)
});

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { watch } from '$libs/reactive';
import { context, type ProviderOpts } from '../../provider';
import { createAttrs } from '$uix/morfo';
@ -127,7 +128,7 @@ export class DatePickerProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: DatePickerOpts;
readonly runtimePart: SomaRuntimePart;

@ -75,7 +75,6 @@
hideTimeZone={provider.opts.hideTimeZone.current}
hourCycle={provider.opts.hourCycle.current}
locale={provider.opts.locale.current ?? ''}
dir={provider.opts.dir.current}
errorMessageId={provider.opts.errorMessageId.current}
{...(identity as Record<string, unknown>)}
>

@ -4,7 +4,6 @@
import { readableActive, writableActive, state } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import {
CalendarDate,
getLocalTimeZone,
@ -16,7 +15,6 @@
import type { DateRangeFieldProviderProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -129,7 +127,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: readableActive(() => locale),
dir: activeDir(() => _dir, soma),
dir: activeDir(() => _dir),
errorMessageId: readableActive(() => errorMessageId)
});

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import {
readableActive,
@ -79,7 +80,7 @@ export class DateRangeFieldProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: DateRangeFieldOpts;
readonly runtimePart: SomaRuntimePart;

@ -144,7 +144,7 @@
}
);
const resolvedDir = activeDir(() => dir, soma);
const assertedDir = activeDir(() => dir);
// ── Shared state via picker context ───────────────────────────────────────
const pickerState = DateRangePickerProvider.create({
@ -180,7 +180,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: readableActive(() => locale ?? soma?.langs.getLocale() ?? ''),
dir: resolvedDir,
dir: assertedDir,
pagedNavigation: readableActive(() => pagedNavigation),
weekStartsOn: readableActive(() => weekStartsOn),
weekdayFormat: readableActive(() => weekdayFormat),
@ -209,7 +209,7 @@
modal: readableActive(() => mode === 'modal'),
// The panel is portalled out of this subtree, so the popover is the only
// route this component's `dir` has into its own chrome.
dir: resolvedDir
dir: assertedDir
});
// ── DateRangeField wired to shared value / config ─────────────────────────
@ -237,7 +237,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: readableActive(() => locale),
dir: resolvedDir,
dir: assertedDir,
errorMessageId: readableActive(() => errorMessageId)
});

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { watch } from '$libs/reactive';
import { context, type ProviderOpts } from '../../provider';
import { createAttrs } from '$uix/morfo';
@ -115,7 +116,7 @@ export class DateRangePickerProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: DateRangePickerOpts;
readonly runtimePart: SomaRuntimePart;

@ -5,13 +5,11 @@
} from '$libs/reactive';
import { createId } from '$active-uix/id';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { DrawerProvider } from '../drawer-provider.svelte';
import type { DrawerProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
id = createId(uid, 'drawer'),
@ -64,7 +62,7 @@
),
onOpenChangeComplete: readableActive(() => onOpenChangeComplete),
direction: readableActive(() => direction),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
variant: readableActive(() => variant),
disabled: readableActive(() => disabled),
dismissible: readableActive(() => dismissible),

@ -1,4 +1,5 @@
import { context } from '../../provider';
import { resolveDir } from '../../direction';
import { boolToEmptyStrOrUndef, getDataOpenClosed } from '$adom';
import {
readableActive,
@ -183,7 +184,7 @@ export class DrawerProvider {
* `prop → soma.prefs.getDir() → 'ltr'` runs in the WRAPPER (`activeDir`),
* so this only supplies the final fallback.
*/
readonly resolvedDir = $derived.by((): Direction => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by((): Direction => resolveDir(this.opts.dir, this.soma));
readonly resolvedSide = $derived.by(
(): DrawerSide => resolveDirection(this.opts.direction.current, this.resolvedDir)

@ -1,6 +1,6 @@
<script lang="ts">
import { readableActive, writableActive } from '$libs/reactive';
import { activeDir } from '../../../direction';
import { activeDir, resolveDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { styleToString } from '../../../css';
import { createId } from '$active-uix/id';
@ -30,15 +30,17 @@
...restProps
}: MenuSubContentProps = $props();
// The MATHS half, and the only reason this file still runs the chain: the
// default side is a concrete left/right, so it needs a resolved value.
// Submenus default to the logical "end" side, which in RTL means the visual
// left; an explicit `side` prop always wins. The parent menu's assertion
// arrives through the DirectionContext link — no hand-composed step. The
// ATTRIBUTE half is not here — the floating layer composes the owner's
// assertion for the portalled panel.
const resolvedSide = activeDir(() => dir, soma);
const effectiveSide = $derived(side ?? (resolvedSide.current === 'rtl' ? 'left' : 'right'));
// The MATHS half, and the only reason this file runs the maths tail: the
// default side is a concrete left/right, so it needs a RESOLVED value —
// assertion (own prop, or the parent menu's via DirectionContext) → prefs →
// 'ltr'. Submenus default to the logical "end" side, which in RTL means the
// visual left; an explicit `side` prop always wins. The ATTRIBUTE half is
// not here — the floating layer composes the owner's assertion for the
// portalled panel.
const assertedDir = activeDir(() => dir);
const effectiveSide = $derived(
side ?? (resolveDir(assertedDir, soma) === 'rtl' ? 'left' : 'right')
);
const state = MenuSubContentProvider.create({
id: readableActive(() => id),
@ -54,11 +56,10 @@
collisionBoundary: readableActive(() => collisionBoundary),
collisionPadding: readableActive(() => collisionPadding),
loop: readableActive(() => loop),
// Raw, NOT `activeDir`: the floating layer runs the chain behind this
// (own prop → the owning provider's assertion → prefs). Resolving here
// would make the value always concrete and the owner could never reach
// through the portal.
dir: readableActive(() => dir),
// The ASSERTION (own prop → ancestor via DirectionContext) — never a
// resolved default, so the floating wrapper can still omit the
// attribute when nobody asserted and inherit from the projected page.
dir: assertedDir,
style: readableActive(() => style)
});

@ -5,10 +5,8 @@
import { MenuProvider } from '../dropdown-menu-provider.svelte';
import type { MenuProps } from '../types';
import { Soma } from '../../../core/soma.svelte';
const uid = $props.id();
const soma = Soma.get();
let {
id = createId(uid, 'dropdown-menu'),
@ -30,7 +28,7 @@
}
),
onOpenChangeComplete: readableActive(() => onOpenChangeComplete),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
accessibleWhenDisabled: readableActive(() => accessibleWhenDisabled)
});
</script>

@ -1,4 +1,5 @@
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { boolToEmptyStrOrUndef, getDataOpenClosed, contains } from '$adom';
import {
@ -64,7 +65,7 @@ export class MenuProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: MenuOpts;
readonly runtimePart: SomaRuntimePart;
@ -1159,7 +1160,7 @@ export class MenuSubTriggerProvider {
readonly onkeydown = (e: SomaKeyboardEvent<HTMLElement>) => {
if (this.opts.disabled.current) return;
const dir = this.sub.provider.opts.dir.current;
const dir = this.sub.provider.resolvedDir;
const { nextKey } = getDirectionalKeys(dir, 'horizontal');
if (e.key === nextKey || e.key === KEYS.ENTER || e.key === KEYS.SPACE) {
e.preventDefault();
@ -1278,7 +1279,7 @@ export class MenuSubContentProvider {
);
if (within && within !== this.opts.ref.current) return;
const dir = this.sub.provider.opts.dir.current;
const dir = this.sub.provider.resolvedDir;
const { prevKey } = getDirectionalKeys(dir, 'horizontal');
const { nextKey: downKey, prevKey: upKey } = getDirectionalKeys(dir, 'vertical');

@ -3,13 +3,11 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { EmojiPickerProvider } from '../emoji-picker-provider.svelte';
import { PopoverProvider } from '../../popover/popover-provider.svelte';
import type { EmojiPickerProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -40,7 +38,7 @@
}
);
const resolvedDir = activeDir(() => dir, soma);
const assertedDir = activeDir(() => dir);
const provider = EmojiPickerProvider.create({
id: readableActive(() => id),
@ -65,7 +63,7 @@
showRecents: readableActive(() => showRecents),
maxRecents: readableActive(() => maxRecents),
closeOnSelect: readableActive(() => closeOnSelect),
dir: resolvedDir
dir: assertedDir
});
// The composed Popover — owns open/close, focus trap/return, portal.
@ -76,7 +74,7 @@
onOpenChangeComplete: readableActive(() => () => {}),
// The panel is portalled out of this subtree, so the popover is the only
// route this component's `dir` has into its own chrome.
dir: resolvedDir
dir: assertedDir
});
const mergedProps = $derived(mergeProps(restProps, provider.props));

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { readableActive, type ActiveProps, type StateProps } from '$libs/reactive';
import { boolToEmptyStrOrUndef } from '$adom';
import type { Direction } from '../../types';
@ -50,7 +51,7 @@ export class EmojiPickerProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: EmojiPickerOpts;
readonly runtimePart: SomaRuntimePart;

@ -3,12 +3,10 @@
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { FeedProvider } from '../feed-provider.svelte';
import type { FeedProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -35,7 +33,7 @@
ariaLabel: readableActive(() => ariaLabel),
ariaLabelledby: readableActive(() => ariaLabelledby),
onLoadMore: readableActive(() => onLoadMore),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
const mergedProps = $derived(mergeProps(restProps, provider.props));

@ -2,12 +2,10 @@
import { readableActive, writableActive } from '$libs/reactive';
import { createId } from '$active-uix/id';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { FloatPanelProvider } from '../float-panel-provider.svelte';
import type { FloatPanelProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
id = createId(uid, 'float-panel'),
@ -95,7 +93,7 @@
closeOnInteractOutside: readableActive(() => closeOnInteractOutside),
zIndex: readableActive(() => zIndex),
onOpenChangeComplete: readableActive(() => onOpenChangeComplete),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
</script>

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { context } from '../../provider';
import {
state,
@ -126,7 +127,7 @@ export class FloatPanelProvider {
* `prop → soma.prefs.getDir() → 'ltr'` runs in the WRAPPER (`activeDir`),
* so this only supplies the final fallback.
*/
readonly resolvedDir = $derived.by((): Direction => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by((): Direction => resolveDir(this.opts.dir, this.soma));
static readonly ctx = context<FloatPanelProvider>('FloatPanel');
static get(): FloatPanelProvider | undefined {

@ -21,11 +21,11 @@
import type { Gradient } from '$libs/gradient';
const uid = $props.id();
const soma = Soma.get();
// No `dir` prop on this component, so the chain runs with the first link
// absent: prefs alone. The popover needs it anyway — the panel is portalled
// and cannot inherit from this subtree.
const resolvedDir = activeDir(() => undefined, soma);
// No `dir` prop on this component: its assertion is whatever the nearest
// ancestor asserted (DirectionContext), or nothing. The composed popover
// needs it because provider creation is not a component boundary — no
// context is published in between.
const assertedDir = activeDir(() => undefined);
let {
ref = $bindable(null),
@ -107,7 +107,7 @@
onOpenChangeComplete: readableActive(() => onOpenChangeComplete),
modal: readableActive(() => mode === 'modal'),
// The panel is portalled out of this subtree and cannot inherit from it.
dir: resolvedDir
dir: assertedDir
});
const mergedProps = $derived(mergeProps(restProps, state.props));

@ -4,14 +4,12 @@
writableActive
} from '$libs/reactive';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { GridListProvider } from '../grid-list-provider.svelte';
import type { GridListProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -53,7 +51,7 @@
typeahead: readableActive(() => typeahead),
typeaheadTimeout: readableActive(() => typeaheadTimeout),
disabled: readableActive(() => disabled),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
readonly: readableActive(() => readonly),
required: readableActive(() => required),
invalid: readableActive(() => invalid),

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { boolToEmptyStrOrUndef } from '$adom';
import { readableActive, type Active, type ActiveProps, type StateProps } from '$libs/reactive';
@ -115,7 +116,7 @@ export class GridListProvider {
* so this only supplies the final fallback.
*/
readonly resolvedDir: Active<Direction> = readableActive(
() => this.opts.dir.current ?? 'ltr'
() => resolveDir(this.opts.dir, this.soma)
);
readonly resolvedAriaLabel: Active<string | undefined> = readableActive(() =>

@ -5,12 +5,10 @@
} from '$libs/reactive';
import { createId } from '$active-uix/id';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { LinkPreviewProvider } from '../link-preview-provider.svelte';
import type { LinkPreviewProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
id = createId(uid, 'link-preview'),
@ -37,7 +35,7 @@
openDelay: readableActive(() => openDelay),
closeDelay: readableActive(() => closeDelay),
disabled: readableActive(() => disabled),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
</script>

@ -4,14 +4,12 @@
writableActive
} from '$libs/reactive';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { ListboxProvider } from '../listbox-provider.svelte';
import type { ListboxProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -52,7 +50,7 @@
typeahead: readableActive(() => typeahead),
typeaheadTimeout: readableActive(() => typeaheadTimeout),
orientation: readableActive(() => orientation),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
disabled: readableActive(() => disabled),
readonly: readableActive(() => readonly),
required: readableActive(() => required),

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { boolToEmptyStrOrUndef } from '$adom';
import { readableActive, type Active, type ActiveProps, type StateProps } from '$libs/reactive';
@ -76,7 +77,7 @@ export class ListboxProvider {
timeout: () => this.opts.typeaheadTimeout.current,
meta: { component: 'listbox', action: 'typeahead-reset' }
});
this.runtime = this.soma.runtime(listboxMorfo, { dir: { get: () => this.assertedDir.current } });
this.runtime = this.soma.runtime(listboxMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -117,20 +118,8 @@ export class ListboxProvider {
);
/** Concrete direction for this component's OWN math (arrow keys). */
readonly resolvedDir: Active<Direction> = readableActive(() => this.assertedDir.current ?? 'ltr');
readonly resolvedDir: Active<Direction> = readableActive(() => resolveDir(this.opts.dir, this.soma));
/**
* What someone actually ASSERTED — `undefined` when neither the prop nor a
* registered `direction` preference said anything. This, not `resolvedDir`,
* is what the `dir` attribute stamps: an omitted attribute inherits, while
* a defaulted `'ltr'` would force this subtree back to LTR inside an RTL
* page. The chain `prop → soma.prefs.getDir()` runs in the WRAPPER
* (`activeDir`), so this is already the asserted value — resolving it here
* again would duplicate the chain.
*/
readonly assertedDir: Active<Direction | undefined> = readableActive(
() => this.opts.dir.current
);
readonly resolvedAriaLabel: Active<string | undefined> = readableActive(() =>
this.opts.ariaLabelledby.current

@ -4,11 +4,9 @@
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { MediaPlayerProvider } from '../media-player-provider.svelte';
import { Soma } from '../../../core/soma.svelte';
import type { MediaPlayerProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -38,7 +36,7 @@
),
media: readableActive(() => media),
seekStep: readableActive(() => seekStep),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
ariaLabel: readableActive(() => ariaLabel),
metadata: readableActive(() => metadata),
onPlay: readableActive(() => onPlay),

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { boolToEmptyStrOrUndef } from '$adom';
import type { Active, ActiveProps } from '$libs/reactive';
import type { TimerHandle } from '$timer';
@ -121,7 +122,7 @@ export class MediaPlayerProvider {
* `opts.dir.current`, so it is omitted when nobody asserted a direction and
* the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
static readonly ctx = context<MediaPlayerProvider>('MediaPlayer');
static get(): MediaPlayerProvider | undefined {

@ -5,10 +5,8 @@
import { createId } from '$active-uix/id';
import { MenubarProvider } from '../menubar-provider.svelte';
import type { MenubarProps } from '../types';
import { Soma } from '../../../core/soma.svelte';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -35,7 +33,7 @@
onValueChange(v);
}
),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
loop: readableActive(() => loop)
});

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { boolToEmptyStrOrUndef, getDataOpenClosed, contains } from '$adom';
import {
readableActive,
@ -52,7 +53,7 @@ export class MenubarProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: MenubarOpts;
readonly runtimePart: SomaRuntimePart;

@ -5,10 +5,8 @@
import { createId } from '$active-uix/id';
import { MonthGridProvider, resolveInitialMonthPlaceholder } from '../month-grid-provider.svelte';
import type { MonthGridProps } from '../types';
import { Soma } from '../../../core/soma.svelte';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -54,7 +52,7 @@
maxValue: readableActive(() => maxValue),
disabled: readableActive(() => disabled),
readonly: readableActive(() => readonly),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
locale: readableActive(() => locale ?? ''),
monthFormat: readableActive(() => monthFormat),
ariaLabel: readableActive(() => ariaLabel)

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import type { Direction } from '../../types';
import { boolToEmptyStrOrUndef } from '$adom';
import { state, type ActiveProps, type StateProps } from '$libs/reactive';
@ -74,7 +75,7 @@ export class MonthGridProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: MonthGridOpts;
readonly runtimePart: SomaRuntimePart;

@ -17,7 +17,6 @@
import type { NaturalTimePickerProviderProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
id = createId(uid, 'natural-time-picker'),
@ -64,7 +63,7 @@
);
// ── Transaction core — no DOM ref; context + workingValue + footer handle ──
const resolvedDir = activeDir(() => dir, soma);
const assertedDir = activeDir(() => dir);
const picker = PickerProvider.create<TimeValue | undefined>({
id: readableActive(() => `${id}-picker`),
@ -100,7 +99,7 @@
band: readableActive(() => band),
chipsIconOnly: readableActive(() => chipsIconOnly),
locale: readableActive(() => locale),
dir: resolvedDir,
dir: assertedDir,
disabled: readableActive(() => disabled),
readonly: readableActive(() => readonly),
required: readableActive(() => required),
@ -115,7 +114,7 @@
modal: readableActive(() => mode === 'modal'),
// The panel is portalled out of this subtree, so the popover is the only
// route this component's `dir` has into its own chrome.
dir: resolvedDir
dir: assertedDir
});
const mergedProps = $derived(mergeProps(restProps, state.props));

@ -1,4 +1,5 @@
import { context, type ProviderOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { writableActive, type ActiveProps, type State, type StateProps } from '$libs/reactive';
import type { Direction } from '../../types';
@ -71,7 +72,7 @@ export class NaturalTimePickerProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: NaturalTimePickerOpts;
readonly soma: Soma;

@ -10,13 +10,11 @@
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { NavTreeProvider } from '../nav-tree-provider.svelte';
import NavTreeNode from './nav-tree-node.svelte';
import type { NavTreeProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -40,7 +38,7 @@
activeHref: readableActive(() => activeHref),
ariaLabel: readableActive(() => ariaLabel),
badge: readableActive(() => badge),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
const mergedProps = $derived(mergeProps(restProps, provider.providerProps));

@ -4,14 +4,12 @@
writableActive
} from '$libs/reactive';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { NavigationMenuProvider } from '../navigation-menu-provider.svelte';
import type { NavigationMenuProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -44,7 +42,7 @@
),
onValueChange: readableActive(() => onValueChange),
orientation: readableActive(() => orientation),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
openDelay: readableActive(() => openDelay),
groupSkipDelay: readableActive(() => groupSkipDelay),
closeDelay: readableActive(() => closeDelay),

@ -1,4 +1,5 @@
import { SvelteMap } from 'svelte/reactivity';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { context, type WithRefOpts } from '../../provider';
import { boolToEmptyStrOrUndef } from '$adom';
@ -84,7 +85,7 @@ export class NavigationMenuProvider {
private constructor(opts: NavigationMenuOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(navigationMenuMorfo, { dir: { get: () => this.assertedDir.current } });
this.runtime = this.soma.runtime(navigationMenuMorfo, { dir: { get: () => this.opts.dir.current } });
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
@ -115,22 +116,8 @@ export class NavigationMenuProvider {
}
/** Concrete direction for this component's OWN math (arrow keys, placement). */
readonly resolvedDir: Active<Direction> = readableActive(
() => this.assertedDir.current ?? 'ltr'
);
readonly resolvedDir: Active<Direction> = readableActive(() => resolveDir(this.opts.dir, this.soma));
/**
* What someone actually ASSERTED — `undefined` when neither the prop nor a
* registered `direction` preference said anything. This, not `resolvedDir`,
* is what the `dir` attribute stamps: an omitted attribute inherits, while
* a defaulted `'ltr'` would force this subtree back to LTR inside an RTL
* page. The chain `prop → soma.prefs.getDir()` runs in the WRAPPER
* (`activeDir`), so this is already the asserted value — resolving it here
* again would duplicate the chain.
*/
readonly assertedDir: Active<Direction | undefined> = readableActive(
() => this.opts.dir.current
);
readonly resolvedAriaLabel: Active<string | undefined> = readableActive(
() =>

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { NumberFieldProvider } from '../number-field-provider.svelte';
import type { NumberFieldProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -56,7 +54,7 @@
largeStep: readableActive(() => largeStep),
maxLength: readableActive(() => maxLength),
locale: readableActive(() => locale),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
allowMouseWheel: readableActive(() => allowMouseWheel),
clampOnBlur: readableActive(() => clampOnBlur),
disabled: readableActive(() => disabled),

@ -1,4 +1,5 @@
import { watch } from '$libs/reactive';
import { resolveDir } from '../../direction';
import { context, type WithRefOpts } from '../../provider';
import { boolToEmptyStrOrUndef, isHTMLElement } from '$adom';
import { readableActive, type Active, type ActiveProps, type StateProps } from '$libs/reactive';
@ -224,7 +225,7 @@ export class NumberFieldProvider {
this.locale = readableActive(() => opts.locale.current ?? this.soma.langs.getLocale() ?? 'en');
this.resolvedDir = readableActive(() => opts.dir.current ?? 'ltr');
this.resolvedDir = readableActive(() => resolveDir(opts.dir, this.soma));
// Sync inputValue with external value/locale changes.
// When focused (user is typing), only update if the value itself changed — not on locale/format changes.

@ -15,11 +15,11 @@
import type { PickerProviderProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
// The generic Picker exposes no `dir` prop, so its chain runs with the first
// link absent: prefs alone. The popover needs it anyway — the panel is
// portalled and cannot inherit from this subtree.
const resolvedDir = activeDir(() => undefined, soma);
// The generic Picker exposes no `dir` prop: its assertion is whatever the
// nearest ancestor asserted (DirectionContext), or nothing. The composed
// popover needs it because provider creation is not a component boundary —
// no context is published in between.
const assertedDir = activeDir(() => undefined);
let {
id = createId(uid, 'picker'),
@ -86,7 +86,7 @@
onOpenChangeComplete: readableActive(() => onOpenChangeComplete),
modal: readableActive(() => mode === 'modal'),
// The panel is portalled out of this subtree and cannot inherit from it.
dir: resolvedDir
dir: assertedDir
});
const mergedProps = $derived(mergeProps(restProps, state.props));

@ -5,13 +5,11 @@
} from '$libs/reactive';
import { createId } from '$active-uix/id';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { PopoverProvider } from '../popover-provider.svelte';
import type { PopoverProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
id = createId(uid, 'popover'),
@ -34,7 +32,7 @@
),
onOpenChangeComplete: readableActive(() => onOpenChangeComplete),
modal: readableActive(() => modal),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
</script>

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { RadioGroupProvider } from '../radio-group-provider.svelte';
import type { RadioGroupProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -49,7 +47,7 @@
invalid: readableActive(() => invalid),
orientation: readableActive(() => orientation),
loop: readableActive(() => loop),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
name: readableActive(() => name),
ariaLabel: readableActive(() => ariaLabel),
ariaLabelledby: readableActive(() => ariaLabelledby)

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { context, type WithRefOpts } from '../../provider';
import { state, type ActiveProps, type StateProps } from '$libs/reactive';
@ -41,7 +42,7 @@ export class RadioGroupProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by<Direction>(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by<Direction>(() => resolveDir(this.opts.dir, this.soma));
readonly opts: RadioGroupOpts;
readonly runtimePart: SomaRuntimePart;

@ -8,10 +8,8 @@
resolveInitialRangeCalendarPlaceholder
} from '../range-calendar-provider.svelte';
import type { RangeCalendarProps } from '../types';
import { Soma } from '../../../core/soma.svelte';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -95,7 +93,7 @@
showWeekNumbers: readableActive(() => showWeekNumbers),
weekNumberFormat: readableActive(() => weekNumberFormat),
announceNavigation: readableActive(() => announceNavigation),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
locale: readableActive(() => locale ?? '')
});

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import type { Direction } from '../../types';
import { createAttrs } from '$uix/morfo';
import { boolToEmptyStrOrUndef, boolToStr } from '$adom';
@ -112,7 +113,7 @@ export class RangeCalendarProvider implements CalendarNav {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: RangeCalendarOpts;
readonly runtimePart: SomaRuntimePart;

@ -6,12 +6,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { RatingGroupProvider } from '../rating-group-provider.svelte';
import type { RatingGroupProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -59,7 +57,7 @@
required: readableActive(() => required),
invalid: readableActive(() => invalid),
orientation: readableActive(() => orientation),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
name: readableActive(() => name),
ariaLabel: readableActive(() => ariaLabel),
ariaLabelledby: readableActive(() => ariaLabelledby),

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { readableActive, type Active, type ActiveProps, type StateProps } from '$libs/reactive';
import type {
Direction,
@ -61,7 +62,7 @@ export class RatingGroupProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: RatingGroupOpts;
readonly runtimePart: SomaRuntimePart;

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { ScrollAreaProvider } from '../scroll-area-provider.svelte';
import type { ScrollAreaProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -29,7 +27,7 @@
),
type: readableActive(() => type),
scrollHideDelay: readableActive(() => scrollHideDelay),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
const mergedProps = $derived(mergeProps(restProps, state.props));

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { boolToEmptyStrOrUndef } from '$adom';
import { readableActive, state, type Active, type ActiveProps } from '$libs/reactive';
import type { Direction } from '../../types';
@ -29,7 +30,7 @@ export class ScrollAreaProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: ScrollAreaOpts;
readonly runtimePart: SomaRuntimePart;

@ -6,10 +6,8 @@
import { SelectProvider } from '../select-provider.svelte';
import type { SelectProps } from '../types';
import { Soma } from '../../../core/soma.svelte';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -67,7 +65,7 @@
loop: readableActive(() => loop),
deselectable: readableActive(() => deselectable),
scrollAlignment: readableActive(() => scrollAlignment),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
onOpenChangeComplete: readableActive(() => onOpenChangeComplete)
});

@ -1,4 +1,5 @@
import { SvelteMap } from 'svelte/reactivity';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
import { boolToEmptyStrOrUndef, getDataOpenClosed } from '$adom';
@ -74,7 +75,7 @@ export class SelectProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: SelectOpts;
readonly runtimePart: SomaRuntimePart;

@ -9,12 +9,10 @@
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { SidebarProvider } from '../sidebar-provider.svelte';
import type { SidebarProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -48,7 +46,7 @@
collapsible: readableActive(() => collapsible),
side: readableActive(() => side),
mobileBreakpoint: readableActive(() => mobileBreakpoint),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
const mergedProps = $derived(mergeProps(restProps, provider.providerProps));

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { SliderProvider } from '../slider-provider.svelte';
import type { SliderProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -56,7 +54,7 @@
orientation: readableActive(() => orientation),
disabled: readableActive(() => disabled),
readonly: readableActive(() => readonly),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
name: readableActive(() => name),
ariaLabel: readableActive(() => ariaLabel),
ariaLabelledby: readableActive(() => ariaLabelledby),

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { boolToEmptyStrOrUndef } from '$adom';
import { readableActive, type Active, type ActiveProps, type StateProps } from '$libs/reactive';
import type { OnChangeFn, Orientation, Direction } from '../../types';
@ -93,7 +94,7 @@ export class SliderProvider {
* nobody asserted a direction and the element inherits instead of forcing
* `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by<Direction>(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by<Direction>(() => resolveDir(this.opts.dir, this.soma));
readonly isDisabled = $derived.by(
() => this.opts.disabled.current || (this.field?.isDisabled ?? false)

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { SplitterProvider } from '../splitter-provider.svelte';
import type { SplitterProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -33,7 +31,7 @@
),
orientation: readableActive(() => orientation),
disabled: readableActive(() => disabled),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
keyboardStep: readableActive(() => keyboardStep),
sizes: writableActive(
() => sizes,

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { context, type WithRefOpts } from '../../provider';
import { state, type Active, type ActiveProps, type StateProps } from '$libs/reactive';
import type { OnChangeFn, Orientation, Direction, SomaKeyboardEvent } from '../../types';
@ -73,7 +74,7 @@ export class SplitterProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: SplitterOpts;
readonly runtimePart: SomaRuntimePart;

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { StepperProvider } from '../stepper-provider.svelte';
import type { StepperProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -37,7 +35,7 @@
}
),
orientation: readableActive(() => orientation),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
linear: readableActive(() => linear)
});

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { boolToEmptyStrOrUndef } from '$adom';
import { state, type Active, type ActiveProps, type StateProps } from '$libs/reactive';
import type { Orientation, Direction, SomaKeyboardEvent, SomaMouseEvent } from '../../types';
@ -31,7 +32,7 @@ export class StepperProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by<Direction>(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by<Direction>(() => resolveDir(this.opts.dir, this.soma));
readonly opts: StepperOpts;
readonly runtimePart: SomaRuntimePart;

@ -3,12 +3,10 @@
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { SwitchProvider } from '../switch-provider.svelte';
import type { SwitchProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -50,7 +48,7 @@
'aria-label': () => ariaLabel,
'aria-labelledby': () => ariaLabelledby
}) as Omit<Parameters<typeof SwitchProvider.create>[0], 'dir'>),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
const mergedProps = $derived(mergeProps(restProps, state.props));

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { TableProvider } from '../table-provider.svelte';
import type { TableProviderProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -29,7 +27,7 @@
),
table: readableActive(() => table),
label: readableActive(() => label),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
const mergedProps = $derived(mergeProps(restProps, state.props));

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { boolToEmptyStrOrUndef } from '$adom';
import { type ActiveProps } from '$libs/reactive';
import type { Direction, SomaKeyboardEvent, SomaMouseEvent } from '../../types';
@ -57,7 +58,7 @@ export class TableProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: TableProviderOpts;
readonly runtimePart: SomaRuntimePart;

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { TabsProvider } from '../tabs-provider.svelte';
import type { TabsProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -44,7 +42,7 @@
activationMode: readableActive(() => activationMode),
disabled: readableActive(() => disabled),
loop: readableActive(() => loop),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
const mergedProps = $derived(mergeProps(restProps, state.props));

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { context, type WithRefOpts } from '../../provider';
import { readableActive, type ActiveProps, type StateProps } from '$libs/reactive';
@ -45,7 +46,7 @@ export class TabsProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by<Direction>(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by<Direction>(() => resolveDir(this.opts.dir, this.soma));
readonly opts: TabsOpts;
readonly runtimePart: SomaRuntimePart;

@ -6,12 +6,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { TagGroupProvider } from '../tag-group-provider.svelte';
import type { TagGroupProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -46,7 +44,7 @@
items: readableActive(() => items),
selectionMode: readableActive(() => selectionMode),
disabled: readableActive(() => disabled),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
ariaLabel: readableActive(() => ariaLabel),
ariaLabelledby: readableActive(() => ariaLabelledby),
onValueChange: readableActive(() => onValueChange),

@ -1,4 +1,5 @@
import { context, type WithRefOpts, type ProviderOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { boolToEmptyStrOrUndef } from '$adom';
import {
@ -85,7 +86,7 @@ export class TagGroupProvider {
* The chain `prop → soma.prefs.getDir() → 'ltr'` runs in the WRAPPER
* (`activeDir`), so this only supplies the final fallback.
*/
readonly resolvedDir: Active<Direction> = readableActive(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir: Active<Direction> = readableActive(() => resolveDir(this.opts.dir, this.soma));
readonly resolvedAriaLabel: Active<string | undefined> = readableActive(() => {
if (this.opts.ariaLabelledby.current || this.labelId.current) return undefined;

@ -93,7 +93,7 @@
required: readableActive(() => required),
errorMessageId: readableActive(() => errorMessageId),
locale: readableActive(() => locale ?? soma?.langs.getLocale() ?? ''),
dir: activeDir(() => _dir, soma),
dir: activeDir(() => _dir),
hourCycle: readableActive(() => hourCycle)
});

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import type { Direction } from '../../types';
import { watch } from '$libs/reactive';
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
@ -117,7 +118,7 @@ export class TimeFieldProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: TimeFieldOpts;
readonly runtimePart: SomaRuntimePart;

@ -88,7 +88,7 @@
}
);
const resolvedDir = activeDir(() => dir, soma);
const assertedDir = activeDir(() => dir);
const state = TimePickerProvider.create({
id: readableActive(() => id),
@ -112,7 +112,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: readableActive(() => locale ?? soma?.langs.getLocale() ?? ''),
dir: resolvedDir,
dir: assertedDir,
hourStep: readableActive(() => hourStep),
minuteStep: readableActive(() => minuteStep),
secondStep: readableActive(() => secondStep),
@ -127,7 +127,7 @@
modal: readableActive(() => mode === 'modal'),
// The panel is portalled out of this subtree, so the popover is the only
// route this component's `dir` has into its own chrome.
dir: resolvedDir
dir: assertedDir
});
TimeFieldProvider.create({
@ -148,7 +148,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: readableActive(() => locale ?? soma?.langs.getLocale() ?? ''),
dir: resolvedDir,
dir: assertedDir,
errorMessageId: readableActive(() => errorMessageId)
});

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { watch } from '$libs/reactive';
import { context, type ProviderOpts } from '../../provider';
import { createAttrs } from '$uix/morfo';
@ -76,7 +77,7 @@ export class TimePickerProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: TimePickerOpts;
readonly runtimePart: SomaRuntimePart;

@ -74,7 +74,6 @@
hideTimeZone={provider.opts.hideTimeZone.current}
hourCycle={provider.opts.hourCycle.current}
locale={provider.opts.locale.current ?? ''}
dir={provider.opts.dir.current}
errorMessageId={provider.opts.errorMessageId.current}
{...(identity as Record<string, unknown>)}
>

@ -4,13 +4,11 @@
import { readableActive, writableActive, state } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { Time, type TimeValue, type TimeRange } from '$libs/days';
import { TimeRangeFieldProvider } from '../time-range-field-provider.svelte';
import type { TimeRangeFieldProviderProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -122,7 +120,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: readableActive(() => locale),
dir: activeDir(() => _dir, soma),
dir: activeDir(() => _dir),
errorMessageId: readableActive(() => errorMessageId)
});

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { state, type ActiveProps, type StateProps } from '$libs/reactive';
import type { Direction } from '../../types';
@ -63,7 +64,7 @@ export class TimeRangeFieldProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: TimeRangeFieldOpts;
readonly runtimePart: SomaRuntimePart;

@ -120,7 +120,7 @@
}
);
const resolvedDir = activeDir(() => dir, soma);
const assertedDir = activeDir(() => dir);
// ── Shared state via picker context ──────────────────────────────────────
const state_ = TimeRangePickerProvider.create({
@ -149,7 +149,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: readableActive(() => locale ?? soma?.langs.getLocale() ?? ''),
dir: resolvedDir,
dir: assertedDir,
hourStep: readableActive(() => hourStep),
minuteStep: readableActive(() => minuteStep),
secondStep: readableActive(() => secondStep),
@ -165,7 +165,7 @@
modal: readableActive(() => mode === 'modal'),
// The panel is portalled out of this subtree, so the popover is the only
// route this component's `dir` has into its own chrome.
dir: resolvedDir
dir: assertedDir
});
// ── TimeRangeField wired to shared value / config ────────────────────────
@ -192,7 +192,7 @@
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
locale: readableActive(() => locale),
dir: resolvedDir,
dir: assertedDir,
errorMessageId: readableActive(() => errorMessageId)
});

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { watch } from '$libs/reactive';
import { context, type ProviderOpts } from '../../provider';
import { createAttrs } from '$uix/morfo';
@ -81,7 +82,7 @@ export class TimeRangePickerProvider {
* is the raw `opts.dir.current`, so it is omitted when nobody asserted a
* direction and the element inherits instead of forcing `ltr`.
*/
readonly resolvedDir = $derived.by(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
readonly opts: TimeRangePickerOpts;
readonly runtimePart: SomaRuntimePart;

@ -67,7 +67,7 @@
readonly: readableActive(() => readonly),
orientation: readableActive(() => orientation),
loop: readableActive(() => loop),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
const mergedProps = $derived(mergeProps(restProps, state.props));

@ -1,4 +1,5 @@
import { context, type WithRefOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import type { ActiveProps, StateProps } from '$libs/reactive';
import type { Orientation, Direction, SomaMouseEvent, SomaKeyboardEvent } from '../../types';
@ -50,7 +51,7 @@ export class ToggleGroupProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by<Direction>(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by<Direction>(() => resolveDir(this.opts.dir, this.soma));
readonly opts: ToggleGroupOpts;
readonly runtimePart: SomaRuntimePart;

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { ToolbarProvider } from '../toolbar-provider.svelte';
import type { ToolbarProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -29,7 +27,7 @@
),
orientation: readableActive(() => orientation),
loop: readableActive(() => loop),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
const mergedProps = $derived(mergeProps(restProps, state.props));

@ -1,4 +1,5 @@
import { untrack } from 'svelte';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { context, type WithRefOpts } from '../../provider';
import { type ActiveProps, type StateProps } from '$libs/reactive';
@ -34,7 +35,7 @@ export class ToolbarProvider {
* raw, so it is omitted when nobody asserted a direction and the element
* inherits instead of forcing `ltr` onto an RTL page.
*/
readonly resolvedDir = $derived.by<Direction>(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir = $derived.by<Direction>(() => resolveDir(this.opts.dir, this.soma));
readonly runtimePart: SomaRuntimePart;
readonly soma: Soma;
readonly runtime: SomaRuntime;

@ -5,13 +5,11 @@
} from '$libs/reactive';
import { createId } from '$active-uix/id';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { TooltipProvider } from '../tooltip-provider.svelte';
import type { TooltipProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
id = createId(uid, 'tooltip'),
@ -42,7 +40,7 @@
disableHoverableContent: readableActive(() => disableHoverableContent),
disableCloseOnTriggerClick: readableActive(() => disableCloseOnTriggerClick),
disabled: readableActive(() => disabled),
dir: activeDir(() => dir, soma)
dir: activeDir(() => dir)
});
</script>

@ -4,14 +4,12 @@
writableActive
} from '$libs/reactive';
import { activeDir } from '../../../direction';
import { Soma } from '../../../core/soma.svelte';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { TreeGridProvider } from '../tree-grid-provider.svelte';
import type { TreeGridProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -59,7 +57,7 @@
typeahead: readableActive(() => typeahead),
typeaheadTimeout: readableActive(() => typeaheadTimeout),
disabled: readableActive(() => disabled),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
readonly: readableActive(() => readonly),
ariaLabel: readableActive(() => ariaLabel),
ariaLabelledby: readableActive(() => ariaLabelledby),

@ -1,4 +1,5 @@
import { context, type WithRefOpts, type ProviderOpts } from '../../provider';
import { resolveDir } from '../../direction';
import { createAttrs } from '$uix/morfo';
import { boolToEmptyStrOrUndef } from '$adom';
import {
@ -98,7 +99,7 @@ export class TreeGridProvider {
* `prop → soma.prefs.getDir() → 'ltr'` runs in the WRAPPER (`activeDir`),
* so this only supplies the final fallback.
*/
readonly resolvedDir: Active<Direction> = readableActive(() => this.opts.dir.current ?? 'ltr');
readonly resolvedDir: Active<Direction> = readableActive(() => resolveDir(this.opts.dir, this.soma));
readonly resolvedAriaLabel: Active<string | undefined> = readableActive(() =>
this.opts.ariaLabelledby.current

@ -3,12 +3,10 @@
import { activeDir } from '../../../direction';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { Soma } from '../../../core/soma.svelte';
import { TreeViewProvider } from '../tree-view-provider.svelte';
import type { TreeViewProps } from '../types';
const uid = $props.id();
const soma = Soma.get();
let {
ref = $bindable(null),
@ -49,7 +47,7 @@
}
),
selectionMode: readableActive(() => selectionMode),
dir: activeDir(() => dir, soma),
dir: activeDir(() => dir),
expandOnClick: readableActive(() => expandOnClick),
typeahead: readableActive(() => typeahead),
ariaLabel: readableActive(() => ariaLabel),

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

Loading…
Cancel
Save

Powered by TurnKey Linux.