From 01cc13d412873c89357df0687268b8d32302b20e Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 5 Aug 2026 02:42:14 +0200 Subject: [PATCH] =?UTF-8?q?feat(direction):=20el=20flip=20=E2=80=94=20el?= =?UTF-8?q?=20atributo=20es=20la=20afirmacion,=20y=20undefined=20ACTUA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 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 --- docs/canon/direction-contract.md | 49 +++++++++++++----- .../accordion/accordion-provider.svelte.ts | 3 +- .../accordion/components/accordion.svelte | 4 +- .../calendar/calendar-provider.svelte.ts | 3 +- .../calendar/components/calendar.svelte | 4 +- .../carousel/carousel-provider.svelte.ts | 3 +- .../carousel/components/carousel.svelte | 4 +- .../color-field-provider.svelte.ts | 3 +- .../color-field/components/color-field.svelte | 4 +- .../color-picker-provider.svelte.ts | 3 +- .../components/color-picker.svelte | 10 ++-- .../combobox/combobox-provider.svelte.ts | 3 +- .../combobox/components/combobox.svelte | 4 +- .../command/command-provider.svelte.ts | 7 +-- .../command/components/command.svelte | 4 +- .../components/context-menu.svelte | 4 +- .../context-menu-provider.svelte.ts | 7 +-- .../css-field/components/css-field.svelte | 4 +- .../css-field/css-field-provider.svelte.ts | 3 +- .../date-field/components/date-field.svelte | 2 +- .../date-field/date-field-provider.svelte.ts | 3 +- .../date-picker/components/date-picker.svelte | 8 +-- .../date-picker-provider.svelte.ts | 3 +- .../components/date-range-field-input.svelte | 1 - .../components/date-range-field.svelte | 4 +- .../date-range-field-provider.svelte.ts | 3 +- .../components/date-range-picker.svelte | 8 +-- .../date-range-picker-provider.svelte.ts | 3 +- .../drawer/components/drawer.svelte | 4 +- .../drawer/drawer-provider.svelte.ts | 3 +- .../dropdown-menu-sub-content.svelte | 31 +++++------ .../components/dropdown-menu.svelte | 4 +- .../dropdown-menu-provider.svelte.ts | 7 +-- .../components/emoji-picker.svelte | 8 ++- .../emoji-picker-provider.svelte.ts | 3 +- .../components/feed/components/feed.svelte | 4 +- .../float-panel/components/float-panel.svelte | 4 +- .../float-panel-provider.svelte.ts | 3 +- .../components/gradient-picker.svelte | 12 ++--- .../grid-list/components/grid-list.svelte | 4 +- .../grid-list/grid-list-provider.svelte.ts | 3 +- .../components/link-preview.svelte | 4 +- .../listbox/components/listbox.svelte | 4 +- .../listbox/listbox-provider.svelte.ts | 17 ++----- .../components/media-player.svelte | 4 +- .../media-player-provider.svelte.ts | 3 +- .../menubar/components/menubar.svelte | 4 +- .../menubar/menubar-provider.svelte.ts | 3 +- .../month-grid/components/month-grid.svelte | 4 +- .../month-grid/month-grid-provider.svelte.ts | 3 +- .../components/natural-time-picker.svelte | 7 ++- .../natural-time-picker-provider.svelte.ts | 3 +- .../nav-tree/components/nav-tree.svelte | 4 +- .../components/navigation-menu.svelte | 4 +- .../navigation-menu-provider.svelte.ts | 19 ++----- .../components/number-field.svelte | 4 +- .../number-field-provider.svelte.ts | 3 +- .../picker/components/picker.svelte | 12 ++--- .../popover/components/popover.svelte | 4 +- .../radio-group/components/radio-group.svelte | 4 +- .../radio-group-provider.svelte.ts | 3 +- .../components/range-calendar.svelte | 4 +- .../range-calendar-provider.svelte.ts | 3 +- .../components/rating-group.svelte | 4 +- .../rating-group-provider.svelte.ts | 3 +- .../scroll-area/components/scroll-area.svelte | 4 +- .../scroll-area-provider.svelte.ts | 3 +- .../select/components/select.svelte | 4 +- .../select/select-provider.svelte.ts | 3 +- .../sidebar/components/sidebar.svelte | 4 +- .../slider/components/slider.svelte | 4 +- .../slider/slider-provider.svelte.ts | 3 +- .../splitter/components/splitter.svelte | 4 +- .../splitter/splitter-provider.svelte.ts | 3 +- .../stepper/components/stepper.svelte | 4 +- .../stepper/stepper-provider.svelte.ts | 3 +- .../switch/components/switch.svelte | 4 +- .../components/table/components/table.svelte | 4 +- .../components/table/table-provider.svelte.ts | 3 +- .../components/tabs/components/tabs.svelte | 4 +- .../components/tabs/tabs-provider.svelte.ts | 3 +- .../tag-group/components/tag-group.svelte | 4 +- .../tag-group/tag-group-provider.svelte.ts | 3 +- .../time-field/components/time-field.svelte | 2 +- .../time-field/time-field-provider.svelte.ts | 3 +- .../time-picker/components/time-picker.svelte | 8 +-- .../time-picker-provider.svelte.ts | 3 +- .../components/time-range-field-input.svelte | 1 - .../components/time-range-field.svelte | 4 +- .../time-range-field-provider.svelte.ts | 3 +- .../components/time-range-picker.svelte | 8 +-- .../time-range-picker-provider.svelte.ts | 3 +- .../components/toggle-group.svelte | 2 +- .../toggle-group-provider.svelte.ts | 3 +- .../toolbar/components/toolbar.svelte | 4 +- .../toolbar/toolbar-provider.svelte.ts | 3 +- .../tooltip/components/tooltip.svelte | 4 +- .../tree-grid/components/tree-grid.svelte | 4 +- .../tree-grid/tree-grid-provider.svelte.ts | 3 +- .../tree-view/components/tree-view.svelte | 4 +- .../tree-view/tree-view-provider.svelte.ts | 3 +- .../components/virtual-grid.svelte | 4 +- .../virtual-grid-provider.svelte.ts | 3 +- .../components/virtual-list.svelte | 4 +- .../virtual-list-provider.svelte.ts | 3 +- .../year-grid/components/year-grid.svelte | 4 +- .../year-grid/year-grid-provider.svelte.ts | 3 +- src/uix/soma/direction.ts | 51 +++++++++++++------ .../soma/test/direction-harness-child.svelte | 2 +- src/uix/soma/test/direction-harness.svelte | 2 +- 110 files changed, 277 insertions(+), 306 deletions(-) diff --git a/docs/canon/direction-contract.md b/docs/canon/direction-contract.md index 3376111d0..8cad53e15 100644 --- a/docs/canon/direction-contract.md +++ b/docs/canon/direction-contract.md @@ -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 ``, 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` — it deliberately stops before the default. -The `'ltr'` tail lives per-provider: +`Active` — 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 `` — together with -``, 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 +`` — together with ``, 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 `` (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 `` (`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 `` of the FIRST render +therefore belongs to `app.html` or the server (the react-aria model): an app +whose default direction is RTL sets `` 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). --- diff --git a/src/uix/soma/components/accordion/accordion-provider.svelte.ts b/src/uix/soma/components/accordion/accordion-provider.svelte.ts index 36fb4cf4a..874227bf3 100644 --- a/src/uix/soma/components/accordion/accordion-provider.svelte.ts +++ b/src/uix/soma/components/accordion/accordion-provider.svelte.ts @@ -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(() => this.opts.dir.current ?? 'ltr'); + readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma)); readonly opts: AccordionOpts; readonly runtimePart: SomaRuntimePart; diff --git a/src/uix/soma/components/accordion/components/accordion.svelte b/src/uix/soma/components/accordion/components/accordion.svelte index e65ae53cd..f0bcf557d 100644 --- a/src/uix/soma/components/accordion/components/accordion.svelte +++ b/src/uix/soma/components/accordion/components/accordion.svelte @@ -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) }); diff --git a/src/uix/soma/components/calendar/calendar-provider.svelte.ts b/src/uix/soma/components/calendar/calendar-provider.svelte.ts index a3c85d242..3706421f9 100644 --- a/src/uix/soma/components/calendar/calendar-provider.svelte.ts +++ b/src/uix/soma/components/calendar/calendar-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/calendar/components/calendar.svelte b/src/uix/soma/components/calendar/components/calendar.svelte index 3c45814a4..d1dff6bc8 100644 --- a/src/uix/soma/components/calendar/components/calendar.svelte +++ b/src/uix/soma/components/calendar/components/calendar.svelte @@ -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 ?? '') }); diff --git a/src/uix/soma/components/carousel/carousel-provider.svelte.ts b/src/uix/soma/components/carousel/carousel-provider.svelte.ts index 27b2be830..72e75d1c3 100644 --- a/src/uix/soma/components/carousel/carousel-provider.svelte.ts +++ b/src/uix/soma/components/carousel/carousel-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/carousel/components/carousel.svelte b/src/uix/soma/components/carousel/components/carousel.svelte index 93485fe78..4ed116758 100644 --- a/src/uix/soma/components/carousel/components/carousel.svelte +++ b/src/uix/soma/components/carousel/components/carousel.svelte @@ -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) }); diff --git a/src/uix/soma/components/color-field/color-field-provider.svelte.ts b/src/uix/soma/components/color-field/color-field-provider.svelte.ts index 64dec6dab..462394ab1 100644 --- a/src/uix/soma/components/color-field/color-field-provider.svelte.ts +++ b/src/uix/soma/components/color-field/color-field-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/color-field/components/color-field.svelte b/src/uix/soma/components/color-field/components/color-field.svelte index d5719d503..20185f8d8 100644 --- a/src/uix/soma/components/color-field/components/color-field.svelte +++ b/src/uix/soma/components/color-field/components/color-field.svelte @@ -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)); diff --git a/src/uix/soma/components/color-picker/color-picker-provider.svelte.ts b/src/uix/soma/components/color-picker/color-picker-provider.svelte.ts index d2d4cddc6..62c02252e 100644 --- a/src/uix/soma/components/color-picker/color-picker-provider.svelte.ts +++ b/src/uix/soma/components/color-picker/color-picker-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/color-picker/components/color-picker.svelte b/src/uix/soma/components/color-picker/components/color-picker.svelte index 95fb65848..673ea95c3 100644 --- a/src/uix/soma/components/color-picker/components/color-picker.svelte +++ b/src/uix/soma/components/color-picker/components/color-picker.svelte @@ -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)); diff --git a/src/uix/soma/components/combobox/combobox-provider.svelte.ts b/src/uix/soma/components/combobox/combobox-provider.svelte.ts index 4d99f6d59..47077ce1f 100644 --- a/src/uix/soma/components/combobox/combobox-provider.svelte.ts +++ b/src/uix/soma/components/combobox/combobox-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/combobox/components/combobox.svelte b/src/uix/soma/components/combobox/components/combobox.svelte index 8b9283fe3..f20fa92ae 100644 --- a/src/uix/soma/components/combobox/components/combobox.svelte +++ b/src/uix/soma/components/combobox/components/combobox.svelte @@ -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) }); diff --git a/src/uix/soma/components/command/command-provider.svelte.ts b/src/uix/soma/components/command/command-provider.svelte.ts index c5611ec0c..c08b21457 100644 --- a/src/uix/soma/components/command/command-provider.svelte.ts +++ b/src/uix/soma/components/command/command-provider.svelte.ts @@ -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) ); } diff --git a/src/uix/soma/components/command/components/command.svelte b/src/uix/soma/components/command/components/command.svelte index 44a06be5f..fa13ba9ad 100644 --- a/src/uix/soma/components/command/components/command.svelte +++ b/src/uix/soma/components/command/components/command.svelte @@ -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) }); diff --git a/src/uix/soma/components/context-menu/components/context-menu.svelte b/src/uix/soma/components/context-menu/components/context-menu.svelte index 08daf9aa0..899562204 100644 --- a/src/uix/soma/components/context-menu/components/context-menu.svelte +++ b/src/uix/soma/components/context-menu/components/context-menu.svelte @@ -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) }); diff --git a/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts b/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts index 68f03aaea..9035fa126 100644 --- a/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts +++ b/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts @@ -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) => { 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'); diff --git a/src/uix/soma/components/css-field/components/css-field.svelte b/src/uix/soma/components/css-field/components/css-field.svelte index 1292506dc..4b7c374ca 100644 --- a/src/uix/soma/components/css-field/components/css-field.svelte +++ b/src/uix/soma/components/css-field/components/css-field.svelte @@ -1,14 +1,12 @@ diff --git a/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts b/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts index c6d166bfd..7bb809a91 100644 --- a/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts +++ b/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts @@ -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) => { 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'); diff --git a/src/uix/soma/components/emoji-picker/components/emoji-picker.svelte b/src/uix/soma/components/emoji-picker/components/emoji-picker.svelte index d7ccd17cf..3ecc48560 100644 --- a/src/uix/soma/components/emoji-picker/components/emoji-picker.svelte +++ b/src/uix/soma/components/emoji-picker/components/emoji-picker.svelte @@ -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)); diff --git a/src/uix/soma/components/emoji-picker/emoji-picker-provider.svelte.ts b/src/uix/soma/components/emoji-picker/emoji-picker-provider.svelte.ts index a0a23785b..4da8c2413 100644 --- a/src/uix/soma/components/emoji-picker/emoji-picker-provider.svelte.ts +++ b/src/uix/soma/components/emoji-picker/emoji-picker-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/feed/components/feed.svelte b/src/uix/soma/components/feed/components/feed.svelte index 4a9044201..02c110226 100644 --- a/src/uix/soma/components/feed/components/feed.svelte +++ b/src/uix/soma/components/feed/components/feed.svelte @@ -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)); diff --git a/src/uix/soma/components/float-panel/components/float-panel.svelte b/src/uix/soma/components/float-panel/components/float-panel.svelte index b8fe379ad..4b0d50c32 100644 --- a/src/uix/soma/components/float-panel/components/float-panel.svelte +++ b/src/uix/soma/components/float-panel/components/float-panel.svelte @@ -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) }); diff --git a/src/uix/soma/components/float-panel/float-panel-provider.svelte.ts b/src/uix/soma/components/float-panel/float-panel-provider.svelte.ts index 407ef72d6..e316c96e0 100644 --- a/src/uix/soma/components/float-panel/float-panel-provider.svelte.ts +++ b/src/uix/soma/components/float-panel/float-panel-provider.svelte.ts @@ -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('FloatPanel'); static get(): FloatPanelProvider | undefined { diff --git a/src/uix/soma/components/gradient-picker/components/gradient-picker.svelte b/src/uix/soma/components/gradient-picker/components/gradient-picker.svelte index fd699a2ed..2c18cc8ce 100644 --- a/src/uix/soma/components/gradient-picker/components/gradient-picker.svelte +++ b/src/uix/soma/components/gradient-picker/components/gradient-picker.svelte @@ -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)); diff --git a/src/uix/soma/components/grid-list/components/grid-list.svelte b/src/uix/soma/components/grid-list/components/grid-list.svelte index 88911962f..653095e4c 100644 --- a/src/uix/soma/components/grid-list/components/grid-list.svelte +++ b/src/uix/soma/components/grid-list/components/grid-list.svelte @@ -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), diff --git a/src/uix/soma/components/grid-list/grid-list-provider.svelte.ts b/src/uix/soma/components/grid-list/grid-list-provider.svelte.ts index 34c544f96..337d938ca 100644 --- a/src/uix/soma/components/grid-list/grid-list-provider.svelte.ts +++ b/src/uix/soma/components/grid-list/grid-list-provider.svelte.ts @@ -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 = readableActive( - () => this.opts.dir.current ?? 'ltr' + () => resolveDir(this.opts.dir, this.soma) ); readonly resolvedAriaLabel: Active = readableActive(() => diff --git a/src/uix/soma/components/link-preview/components/link-preview.svelte b/src/uix/soma/components/link-preview/components/link-preview.svelte index 888c73a69..1255b9b96 100644 --- a/src/uix/soma/components/link-preview/components/link-preview.svelte +++ b/src/uix/soma/components/link-preview/components/link-preview.svelte @@ -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) }); diff --git a/src/uix/soma/components/listbox/components/listbox.svelte b/src/uix/soma/components/listbox/components/listbox.svelte index f35dafc2b..9d1ed9675 100644 --- a/src/uix/soma/components/listbox/components/listbox.svelte +++ b/src/uix/soma/components/listbox/components/listbox.svelte @@ -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), diff --git a/src/uix/soma/components/listbox/listbox-provider.svelte.ts b/src/uix/soma/components/listbox/listbox-provider.svelte.ts index cff2523c8..d52c15039 100644 --- a/src/uix/soma/components/listbox/listbox-provider.svelte.ts +++ b/src/uix/soma/components/listbox/listbox-provider.svelte.ts @@ -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 = readableActive(() => this.assertedDir.current ?? 'ltr'); + readonly resolvedDir: Active = 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 = readableActive( - () => this.opts.dir.current - ); readonly resolvedAriaLabel: Active = readableActive(() => this.opts.ariaLabelledby.current diff --git a/src/uix/soma/components/media-player/components/media-player.svelte b/src/uix/soma/components/media-player/components/media-player.svelte index ec38b49f0..4f2738196 100644 --- a/src/uix/soma/components/media-player/components/media-player.svelte +++ b/src/uix/soma/components/media-player/components/media-player.svelte @@ -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), diff --git a/src/uix/soma/components/media-player/media-player-provider.svelte.ts b/src/uix/soma/components/media-player/media-player-provider.svelte.ts index d97b5cfea..eb15a1389 100644 --- a/src/uix/soma/components/media-player/media-player-provider.svelte.ts +++ b/src/uix/soma/components/media-player/media-player-provider.svelte.ts @@ -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('MediaPlayer'); static get(): MediaPlayerProvider | undefined { diff --git a/src/uix/soma/components/menubar/components/menubar.svelte b/src/uix/soma/components/menubar/components/menubar.svelte index f08183ade..abbf685e6 100644 --- a/src/uix/soma/components/menubar/components/menubar.svelte +++ b/src/uix/soma/components/menubar/components/menubar.svelte @@ -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) }); diff --git a/src/uix/soma/components/menubar/menubar-provider.svelte.ts b/src/uix/soma/components/menubar/menubar-provider.svelte.ts index 9f9c38b42..df5fc26ff 100644 --- a/src/uix/soma/components/menubar/menubar-provider.svelte.ts +++ b/src/uix/soma/components/menubar/menubar-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/month-grid/components/month-grid.svelte b/src/uix/soma/components/month-grid/components/month-grid.svelte index 9339313fd..4e4b3158b 100644 --- a/src/uix/soma/components/month-grid/components/month-grid.svelte +++ b/src/uix/soma/components/month-grid/components/month-grid.svelte @@ -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) diff --git a/src/uix/soma/components/month-grid/month-grid-provider.svelte.ts b/src/uix/soma/components/month-grid/month-grid-provider.svelte.ts index 73c4fab3c..896379081 100644 --- a/src/uix/soma/components/month-grid/month-grid-provider.svelte.ts +++ b/src/uix/soma/components/month-grid/month-grid-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/natural-time-picker/components/natural-time-picker.svelte b/src/uix/soma/components/natural-time-picker/components/natural-time-picker.svelte index 5d4e054a4..6eecf35b9 100644 --- a/src/uix/soma/components/natural-time-picker/components/natural-time-picker.svelte +++ b/src/uix/soma/components/natural-time-picker/components/natural-time-picker.svelte @@ -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({ 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)); diff --git a/src/uix/soma/components/natural-time-picker/natural-time-picker-provider.svelte.ts b/src/uix/soma/components/natural-time-picker/natural-time-picker-provider.svelte.ts index 086c1d174..22e2f381e 100644 --- a/src/uix/soma/components/natural-time-picker/natural-time-picker-provider.svelte.ts +++ b/src/uix/soma/components/natural-time-picker/natural-time-picker-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/nav-tree/components/nav-tree.svelte b/src/uix/soma/components/nav-tree/components/nav-tree.svelte index f92a214a2..f54665f74 100644 --- a/src/uix/soma/components/nav-tree/components/nav-tree.svelte +++ b/src/uix/soma/components/nav-tree/components/nav-tree.svelte @@ -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)); diff --git a/src/uix/soma/components/navigation-menu/components/navigation-menu.svelte b/src/uix/soma/components/navigation-menu/components/navigation-menu.svelte index 81570a2f6..f6546e4b7 100644 --- a/src/uix/soma/components/navigation-menu/components/navigation-menu.svelte +++ b/src/uix/soma/components/navigation-menu/components/navigation-menu.svelte @@ -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), diff --git a/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts b/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts index 244123488..02c119b93 100644 --- a/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts +++ b/src/uix/soma/components/navigation-menu/navigation-menu-provider.svelte.ts @@ -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 = readableActive( - () => this.assertedDir.current ?? 'ltr' - ); + readonly resolvedDir: Active = 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 = readableActive( - () => this.opts.dir.current - ); readonly resolvedAriaLabel: Active = readableActive( () => diff --git a/src/uix/soma/components/number-field/components/number-field.svelte b/src/uix/soma/components/number-field/components/number-field.svelte index 3946605c4..dce1d3230 100644 --- a/src/uix/soma/components/number-field/components/number-field.svelte +++ b/src/uix/soma/components/number-field/components/number-field.svelte @@ -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), diff --git a/src/uix/soma/components/number-field/number-field-provider.svelte.ts b/src/uix/soma/components/number-field/number-field-provider.svelte.ts index 22519d532..6ed96d011 100644 --- a/src/uix/soma/components/number-field/number-field-provider.svelte.ts +++ b/src/uix/soma/components/number-field/number-field-provider.svelte.ts @@ -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. diff --git a/src/uix/soma/components/picker/components/picker.svelte b/src/uix/soma/components/picker/components/picker.svelte index 5f7a7f6cd..bc1bdc483 100644 --- a/src/uix/soma/components/picker/components/picker.svelte +++ b/src/uix/soma/components/picker/components/picker.svelte @@ -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)); diff --git a/src/uix/soma/components/popover/components/popover.svelte b/src/uix/soma/components/popover/components/popover.svelte index 6cccd33a2..3fae4a393 100644 --- a/src/uix/soma/components/popover/components/popover.svelte +++ b/src/uix/soma/components/popover/components/popover.svelte @@ -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) }); diff --git a/src/uix/soma/components/radio-group/components/radio-group.svelte b/src/uix/soma/components/radio-group/components/radio-group.svelte index 6a66be1af..9b4a61727 100644 --- a/src/uix/soma/components/radio-group/components/radio-group.svelte +++ b/src/uix/soma/components/radio-group/components/radio-group.svelte @@ -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) diff --git a/src/uix/soma/components/radio-group/radio-group-provider.svelte.ts b/src/uix/soma/components/radio-group/radio-group-provider.svelte.ts index 2b21cf07e..e7612c874 100644 --- a/src/uix/soma/components/radio-group/radio-group-provider.svelte.ts +++ b/src/uix/soma/components/radio-group/radio-group-provider.svelte.ts @@ -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(() => this.opts.dir.current ?? 'ltr'); + readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma)); readonly opts: RadioGroupOpts; readonly runtimePart: SomaRuntimePart; diff --git a/src/uix/soma/components/range-calendar/components/range-calendar.svelte b/src/uix/soma/components/range-calendar/components/range-calendar.svelte index b7d3bf3b5..71d7e0228 100644 --- a/src/uix/soma/components/range-calendar/components/range-calendar.svelte +++ b/src/uix/soma/components/range-calendar/components/range-calendar.svelte @@ -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 ?? '') }); diff --git a/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts b/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts index 9c7641c9f..2df450c76 100644 --- a/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts +++ b/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/rating-group/components/rating-group.svelte b/src/uix/soma/components/rating-group/components/rating-group.svelte index 43fde7368..58969328b 100644 --- a/src/uix/soma/components/rating-group/components/rating-group.svelte +++ b/src/uix/soma/components/rating-group/components/rating-group.svelte @@ -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), diff --git a/src/uix/soma/components/rating-group/rating-group-provider.svelte.ts b/src/uix/soma/components/rating-group/rating-group-provider.svelte.ts index d3ac59c7c..07814d7cf 100644 --- a/src/uix/soma/components/rating-group/rating-group-provider.svelte.ts +++ b/src/uix/soma/components/rating-group/rating-group-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/scroll-area/components/scroll-area.svelte b/src/uix/soma/components/scroll-area/components/scroll-area.svelte index b577a2e80..d5678c50f 100644 --- a/src/uix/soma/components/scroll-area/components/scroll-area.svelte +++ b/src/uix/soma/components/scroll-area/components/scroll-area.svelte @@ -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)); diff --git a/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts b/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts index 399071a3e..6ac6ad4fe 100644 --- a/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts +++ b/src/uix/soma/components/scroll-area/scroll-area-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/select/components/select.svelte b/src/uix/soma/components/select/components/select.svelte index e98668d7b..cd56e0abb 100644 --- a/src/uix/soma/components/select/components/select.svelte +++ b/src/uix/soma/components/select/components/select.svelte @@ -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) }); diff --git a/src/uix/soma/components/select/select-provider.svelte.ts b/src/uix/soma/components/select/select-provider.svelte.ts index 31d282547..3a16b58c2 100644 --- a/src/uix/soma/components/select/select-provider.svelte.ts +++ b/src/uix/soma/components/select/select-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/sidebar/components/sidebar.svelte b/src/uix/soma/components/sidebar/components/sidebar.svelte index 2656c5dba..678376ac1 100644 --- a/src/uix/soma/components/sidebar/components/sidebar.svelte +++ b/src/uix/soma/components/sidebar/components/sidebar.svelte @@ -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)); diff --git a/src/uix/soma/components/slider/components/slider.svelte b/src/uix/soma/components/slider/components/slider.svelte index d30abc776..9ac9334fa 100644 --- a/src/uix/soma/components/slider/components/slider.svelte +++ b/src/uix/soma/components/slider/components/slider.svelte @@ -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), diff --git a/src/uix/soma/components/slider/slider-provider.svelte.ts b/src/uix/soma/components/slider/slider-provider.svelte.ts index 0a0e4f75c..f149c0361 100644 --- a/src/uix/soma/components/slider/slider-provider.svelte.ts +++ b/src/uix/soma/components/slider/slider-provider.svelte.ts @@ -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(() => this.opts.dir.current ?? 'ltr'); + readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma)); readonly isDisabled = $derived.by( () => this.opts.disabled.current || (this.field?.isDisabled ?? false) diff --git a/src/uix/soma/components/splitter/components/splitter.svelte b/src/uix/soma/components/splitter/components/splitter.svelte index 671a5384e..79e4ef9cf 100644 --- a/src/uix/soma/components/splitter/components/splitter.svelte +++ b/src/uix/soma/components/splitter/components/splitter.svelte @@ -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, diff --git a/src/uix/soma/components/splitter/splitter-provider.svelte.ts b/src/uix/soma/components/splitter/splitter-provider.svelte.ts index 98a8f81c6..2ec8e7718 100644 --- a/src/uix/soma/components/splitter/splitter-provider.svelte.ts +++ b/src/uix/soma/components/splitter/splitter-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/stepper/components/stepper.svelte b/src/uix/soma/components/stepper/components/stepper.svelte index d83d68e32..a4e1ff0ca 100644 --- a/src/uix/soma/components/stepper/components/stepper.svelte +++ b/src/uix/soma/components/stepper/components/stepper.svelte @@ -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) }); diff --git a/src/uix/soma/components/stepper/stepper-provider.svelte.ts b/src/uix/soma/components/stepper/stepper-provider.svelte.ts index fd53b722a..897c53658 100644 --- a/src/uix/soma/components/stepper/stepper-provider.svelte.ts +++ b/src/uix/soma/components/stepper/stepper-provider.svelte.ts @@ -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(() => this.opts.dir.current ?? 'ltr'); + readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma)); readonly opts: StepperOpts; readonly runtimePart: SomaRuntimePart; diff --git a/src/uix/soma/components/switch/components/switch.svelte b/src/uix/soma/components/switch/components/switch.svelte index c40a0a3cc..798edeb25 100644 --- a/src/uix/soma/components/switch/components/switch.svelte +++ b/src/uix/soma/components/switch/components/switch.svelte @@ -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[0], 'dir'>), - dir: activeDir(() => dir, soma) + dir: activeDir(() => dir) }); const mergedProps = $derived(mergeProps(restProps, state.props)); diff --git a/src/uix/soma/components/table/components/table.svelte b/src/uix/soma/components/table/components/table.svelte index 30894fe12..af65daf77 100644 --- a/src/uix/soma/components/table/components/table.svelte +++ b/src/uix/soma/components/table/components/table.svelte @@ -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)); diff --git a/src/uix/soma/components/table/table-provider.svelte.ts b/src/uix/soma/components/table/table-provider.svelte.ts index 786c77194..e9195c00e 100644 --- a/src/uix/soma/components/table/table-provider.svelte.ts +++ b/src/uix/soma/components/table/table-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/tabs/components/tabs.svelte b/src/uix/soma/components/tabs/components/tabs.svelte index 8a64b7aef..47a48252b 100644 --- a/src/uix/soma/components/tabs/components/tabs.svelte +++ b/src/uix/soma/components/tabs/components/tabs.svelte @@ -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)); diff --git a/src/uix/soma/components/tabs/tabs-provider.svelte.ts b/src/uix/soma/components/tabs/tabs-provider.svelte.ts index 2e9a0cf0c..888e93406 100644 --- a/src/uix/soma/components/tabs/tabs-provider.svelte.ts +++ b/src/uix/soma/components/tabs/tabs-provider.svelte.ts @@ -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(() => this.opts.dir.current ?? 'ltr'); + readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma)); readonly opts: TabsOpts; readonly runtimePart: SomaRuntimePart; diff --git a/src/uix/soma/components/tag-group/components/tag-group.svelte b/src/uix/soma/components/tag-group/components/tag-group.svelte index 4e192cf3b..32f6fa9e2 100644 --- a/src/uix/soma/components/tag-group/components/tag-group.svelte +++ b/src/uix/soma/components/tag-group/components/tag-group.svelte @@ -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), diff --git a/src/uix/soma/components/tag-group/tag-group-provider.svelte.ts b/src/uix/soma/components/tag-group/tag-group-provider.svelte.ts index 46c742854..52cf899b9 100644 --- a/src/uix/soma/components/tag-group/tag-group-provider.svelte.ts +++ b/src/uix/soma/components/tag-group/tag-group-provider.svelte.ts @@ -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 = readableActive(() => this.opts.dir.current ?? 'ltr'); + readonly resolvedDir: Active = readableActive(() => resolveDir(this.opts.dir, this.soma)); readonly resolvedAriaLabel: Active = readableActive(() => { if (this.opts.ariaLabelledby.current || this.labelId.current) return undefined; diff --git a/src/uix/soma/components/time-field/components/time-field.svelte b/src/uix/soma/components/time-field/components/time-field.svelte index 9d6b6efa6..3aedf8310 100644 --- a/src/uix/soma/components/time-field/components/time-field.svelte +++ b/src/uix/soma/components/time-field/components/time-field.svelte @@ -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) }); diff --git a/src/uix/soma/components/time-field/time-field-provider.svelte.ts b/src/uix/soma/components/time-field/time-field-provider.svelte.ts index d0042fe46..ff10daac4 100644 --- a/src/uix/soma/components/time-field/time-field-provider.svelte.ts +++ b/src/uix/soma/components/time-field/time-field-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/time-picker/components/time-picker.svelte b/src/uix/soma/components/time-picker/components/time-picker.svelte index addc07a9c..cd28aaa71 100644 --- a/src/uix/soma/components/time-picker/components/time-picker.svelte +++ b/src/uix/soma/components/time-picker/components/time-picker.svelte @@ -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) }); diff --git a/src/uix/soma/components/time-picker/time-picker-provider.svelte.ts b/src/uix/soma/components/time-picker/time-picker-provider.svelte.ts index fdea2f21f..0f78b94c2 100644 --- a/src/uix/soma/components/time-picker/time-picker-provider.svelte.ts +++ b/src/uix/soma/components/time-picker/time-picker-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/time-range-field/components/time-range-field-input.svelte b/src/uix/soma/components/time-range-field/components/time-range-field-input.svelte index 034a0889f..7ac90984a 100644 --- a/src/uix/soma/components/time-range-field/components/time-range-field-input.svelte +++ b/src/uix/soma/components/time-range-field/components/time-range-field-input.svelte @@ -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)} > diff --git a/src/uix/soma/components/time-range-field/components/time-range-field.svelte b/src/uix/soma/components/time-range-field/components/time-range-field.svelte index baaf0490b..e5cd893d6 100644 --- a/src/uix/soma/components/time-range-field/components/time-range-field.svelte +++ b/src/uix/soma/components/time-range-field/components/time-range-field.svelte @@ -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) }); diff --git a/src/uix/soma/components/time-range-field/time-range-field-provider.svelte.ts b/src/uix/soma/components/time-range-field/time-range-field-provider.svelte.ts index 8b96f3f1e..173897b4a 100644 --- a/src/uix/soma/components/time-range-field/time-range-field-provider.svelte.ts +++ b/src/uix/soma/components/time-range-field/time-range-field-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/time-range-picker/components/time-range-picker.svelte b/src/uix/soma/components/time-range-picker/components/time-range-picker.svelte index beb9099b1..a57a8b469 100644 --- a/src/uix/soma/components/time-range-picker/components/time-range-picker.svelte +++ b/src/uix/soma/components/time-range-picker/components/time-range-picker.svelte @@ -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) }); diff --git a/src/uix/soma/components/time-range-picker/time-range-picker-provider.svelte.ts b/src/uix/soma/components/time-range-picker/time-range-picker-provider.svelte.ts index 77d061c76..3ad3f1202 100644 --- a/src/uix/soma/components/time-range-picker/time-range-picker-provider.svelte.ts +++ b/src/uix/soma/components/time-range-picker/time-range-picker-provider.svelte.ts @@ -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; diff --git a/src/uix/soma/components/toggle-group/components/toggle-group.svelte b/src/uix/soma/components/toggle-group/components/toggle-group.svelte index 91512c3a3..4a450df75 100644 --- a/src/uix/soma/components/toggle-group/components/toggle-group.svelte +++ b/src/uix/soma/components/toggle-group/components/toggle-group.svelte @@ -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)); diff --git a/src/uix/soma/components/toggle-group/toggle-group-provider.svelte.ts b/src/uix/soma/components/toggle-group/toggle-group-provider.svelte.ts index c3c76a83f..3b02457dc 100644 --- a/src/uix/soma/components/toggle-group/toggle-group-provider.svelte.ts +++ b/src/uix/soma/components/toggle-group/toggle-group-provider.svelte.ts @@ -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(() => this.opts.dir.current ?? 'ltr'); + readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma)); readonly opts: ToggleGroupOpts; readonly runtimePart: SomaRuntimePart; diff --git a/src/uix/soma/components/toolbar/components/toolbar.svelte b/src/uix/soma/components/toolbar/components/toolbar.svelte index 4189d9187..d9689296f 100644 --- a/src/uix/soma/components/toolbar/components/toolbar.svelte +++ b/src/uix/soma/components/toolbar/components/toolbar.svelte @@ -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)); diff --git a/src/uix/soma/components/toolbar/toolbar-provider.svelte.ts b/src/uix/soma/components/toolbar/toolbar-provider.svelte.ts index c5aff416e..8cc4bcaa7 100644 --- a/src/uix/soma/components/toolbar/toolbar-provider.svelte.ts +++ b/src/uix/soma/components/toolbar/toolbar-provider.svelte.ts @@ -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(() => this.opts.dir.current ?? 'ltr'); + readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma)); readonly runtimePart: SomaRuntimePart; readonly soma: Soma; readonly runtime: SomaRuntime; diff --git a/src/uix/soma/components/tooltip/components/tooltip.svelte b/src/uix/soma/components/tooltip/components/tooltip.svelte index f6c2952a4..0cd96f7e8 100644 --- a/src/uix/soma/components/tooltip/components/tooltip.svelte +++ b/src/uix/soma/components/tooltip/components/tooltip.svelte @@ -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) }); diff --git a/src/uix/soma/components/tree-grid/components/tree-grid.svelte b/src/uix/soma/components/tree-grid/components/tree-grid.svelte index c33572a9d..0d8418ad3 100644 --- a/src/uix/soma/components/tree-grid/components/tree-grid.svelte +++ b/src/uix/soma/components/tree-grid/components/tree-grid.svelte @@ -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), diff --git a/src/uix/soma/components/tree-grid/tree-grid-provider.svelte.ts b/src/uix/soma/components/tree-grid/tree-grid-provider.svelte.ts index c08ca2676..af9e4224d 100644 --- a/src/uix/soma/components/tree-grid/tree-grid-provider.svelte.ts +++ b/src/uix/soma/components/tree-grid/tree-grid-provider.svelte.ts @@ -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 = readableActive(() => this.opts.dir.current ?? 'ltr'); + readonly resolvedDir: Active = readableActive(() => resolveDir(this.opts.dir, this.soma)); readonly resolvedAriaLabel: Active = readableActive(() => this.opts.ariaLabelledby.current diff --git a/src/uix/soma/components/tree-view/components/tree-view.svelte b/src/uix/soma/components/tree-view/components/tree-view.svelte index a27732e18..25b72559a 100644 --- a/src/uix/soma/components/tree-view/components/tree-view.svelte +++ b/src/uix/soma/components/tree-view/components/tree-view.svelte @@ -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), diff --git a/src/uix/soma/components/tree-view/tree-view-provider.svelte.ts b/src/uix/soma/components/tree-view/tree-view-provider.svelte.ts index cdb8ef5ca..0dcf10bf1 100644 --- a/src/uix/soma/components/tree-view/tree-view-provider.svelte.ts +++ b/src/uix/soma/components/tree-view/tree-view-provider.svelte.ts @@ -1,4 +1,5 @@ import { context, type WithRefOpts } from '../../provider'; +import { resolveDir } from '../../direction'; import { createAttrs } from '$uix/morfo'; import { boolToEmptyStrOrUndef } from '$adom'; import { state, type Active, type ActiveProps, type StateProps } from '$libs/reactive'; @@ -41,7 +42,7 @@ export class TreeViewProvider { * 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: TreeViewOpts; readonly runtimePart: SomaRuntimePart; diff --git a/src/uix/soma/components/virtual-grid/components/virtual-grid.svelte b/src/uix/soma/components/virtual-grid/components/virtual-grid.svelte index cb08522f7..0d61ba817 100644 --- a/src/uix/soma/components/virtual-grid/components/virtual-grid.svelte +++ b/src/uix/soma/components/virtual-grid/components/virtual-grid.svelte @@ -6,12 +6,10 @@ import { mergeProps } from '../../../props'; import { createId } from '$active-uix/id'; import { activeDir } from '../../../direction'; - import { Soma } from '../../../core/soma.svelte'; import { VirtualGridProvider } from '../virtual-grid-provider.svelte'; import type { VirtualGridProps } from '../types'; const uid = $props.id(); - const soma = Soma.get(); let { ref = $bindable(null), @@ -44,7 +42,7 @@ columnOverscan: readableActive(() => columnOverscan), getRowKey: readableActive(() => getRowKey), getColumnKey: readableActive(() => getColumnKey), - dir: activeDir(() => dir, soma) + dir: activeDir(() => dir) }); const mergedProps = $derived(mergeProps(restProps, state.props)); diff --git a/src/uix/soma/components/virtual-grid/virtual-grid-provider.svelte.ts b/src/uix/soma/components/virtual-grid/virtual-grid-provider.svelte.ts index db78b7180..313423c89 100644 --- a/src/uix/soma/components/virtual-grid/virtual-grid-provider.svelte.ts +++ b/src/uix/soma/components/virtual-grid/virtual-grid-provider.svelte.ts @@ -1,4 +1,5 @@ import { context, type WithRefOpts } from '../../provider'; +import { resolveDir } from '../../direction'; import { type ActiveProps } from '$libs/reactive'; import type { Direction } from '../../types'; import type { ScrollToCellOptions, VirtualCell } from './types'; @@ -79,7 +80,7 @@ export class VirtualGridProvider { * `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 opts: VirtualGridOpts; readonly runtimePart: SomaRuntimePart; diff --git a/src/uix/soma/components/virtual-list/components/virtual-list.svelte b/src/uix/soma/components/virtual-list/components/virtual-list.svelte index 0cd68992a..6c936e5fe 100644 --- a/src/uix/soma/components/virtual-list/components/virtual-list.svelte +++ b/src/uix/soma/components/virtual-list/components/virtual-list.svelte @@ -6,12 +6,10 @@ import { mergeProps } from '../../../props'; import { createId } from '$active-uix/id'; import { activeDir } from '../../../direction'; - import { Soma } from '../../../core/soma.svelte'; import { VirtualListProvider } from '../virtual-list-provider.svelte'; import type { VirtualListProps } from '../types'; const uid = $props.id(); - const soma = Soma.get(); let { ref = $bindable(null), @@ -50,7 +48,7 @@ followBehavior: readableActive(() => followBehavior), endThreshold: readableActive(() => endThreshold), onAtEndChange: readableActive(() => onAtEndChange), - dir: activeDir(() => dir, soma) + dir: activeDir(() => dir) }); const mergedProps = $derived(mergeProps(restProps, state.props)); diff --git a/src/uix/soma/components/virtual-list/virtual-list-provider.svelte.ts b/src/uix/soma/components/virtual-list/virtual-list-provider.svelte.ts index c21351710..f0fa8818d 100644 --- a/src/uix/soma/components/virtual-list/virtual-list-provider.svelte.ts +++ b/src/uix/soma/components/virtual-list/virtual-list-provider.svelte.ts @@ -1,4 +1,5 @@ import { SvelteMap } from 'svelte/reactivity'; +import { resolveDir } from '../../direction'; import { context, type WithRefOpts } from '../../provider'; import { watch, type ActiveProps } from '$libs/reactive'; import type { Orientation, Direction } from '../../types'; @@ -40,7 +41,7 @@ export class VirtualListProvider { * `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 opts: VirtualListOpts; readonly runtimePart: SomaRuntimePart; diff --git a/src/uix/soma/components/year-grid/components/year-grid.svelte b/src/uix/soma/components/year-grid/components/year-grid.svelte index a519234e2..fe251980f 100644 --- a/src/uix/soma/components/year-grid/components/year-grid.svelte +++ b/src/uix/soma/components/year-grid/components/year-grid.svelte @@ -5,10 +5,8 @@ import { createId } from '$active-uix/id'; import { YearGridProvider, resolveInitialYearPlaceholder } from '../year-grid-provider.svelte'; import type { YearGridProps } from '../types'; - import { Soma } from '../../../core/soma.svelte'; const uid = $props.id(); - const soma = Soma.get(); let { ref = $bindable(null), @@ -55,7 +53,7 @@ maxValue: readableActive(() => maxValue), disabled: readableActive(() => disabled), readonly: readableActive(() => readonly), - dir: activeDir(() => dir, soma), + dir: activeDir(() => dir), locale: readableActive(() => locale ?? ''), decadeSize: readableActive(() => decadeSize), pageAligned: readableActive(() => pageAligned), diff --git a/src/uix/soma/components/year-grid/year-grid-provider.svelte.ts b/src/uix/soma/components/year-grid/year-grid-provider.svelte.ts index 8b2f6c81b..ae547b1c6 100644 --- a/src/uix/soma/components/year-grid/year-grid-provider.svelte.ts +++ b/src/uix/soma/components/year-grid/year-grid-provider.svelte.ts @@ -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'; @@ -68,7 +69,7 @@ export class YearGridProvider { * 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: YearGridOpts; readonly runtimePart: SomaRuntimePart; diff --git a/src/uix/soma/direction.ts b/src/uix/soma/direction.ts index 7382cddad..3e1681aea 100644 --- a/src/uix/soma/direction.ts +++ b/src/uix/soma/direction.ts @@ -19,7 +19,7 @@ import type { Direction } from './types'; export const DirectionContext = new Context>('SomaDirection'); /** - * The ONE way a component resolves its text direction. + * The ONE way a component resolves its ASSERTED text direction. * * Before this existed the catalogue had ten spellings of these three lines — * some with a `?? 'ltr'` tail, some with an `as 'ltr' | 'rtl'` cast, some @@ -28,12 +28,21 @@ export const DirectionContext = new Context>('Soma * framework that sells homogeneity cannot afford ten shapes for its most * cross-cutting axis. * - * ## The chain + * ## The chain, split at the assertion * * ```text - * prop dir → ancestor assertion (DirectionContext) → soma.prefs.getDir() + * assertion: prop dir → ancestor assertion (DirectionContext) ← activeDir + * maths: assertion → soma.prefs.getDir() → 'ltr' ← resolveDir * ``` * + * `activeDir` returns the ASSERTION — what somebody explicitly chose for this + * subtree. The app-wide preference is deliberately NOT part of it: prefs + * reach the page once, through the boot's DOM projection on ``, and + * everything below inherits. Folding prefs into the per-component value was + * what made every component stamp `dir="ltr"` on an unasserted page — the + * attribute never got to be absent, so DOM inheritance never got to work, and + * every composed child cut its parent's assertion with the prefs default. + * * The context link is what closes the two holes measured in the field: a menu * composed inside an RTL player used to start its own chain and land on the * app-global preference (in place), and its portalled panel resolved on its @@ -43,16 +52,14 @@ export const DirectionContext = new Context>('Soma * ## Why the result can be `undefined` * * `undefined` means **nobody asserted a direction** — not the consumer via the - * prop, not an ancestor via its own assertion, not the app via a registered - * `direction` preference. That is NOT the same as `'ltr'`, and collapsing the - * two is the bug this signature exists to prevent: + * prop, not an ancestor via its own assertion. That is NOT the same as + * `'ltr'`, and collapsing the two is the bug this signature exists to prevent: * - * - A component's own math (arrow keys, pointer sign, placement flip) wants a - * concrete value. Providers default it once, in `resolvedDir`. * - The `dir` ATTRIBUTE a component writes to the DOM must only appear when - * someone asserted something. Stamping a defaulted `'ltr'` is what makes a - * component force its subtree back to LTR inside an `` page. - * An omitted attribute inherits — for free, with no DOM read. + * someone asserted something. An omitted attribute inherits — for free, + * with no DOM read — from the projected ``. + * - A component's own maths wants a concrete value: that is `resolveDir`, + * in the provider, once. * * ## Placement rule * @@ -61,12 +68,24 @@ export const DirectionContext = new Context>('Soma * constructor (provider tests construct providers directly, outside any * component). Every wrapper already calls it at init; keep it that way. */ -export function activeDir( - dir: () => Direction | undefined, - soma: Soma | undefined -): Active { +export function activeDir(dir: () => Direction | undefined): Active { const inherited = DirectionContext.getOr(undefined); const asserted = readableActive(() => dir() ?? inherited?.current); DirectionContext.set(asserted); - return readableActive(() => asserted.current ?? soma?.prefs.getDir()); + return asserted; +} + +/** + * The maths tail of the chain: `assertion → prefs → 'ltr'`. Lives in the + * PROVIDER (`readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, + * this.soma))`), once — arrow keys, pointer signs and placement flips read a + * concrete value here while the ATTRIBUTE stays the raw assertion. + * + * Prefs sit in this tail and not in `activeDir` because the maths cannot read + * the DOM (the attribute is a projection, never a source) — this is the one + * consumer that still needs the preference as a VALUE rather than as + * inheritance. + */ +export function resolveDir(dir: Active, soma: Soma | undefined): Direction { + return dir.current ?? soma?.prefs?.getDir() ?? 'ltr'; } diff --git a/src/uix/soma/test/direction-harness-child.svelte b/src/uix/soma/test/direction-harness-child.svelte index 1c75addfc..5c11f9835 100644 --- a/src/uix/soma/test/direction-harness-child.svelte +++ b/src/uix/soma/test/direction-harness-child.svelte @@ -17,5 +17,5 @@ // svelte-ignore state_referenced_locally — the harness deliberately hands // the Active out once at init; `report` is not expected to be reactive. - report(activeDir(() => dir, undefined)); + report(activeDir(() => dir)); diff --git a/src/uix/soma/test/direction-harness.svelte b/src/uix/soma/test/direction-harness.svelte index e35cd0364..25276499a 100644 --- a/src/uix/soma/test/direction-harness.svelte +++ b/src/uix/soma/test/direction-harness.svelte @@ -23,7 +23,7 @@ // svelte-ignore state_referenced_locally — the harness deliberately hands // the Active out once at init; `report` is not expected to be reactive. - report(activeDir(() => dir, undefined)); + report(activeDir(() => dir));