active-uix: uix.runtime(morfo, sources) auto-injects dom + semantic

Adds a single convenience method on `ActiveUix` so soma providers stop
duplicating the `dom: this.soma.dom, semantic: this.soma.semantic`
boilerplate at every `createMorfoRuntime` callsite.

Before (8 providers, identical lines):

    this.runtime = createMorfoRuntime(dialogMorfo, {
        dom: this.soma.dom,           // ← duplicated everywhere
        semantic: this.soma.semantic, // ← duplicated everywhere
        states: { … },
        props: { … },
        events: { … }
    })

After:

    this.runtime = this.soma.uix.runtime(dialogMorfo, {
        states: { … },
        props: { … },
        events: { … }
    })

The implementation is a thin convenience — internally `uix.runtime()`
calls `createMorfoRuntime(morfo, { dom: this.dom, semantic: this.semantic,
…sources })`. No registry, no scope ids, no per-instance lifecycle to
track. Per-provider runtime ownership unchanged. The user explicitly
chose this minimal fix over a "single shared runtime + scope handles"
design after weighing the tradeoffs.

Migrated callsites: avatar, collapsible, dialog, drawer, popover,
switch, toast, toggle (8 providers, ~10 callsites including toast's
two — root + item). The `createMorfoRuntime` named import dropped from
each (only `MorfoRuntime` type kept where the field is annotated).

3 new tests on `active-uix.svelte.test.ts`:
  - returns a MorfoRuntime with auto-injected services
  - caller does not pass dom or semantic
  - attach mode uses the external app's dom + semantic

Verified: vitest 17/17 active-uix (was 14), 211/211 UIX, smoke 68/68,
morfo-check 66/66.
morfo-runtime
dev 5 months ago
parent 9009cbfe8c
commit 217d6969e4

