diff --git a/src/uix/active-uix/active-uix.svelte.test.ts b/src/uix/active-uix/active-uix.svelte.test.ts index 03813af68..058ff8318 100644 --- a/src/uix/active-uix/active-uix.svelte.test.ts +++ b/src/uix/active-uix/active-uix.svelte.test.ts @@ -212,6 +212,75 @@ describe('attachActiveUix (integration mode)', () => { }) }) +// ── uix.runtime(morfo, sources) ───────────────────────────────────────────── + +describe('uix.runtime(morfo, sources)', () => { + const minimalMorfo = { + name: 'TestComp', + kebab: 'test-comp', + scope: ['soma'], + events: [], + parts: [ + { + name: 'Provider', + kebab: 'provider', + archetype: 'provider', + kind: 'virtual', + defaultElement: 'none', + optional: false, + states: [], + data: [], + aria: [] + } + ] + } as const + + it('returns a MorfoRuntime with dom + semantic auto-injected from the uix', () => { + const uix = createActiveUix({ lang: minimalLang }) + try { + const rt = uix.runtime(minimalMorfo, { + states: {}, + props: {}, + parts: {}, + events: {} + }) + expect(rt).toBeDefined() + expect(typeof rt.trigger).toBe('function') + expect(typeof rt.registerPart).toBe('function') + expect(typeof rt.partProps).toBe('function') + } finally { + uix.dispose() + } + }) + + it('caller does not need to pass dom or semantic — they come from the uix', () => { + const uix = createActiveUix({ lang: minimalLang }) + try { + // Same call as above. The point of the test: the call type-checks + // without `dom` or `semantic` keys. If the runtime were missing + // either, registerPart / trigger would throw at runtime. + const rt = uix.runtime(minimalMorfo, {}) + expect(rt).toBeDefined() + } finally { + uix.dispose() + } + }) + + it('attach mode: runtime uses the external app\'s dom + semantic', () => { + const app = createActiveApp({ + services: defineUixServices({ lang: minimalLang }) + }) + const uix = attachActiveUix(app as unknown as ActiveApp) + try { + const rt = uix.runtime(minimalMorfo, {}) + expect(rt).toBeDefined() + } finally { + uix.dispose() + app.dispose() + } + }) +}) + // ── Cross-mode parity ─────────────────────────────────────────────────────── describe('cross-mode parity', () => { diff --git a/src/uix/active-uix/active-uix.svelte.ts b/src/uix/active-uix/active-uix.svelte.ts index b16cf6255..c4a402055 100644 --- a/src/uix/active-uix/active-uix.svelte.ts +++ b/src/uix/active-uix/active-uix.svelte.ts @@ -29,6 +29,7 @@ import { createActiveDom, type ActiveDom } from '$adom' import { createActiveFrontend, type ActiveFrontend } from '$frontend/active-frontend.svelte' import { createActiveFormat, type ActiveFormat } from '$format' import { EngineSemantic } from '$uix/sema' +import { createMorfoRuntime, type Morfo, type MorfoRuntime, type MorfoRuntimeSources } from '$uix/morfo' import { CodeError, errCode, moduleSeed, type ErrCode, type ModuleSeed } from '$libs/errs' import { componentLangs } from '$soma/core/langs' @@ -268,6 +269,22 @@ class ActiveUixImpl implements ActiveUix { return this.init.mode === 'standalone' ? this.init.prefs : this.init.app.prefs } + /** + * Build a `MorfoRuntime` for a component instance. Auto-injects + * `dom` + `semantic` from the uix; caller passes the per-instance + * `states`, `props`, `parts`, `events`, `actions`. + */ + runtime( + morfo: M, + sources: Omit + ): MorfoRuntime { + return createMorfoRuntime(morfo, { + dom: this.dom, + semantic: this.semantic, + ...sources + }) + } + dispose(): void { if (this.disposed) return this.disposed = true diff --git a/src/uix/active-uix/types.ts b/src/uix/active-uix/types.ts index 22cf151f7..c476bc46d 100644 --- a/src/uix/active-uix/types.ts +++ b/src/uix/active-uix/types.ts @@ -21,6 +21,7 @@ import type { ActiveTimers } from '$timer' import type { ActivePrefs } from '$prefs' import type { LangNode, SupportedLocale } from '$libs/lang' import type { EngineSemantic, EngineSemanticOptions } from '$uix/sema' +import type { Morfo, MorfoRuntime, MorfoRuntimeSources } from '$uix/morfo' /** * Lang configuration. Required because every UIX consumer needs at @@ -102,6 +103,20 @@ export interface ActiveUix { // ── UIX-level config ─────────────────────────────────────────────── readonly somaPortalTo: string | HTMLElement | undefined + /** + * Instantiates a `MorfoRuntime` for a component, auto-injecting + * `dom` and `semantic` from the uix. The caller (typically a soma + * provider) supplies only the per-instance `states`, `props`, + * `parts`, `events` and `actions` sources. + * + * Eliminates the duplicate `dom: this.soma.dom, semantic: + * this.soma.semantic` boilerplate every provider used to write. + */ + runtime( + morfo: M, + sources: Omit + ): MorfoRuntime + /** * Underlying `ActiveApp` — present ONLY when `ActiveUix` was * obtained via `attachActiveUix(app)`. In standalone mode diff --git a/src/uix/soma/components/avatar/avatar-provider.svelte.ts b/src/uix/soma/components/avatar/avatar-provider.svelte.ts index 3375e0e37..e60d7a9aa 100644 --- a/src/uix/soma/components/avatar/avatar-provider.svelte.ts +++ b/src/uix/soma/components/avatar/avatar-provider.svelte.ts @@ -12,7 +12,7 @@ import type { } from '$soma/provider'; import type { OnChangeFn } from '../../types'; import { Soma } from '../../core/soma.svelte'; -import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; +import { type MorfoRuntime } from '$uix/morfo'; import type { HTMLImgAttributes } from 'svelte/elements'; import type { AvatarLoadingStatus, AvatarProps, AvatarImageProps } from './types'; @@ -53,8 +53,7 @@ export class AvatarProvider extends Provider { super(opts, { morfo: avatarMorfo, part: 'provider' }, AvatarProvider.ctx); const soma = Soma.require(); - this.runtime = createMorfoRuntime(avatarMorfo, { - dom: soma.dom, + this.runtime = soma.uix.runtime(avatarMorfo, { props: { status: () => opts.status.current } diff --git a/src/uix/soma/components/collapsible/collapsible-provider.svelte.ts b/src/uix/soma/components/collapsible/collapsible-provider.svelte.ts index b49f73b29..d93675937 100644 --- a/src/uix/soma/components/collapsible/collapsible-provider.svelte.ts +++ b/src/uix/soma/components/collapsible/collapsible-provider.svelte.ts @@ -10,7 +10,7 @@ import { } from '$libs/reactive'; import type { SomaMouseEvent } from '../../types'; import { Soma } from '../../core/soma.svelte'; -import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; +import { type MorfoRuntime } from '$uix/morfo'; import { collapsibleMorfo } from '../../../morfo/components/collapsible'; createAttrs(collapsibleMorfo); @@ -44,8 +44,7 @@ export class CollapsibleProvider extends Provider { super(opts, { morfo: collapsibleMorfo, part: 'provider' }, CollapsibleProvider.ctx); const soma = Soma.require(); - this.runtime = createMorfoRuntime(collapsibleMorfo, { - dom: soma.dom, + this.runtime = soma.uix.runtime(collapsibleMorfo, { states: { open: () => opts.open.current }, diff --git a/src/uix/soma/components/dialog/dialog-provider.svelte.ts b/src/uix/soma/components/dialog/dialog-provider.svelte.ts index 1e8094482..6cea7d5c1 100644 --- a/src/uix/soma/components/dialog/dialog-provider.svelte.ts +++ b/src/uix/soma/components/dialog/dialog-provider.svelte.ts @@ -22,7 +22,7 @@ import { ScrollLock } from '../../layers/scroll-lock.svelte'; import { TextSelection } from '../../layers/text-selection.svelte'; import { dialogMorfo } from '../../../morfo/components/dialog'; -import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; +import { type MorfoRuntime } from '$uix/morfo'; // ── Provider (root) ───────────────────────────────────────────────────────── @@ -112,9 +112,7 @@ export class DialogProvider extends Provider { ref: this.overlayRef }); - this.runtime = createMorfoRuntime(dialogMorfo, { - dom: this.soma.dom, - semantic: this.soma.semantic, + this.runtime = this.soma.uix.runtime(dialogMorfo, { states: { open: () => opts.open.current }, props: { disabled: () => opts.disabled.current, diff --git a/src/uix/soma/components/drawer/drawer-provider.svelte.ts b/src/uix/soma/components/drawer/drawer-provider.svelte.ts index 780567a3f..bf453ff7e 100644 --- a/src/uix/soma/components/drawer/drawer-provider.svelte.ts +++ b/src/uix/soma/components/drawer/drawer-provider.svelte.ts @@ -39,7 +39,7 @@ import type { DragDirection } from '../../layers/gesture'; import { DRAWER_LANGS } from './langs'; import { drawerMorfo } from '../../../morfo/components/drawer'; -import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; +import { type MorfoRuntime } from '$uix/morfo'; const attrs = createAttrs(drawerMorfo); registerContract(drawerMorfo); @@ -147,9 +147,7 @@ export class DrawerProvider extends Provider { ref: this.overlayRef }); - this.runtime = createMorfoRuntime(drawerMorfo, { - dom: this.soma!.dom, - semantic: this.soma!.semantic, + this.runtime = this.soma!.uix.runtime(drawerMorfo, { states: { open: () => opts.open.current }, props: { disabled: () => opts.disabled.current, diff --git a/src/uix/soma/components/popover/popover-provider.svelte.ts b/src/uix/soma/components/popover/popover-provider.svelte.ts index 9fb899e1c..cb17efb5d 100644 --- a/src/uix/soma/components/popover/popover-provider.svelte.ts +++ b/src/uix/soma/components/popover/popover-provider.svelte.ts @@ -42,7 +42,7 @@ import { } from '../../layers/floating'; import { popoverMorfo } from '../../../morfo/components/popover'; -import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; +import { type MorfoRuntime } from '$uix/morfo'; const attrs = createAttrs(popoverMorfo); registerContract(popoverMorfo); @@ -99,9 +99,7 @@ export class PopoverProvider extends Provider { ref: this.overlayRef }); - this.runtime = createMorfoRuntime(popoverMorfo, { - dom: this.soma!.dom, - semantic: this.soma!.semantic, + this.runtime = this.soma!.uix.runtime(popoverMorfo, { states: { open: () => opts.open.current }, props: { modal: () => opts.modal.current }, events: { diff --git a/src/uix/soma/components/switch/switch-provider.svelte.ts b/src/uix/soma/components/switch/switch-provider.svelte.ts index 43b2f98e5..0de3004bb 100644 --- a/src/uix/soma/components/switch/switch-provider.svelte.ts +++ b/src/uix/soma/components/switch/switch-provider.svelte.ts @@ -8,7 +8,7 @@ import type { } from '$soma/provider'; import type { OnChangeFn, SomaMouseEvent } from '../../types'; import { Soma } from '../../core/soma.svelte'; -import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; +import { type MorfoRuntime } from '$uix/morfo'; import { switchMorfo } from '../../../morfo/components/switch'; import type { SwitchProps } from './types'; @@ -43,8 +43,7 @@ export class SwitchProvider extends Provider { super(opts, { morfo: switchMorfo, part: 'provider' }, SwitchProvider.ctx); const soma = Soma.require(); - this.runtime = createMorfoRuntime(switchMorfo, { - dom: soma.dom, + this.runtime = soma.uix.runtime(switchMorfo, { states: { checked: () => opts.checked.current }, diff --git a/src/uix/soma/components/toast/toast-provider.svelte.ts b/src/uix/soma/components/toast/toast-provider.svelte.ts index b5bc0a2e6..55ac20165 100644 --- a/src/uix/soma/components/toast/toast-provider.svelte.ts +++ b/src/uix/soma/components/toast/toast-provider.svelte.ts @@ -13,7 +13,7 @@ import { import type { SomaMouseEvent } from '../../types'; import { Soma } from '../../core/soma.svelte'; import { isBrowser } from '$libs/dom'; -import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; +import { type MorfoRuntime } from '$uix/morfo'; import { Presence } from '../../layers/presence.svelte'; import type { Toaster, ToastData, SwipeDirection } from './toaster.svelte'; @@ -54,9 +54,7 @@ export class ToastProvider extends Provider { // Root-scope runtime — only the Viewport part registers here. Item and // its children get their own runtime per-instance (see ToastItemProvider). const soma = Soma.require(); - this.runtime = createMorfoRuntime(toastMorfo, { - dom: soma.dom, - semantic: soma.semantic, + this.runtime = soma.uix.runtime(toastMorfo, { props: { label: () => opts.label.current } @@ -218,9 +216,7 @@ export class ToastItemProvider extends Provider { // perceptual signal first, then this callback mutates state, then the // item's effect re-derives data-state etc. via dom.apply. const soma = Soma.require(); - this.runtime = createMorfoRuntime(toastMorfo, { - dom: soma.dom, - semantic: soma.semantic, + this.runtime = soma.uix.runtime(toastMorfo, { states: { open: () => this.isOpen }, diff --git a/src/uix/soma/components/toggle/toggle-provider.svelte.ts b/src/uix/soma/components/toggle/toggle-provider.svelte.ts index 66efb9d37..b2bb0401a 100644 --- a/src/uix/soma/components/toggle/toggle-provider.svelte.ts +++ b/src/uix/soma/components/toggle/toggle-provider.svelte.ts @@ -8,7 +8,7 @@ import type { } from '$soma/provider'; import type { OnChangeFn, SomaMouseEvent } from '../../types'; import { Soma } from '../../core/soma.svelte'; -import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; +import { type MorfoRuntime } from '$uix/morfo'; import { FieldProvider } from '../field/field-provider.svelte'; import { toggleMorfo } from '../../../morfo/components/toggle'; @@ -63,8 +63,7 @@ export class ToggleProvider extends Provider { // aria-disabled, etc.); this provider keeps only handler wiring + the // HTML `disabled` attribute (not a morfo concern). const soma = Soma.require(); - this.runtime = createMorfoRuntime(toggleMorfo, { - dom: soma.dom, + this.runtime = soma.uix.runtime(toggleMorfo, { states: { on: () => opts.pressed.current },