@ -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 ─────────────────────────────────────────────────────── // ── Cross-mode parity ───────────────────────────────────────────────────────
describe('cross-mode parity', () => { describe('cross-mode parity', () => {

@ -29,6 +29,7 @@ import { createActiveDom, type ActiveDom } from '$adom'
import { createActiveFrontend, type ActiveFrontend } from '$frontend/active-frontend.svelte' import { createActiveFrontend, type ActiveFrontend } from '$frontend/active-frontend.svelte'
import { createActiveFormat, type ActiveFormat } from '$format' import { createActiveFormat, type ActiveFormat } from '$format'
import { EngineSemantic } from '$uix/sema' 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 { CodeError, errCode, moduleSeed, type ErrCode, type ModuleSeed } from '$libs/errs'
import { componentLangs } from '$soma/core/langs' 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 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<M extends Morfo>(
morfo: M,
sources: Omit<MorfoRuntimeSources, 'dom' | 'semantic'>
): MorfoRuntime {
return createMorfoRuntime(morfo, {
dom: this.dom,
semantic: this.semantic,
...sources
})
}
dispose(): void { dispose(): void {
if (this.disposed) return if (this.disposed) return
this.disposed = true this.disposed = true

@ -21,6 +21,7 @@ import type { ActiveTimers } from '$timer'
import type { ActivePrefs } from '$prefs' import type { ActivePrefs } from '$prefs'
import type { LangNode, SupportedLocale } from '$libs/lang' import type { LangNode, SupportedLocale } from '$libs/lang'
import type { EngineSemantic, EngineSemanticOptions } from '$uix/sema' import type { EngineSemantic, EngineSemanticOptions } from '$uix/sema'
import type { Morfo, MorfoRuntime, MorfoRuntimeSources } from '$uix/morfo'
/** /**
* Lang configuration. Required because every UIX consumer needs at * Lang configuration. Required because every UIX consumer needs at
@ -102,6 +103,20 @@ export interface ActiveUix {
// ── UIX-level config ─────────────────────────────────────────────── // ── UIX-level config ───────────────────────────────────────────────
readonly somaPortalTo: string | HTMLElement | undefined 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<M extends Morfo>(
morfo: M,
sources: Omit<MorfoRuntimeSources, 'dom' | 'semantic'>
): MorfoRuntime
/** /**
* Underlying `ActiveApp` — present ONLY when `ActiveUix` was * Underlying `ActiveApp` — present ONLY when `ActiveUix` was
* obtained via `attachActiveUix(app)`. In standalone mode * obtained via `attachActiveUix(app)`. In standalone mode

@ -12,7 +12,7 @@ import type {
} from '$soma/provider'; } from '$soma/provider';
import type { OnChangeFn } from '../../types'; import type { OnChangeFn } from '../../types';
import { Soma } from '../../core/soma.svelte'; 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 { HTMLImgAttributes } from 'svelte/elements';
import type { AvatarLoadingStatus, AvatarProps, AvatarImageProps } from './types'; import type { AvatarLoadingStatus, AvatarProps, AvatarImageProps } from './types';
@ -53,8 +53,7 @@ export class AvatarProvider extends Provider<AvatarOpts> {
super(opts, { morfo: avatarMorfo, part: 'provider' }, AvatarProvider.ctx); super(opts, { morfo: avatarMorfo, part: 'provider' }, AvatarProvider.ctx);
const soma = Soma.require(); const soma = Soma.require();
this.runtime = createMorfoRuntime(avatarMorfo, { this.runtime = soma.uix.runtime(avatarMorfo, {
dom: soma.dom,
props: { props: {
status: () => opts.status.current status: () => opts.status.current
} }

@ -10,7 +10,7 @@ import {
} from '$libs/reactive'; } from '$libs/reactive';
import type { SomaMouseEvent } from '../../types'; import type { SomaMouseEvent } from '../../types';
import { Soma } from '../../core/soma.svelte'; 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'; import { collapsibleMorfo } from '../../../morfo/components/collapsible';
createAttrs(collapsibleMorfo); createAttrs(collapsibleMorfo);
@ -44,8 +44,7 @@ export class CollapsibleProvider extends Provider<CollapsibleOpts> {
super(opts, { morfo: collapsibleMorfo, part: 'provider' }, CollapsibleProvider.ctx); super(opts, { morfo: collapsibleMorfo, part: 'provider' }, CollapsibleProvider.ctx);
const soma = Soma.require(); const soma = Soma.require();
this.runtime = createMorfoRuntime(collapsibleMorfo, { this.runtime = soma.uix.runtime(collapsibleMorfo, {
dom: soma.dom,
states: { states: {
open: () => opts.open.current open: () => opts.open.current
}, },

@ -22,7 +22,7 @@ import { ScrollLock } from '../../layers/scroll-lock.svelte';
import { TextSelection } from '../../layers/text-selection.svelte'; import { TextSelection } from '../../layers/text-selection.svelte';
import { dialogMorfo } from '../../../morfo/components/dialog'; import { dialogMorfo } from '../../../morfo/components/dialog';
import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; import { type MorfoRuntime } from '$uix/morfo';
// ── Provider (root) ───────────────────────────────────────────────────────── // ── Provider (root) ─────────────────────────────────────────────────────────
@ -112,9 +112,7 @@ export class DialogProvider extends Provider<DialogOpts> {
ref: this.overlayRef ref: this.overlayRef
}); });
this.runtime = createMorfoRuntime(dialogMorfo, { this.runtime = this.soma.uix.runtime(dialogMorfo, {
dom: this.soma.dom,
semantic: this.soma.semantic,
states: { open: () => opts.open.current }, states: { open: () => opts.open.current },
props: { props: {
disabled: () => opts.disabled.current, disabled: () => opts.disabled.current,

@ -39,7 +39,7 @@ import type { DragDirection } from '../../layers/gesture';
import { DRAWER_LANGS } from './langs'; import { DRAWER_LANGS } from './langs';
import { drawerMorfo } from '../../../morfo/components/drawer'; import { drawerMorfo } from '../../../morfo/components/drawer';
import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; import { type MorfoRuntime } from '$uix/morfo';
const attrs = createAttrs(drawerMorfo); const attrs = createAttrs(drawerMorfo);
registerContract(drawerMorfo); registerContract(drawerMorfo);
@ -147,9 +147,7 @@ export class DrawerProvider extends Provider<DrawerOpts> {
ref: this.overlayRef ref: this.overlayRef
}); });
this.runtime = createMorfoRuntime(drawerMorfo, { this.runtime = this.soma!.uix.runtime(drawerMorfo, {
dom: this.soma!.dom,
semantic: this.soma!.semantic,
states: { open: () => opts.open.current }, states: { open: () => opts.open.current },
props: { props: {
disabled: () => opts.disabled.current, disabled: () => opts.disabled.current,

@ -42,7 +42,7 @@ import {
} from '../../layers/floating'; } from '../../layers/floating';
import { popoverMorfo } from '../../../morfo/components/popover'; import { popoverMorfo } from '../../../morfo/components/popover';
import { createMorfoRuntime, type MorfoRuntime } from '$uix/morfo'; import { type MorfoRuntime } from '$uix/morfo';
const attrs = createAttrs(popoverMorfo); const attrs = createAttrs(popoverMorfo);
registerContract(popoverMorfo); registerContract(popoverMorfo);
@ -99,9 +99,7 @@ export class PopoverProvider extends Provider<PopoverOpts> {
ref: this.overlayRef ref: this.overlayRef
}); });
this.runtime = createMorfoRuntime(popoverMorfo, { this.runtime = this.soma!.uix.runtime(popoverMorfo, {
dom: this.soma!.dom,
semantic: this.soma!.semantic,
states: { open: () => opts.open.current }, states: { open: () => opts.open.current },
props: { modal: () => opts.modal.current }, props: { modal: () => opts.modal.current },
events: { events: {

@ -8,7 +8,7 @@ import type {
} from '$soma/provider'; } from '$soma/provider';
import type { OnChangeFn, SomaMouseEvent } from '../../types'; import type { OnChangeFn, SomaMouseEvent } from '../../types';
import { Soma } from '../../core/soma.svelte'; 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 { switchMorfo } from '../../../morfo/components/switch';
import type { SwitchProps } from './types'; import type { SwitchProps } from './types';
@ -43,8 +43,7 @@ export class SwitchProvider extends Provider<SwitchOpts> {
super(opts, { morfo: switchMorfo, part: 'provider' }, SwitchProvider.ctx); super(opts, { morfo: switchMorfo, part: 'provider' }, SwitchProvider.ctx);
const soma = Soma.require(); const soma = Soma.require();
this.runtime = createMorfoRuntime(switchMorfo, { this.runtime = soma.uix.runtime(switchMorfo, {
dom: soma.dom,
states: { states: {
checked: () => opts.checked.current checked: () => opts.checked.current
}, },

@ -13,7 +13,7 @@ import {
import type { SomaMouseEvent } from '../../types'; import type { SomaMouseEvent } from '../../types';
import { Soma } from '../../core/soma.svelte'; import { Soma } from '../../core/soma.svelte';
import { isBrowser } from '$libs/dom'; 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 { Presence } from '../../layers/presence.svelte';
import type { Toaster, ToastData, SwipeDirection } from './toaster.svelte'; import type { Toaster, ToastData, SwipeDirection } from './toaster.svelte';
@ -54,9 +54,7 @@ export class ToastProvider extends Provider<ToastProviderOpts> {
// Root-scope runtime — only the Viewport part registers here. Item and // Root-scope runtime — only the Viewport part registers here. Item and
// its children get their own runtime per-instance (see ToastItemProvider). // its children get their own runtime per-instance (see ToastItemProvider).
const soma = Soma.require(); const soma = Soma.require();
this.runtime = createMorfoRuntime(toastMorfo, { this.runtime = soma.uix.runtime(toastMorfo, {
dom: soma.dom,
semantic: soma.semantic,
props: { props: {
label: () => opts.label.current label: () => opts.label.current
} }
@ -218,9 +216,7 @@ export class ToastItemProvider extends Provider<ToastItemOpts> {
// perceptual signal first, then this callback mutates state, then the // perceptual signal first, then this callback mutates state, then the
// item's effect re-derives data-state etc. via dom.apply. // item's effect re-derives data-state etc. via dom.apply.
const soma = Soma.require(); const soma = Soma.require();
this.runtime = createMorfoRuntime(toastMorfo, { this.runtime = soma.uix.runtime(toastMorfo, {
dom: soma.dom,
semantic: soma.semantic,
states: { states: {
open: () => this.isOpen open: () => this.isOpen
}, },

@ -8,7 +8,7 @@ import type {
} from '$soma/provider'; } from '$soma/provider';
import type { OnChangeFn, SomaMouseEvent } from '../../types'; import type { OnChangeFn, SomaMouseEvent } from '../../types';
import { Soma } from '../../core/soma.svelte'; 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 { FieldProvider } from '../field/field-provider.svelte';
import { toggleMorfo } from '../../../morfo/components/toggle'; import { toggleMorfo } from '../../../morfo/components/toggle';
@ -63,8 +63,7 @@ export class ToggleProvider extends Provider<ToggleOpts> {
// aria-disabled, etc.); this provider keeps only handler wiring + the // aria-disabled, etc.); this provider keeps only handler wiring + the
// HTML `disabled` attribute (not a morfo concern). // HTML `disabled` attribute (not a morfo concern).
const soma = Soma.require(); const soma = Soma.require();
this.runtime = createMorfoRuntime(toggleMorfo, { this.runtime = soma.uix.runtime(toggleMorfo, {
dom: soma.dom,
states: { states: {
on: () => opts.pressed.current on: () => opts.pressed.current
}, },

Loading…
Cancel
Save

Powered by TurnKey Linux.