From 6df2f062ad64a2259fb0a1f0068cf91a3a20636a Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 15 May 2026 02:17:30 +0200 Subject: [PATCH] Consolidate Soma form feed runtimes --- .../announce/announce-provider.svelte.ts | 19 +++-- .../components/feed/feed-provider.svelte.ts | 65 +++++++++------- .../components/field/field-provider.svelte.ts | 77 ++++++++++++------- .../components/form/form-provider.svelte.ts | 45 +++++++---- 4 files changed, 128 insertions(+), 78 deletions(-) diff --git a/src/uix/soma/components/announce/announce-provider.svelte.ts b/src/uix/soma/components/announce/announce-provider.svelte.ts index b0b170742..fe957c023 100644 --- a/src/uix/soma/components/announce/announce-provider.svelte.ts +++ b/src/uix/soma/components/announce/announce-provider.svelte.ts @@ -5,7 +5,7 @@ import { ANNOUNCE_LANGS } from './langs'; import type { AnnouncePriority, AnnounceRole } from './types'; import { announceMorfo } from '../../../morfo/components/announce'; -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; /** Visually-hidden style kept in-sync with the WAI "sr-only" recipe. */ @@ -36,6 +36,8 @@ interface AnnounceOpts extends ProviderOpts, ActiveProps<{ defaultTimeout: numbe export class AnnounceProvider { readonly opts: AnnounceOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('Announce'); static get(): AnnounceProvider | undefined { return this.ctx.getOr(undefined) as AnnounceProvider | undefined; @@ -48,8 +50,6 @@ export class AnnounceProvider { return new AnnounceProvider(opts); } - readonly soma = Soma.get(); - /** Current text for each internal region. Swap alternates so repeated announcements re-fire. */ politeA = $state(''); politeB = $state(''); @@ -63,9 +63,14 @@ export class AnnounceProvider { private constructor(opts: AnnounceOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(announceMorfo, {}) - .part('provider', { id: opts.id, ref: opts.ref, owner: this, context: AnnounceProvider.ctx }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(announceMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: AnnounceProvider.ctx + }); $effect(() => { return () => { @@ -134,7 +139,7 @@ export class AnnounceProvider { readonly internalRegionStyle = VISUALLY_HIDDEN_STYLE; readonly politeLabel = $derived.by( - () => this.soma?.langs.ts(ANNOUNCE_LANGS.LABEL) ?? 'Announcements' + () => this.soma.langs.ts(ANNOUNCE_LANGS.LABEL) ?? 'Announcements' ); readonly props = $derived.by(() => diff --git a/src/uix/soma/components/feed/feed-provider.svelte.ts b/src/uix/soma/components/feed/feed-provider.svelte.ts index 8d41450ba..923a24ac3 100644 --- a/src/uix/soma/components/feed/feed-provider.svelte.ts +++ b/src/uix/soma/components/feed/feed-provider.svelte.ts @@ -8,7 +8,7 @@ import { Soma } from '../../core/soma.svelte'; import { FEED_LANGS } from './langs'; import { feedMorfo } from '../../../morfo/components/feed'; -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; const attrs = createAttrs(feedMorfo); @@ -28,6 +28,8 @@ interface FeedOpts export class FeedProvider { readonly opts: FeedOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('Feed'); static get(): FeedProvider | undefined { return this.ctx.getOr(undefined) as FeedProvider | undefined; @@ -40,18 +42,21 @@ export class FeedProvider { return new FeedProvider(opts); } - readonly soma = Soma.get(); - private constructor(opts: FeedOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(feedMorfo, {}) - .part('provider', { id: opts.id, ref: opts.ref, owner: this, context: FeedProvider.ctx }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(feedMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: FeedProvider.ctx + }); } readonly resolvedAriaLabel: Active = readableActive(() => { if (this.opts.ariaLabelledby.current) return undefined; - return this.opts.ariaLabel.current || this.soma?.langs.ts(FEED_LANGS.LABEL) || undefined; + return this.opts.ariaLabel.current || this.soma.langs.ts(FEED_LANGS.LABEL) || undefined; }); /** All article elements, in DOM order. */ @@ -160,14 +165,19 @@ export class FeedThreadProvider { } readonly parentArticle: FeedArticleProvider; + readonly provider: FeedProvider; readonly level: number; private constructor(opts: FeedThreadOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(feedMorfo, {}) - .part('thread', { id: opts.id, ref: opts.ref, owner: this, context: FeedThreadProvider.ctx }); this.parentArticle = FeedArticleProvider.require(); + this.provider = this.parentArticle.provider; + this.runtimePart = this.provider.runtime.part('thread', { + id: opts.id, + ref: opts.ref, + owner: this, + context: FeedThreadProvider.ctx + }); this.level = this.parentArticle.level + 1; } @@ -211,16 +221,14 @@ export class FeedArticleProvider { private constructor(opts: FeedArticleOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(feedMorfo, {}) - .part('article', { - id: opts.id, - ref: opts.ref, - owner: this, - context: FeedArticleProvider.ctx - }); this.provider = FeedProvider.require(); this.thread = FeedThreadProvider.get(); + this.runtimePart = this.provider.runtime.part('article', { + id: opts.id, + ref: opts.ref, + owner: this, + context: FeedArticleProvider.ctx + }); this.level = this.thread ? this.thread.level : 1; } @@ -299,10 +307,12 @@ export class FeedArticleTitleProvider { private constructor(opts: FeedArticleTitleOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(feedMorfo, {}) - .part('article-title', { id: opts.id, ref: opts.ref, owner: this }); this.article = FeedArticleProvider.require(); + this.runtimePart = this.article.provider.runtime.part('article-title', { + id: opts.id, + ref: opts.ref, + owner: this + }); // A30: direct assignment. this.article.titleId.current = opts.id.current; } @@ -342,10 +352,12 @@ export class FeedArticleDescriptionProvider { private constructor(opts: FeedArticleDescriptionOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(feedMorfo, {}) - .part('article-description', { id: opts.id, ref: opts.ref, owner: this }); this.article = FeedArticleProvider.require(); + this.runtimePart = this.article.provider.runtime.part('article-description', { + id: opts.id, + ref: opts.ref, + owner: this + }); this.article.descriptionId.current = opts.id.current; } @@ -387,10 +399,9 @@ export class FeedSentinelProvider { private constructor(opts: FeedSentinelOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(feedMorfo, {}) - .part('sentinel', { id: opts.id, ref: opts.ref, owner: this }); this.feed = FeedProvider.get(); + const runtime = this.feed?.runtime ?? Soma.require().runtime(feedMorfo, {}); + this.runtimePart = runtime.part('sentinel', { id: opts.id, ref: opts.ref, owner: this }); $effect(() => { const el = opts.ref.current; diff --git a/src/uix/soma/components/field/field-provider.svelte.ts b/src/uix/soma/components/field/field-provider.svelte.ts index da45b34d1..2de2ddd82 100644 --- a/src/uix/soma/components/field/field-provider.svelte.ts +++ b/src/uix/soma/components/field/field-provider.svelte.ts @@ -7,7 +7,7 @@ import { FormProvider } from '../form/form-provider.svelte'; import { fieldMorfo } from '../../../morfo/components/field'; import { Soma } from '../../core/soma.svelte'; -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; // ── Root ───────────────────────────────────────────────────────────────────── @@ -30,6 +30,8 @@ interface FieldOpts export class FieldProvider { readonly opts: FieldOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('Field'); static get(): FieldProvider | undefined { return this.ctx.getOr(undefined) as FieldProvider | undefined; @@ -53,9 +55,14 @@ export class FieldProvider { private constructor(opts: FieldOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fieldMorfo, {}) - .part('provider', { id: opts.id, ref: opts.ref, owner: this, context: FieldProvider.ctx }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(fieldMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: FieldProvider.ctx + }); // Auto-register / unregister with the parent Form (if `name` is set). $effect(() => { @@ -174,10 +181,12 @@ export class FieldLabelProvider { private constructor(opts: FieldLabelOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fieldMorfo, {}) - .part('label', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FieldProvider.require(); + this.runtimePart = this.provider.runtime.part('label', { + id: opts.id, + ref: opts.ref, + owner: this + }); this.provider.labelId.current = opts.id.current; } @@ -207,10 +216,12 @@ export class FieldControlProvider { private constructor(opts: FieldControlOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fieldMorfo, {}) - .part('control', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FieldProvider.require(); + this.runtimePart = this.provider.runtime.part('control', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => @@ -246,10 +257,12 @@ export class FieldInputProvider { private constructor(opts: FieldInputOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fieldMorfo, {}) - .part('input', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FieldProvider.require(); + this.runtimePart = this.provider.runtime.part('input', { + id: opts.id, + ref: opts.ref, + owner: this + }); this.provider.inputId.current = opts.id.current; } @@ -303,10 +316,12 @@ export class FieldHelperTextProvider { private constructor(opts: FieldHelperTextOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fieldMorfo, {}) - .part('helper-text', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FieldProvider.require(); + this.runtimePart = this.provider.runtime.part('helper-text', { + id: opts.id, + ref: opts.ref, + owner: this + }); this.provider.helperId.current = opts.id.current; $effect(() => { @@ -340,10 +355,12 @@ export class FieldErrorTextProvider { private constructor(opts: FieldErrorTextOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fieldMorfo, {}) - .part('error-text', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FieldProvider.require(); + this.runtimePart = this.provider.runtime.part('error-text', { + id: opts.id, + ref: opts.ref, + owner: this + }); this.provider.errorId.current = opts.id.current; $effect(() => { @@ -370,16 +387,19 @@ interface FieldPrefixOpts extends WithRefOpts {} export class FieldPrefixProvider { readonly opts: FieldPrefixOpts; readonly runtimePart: SomaRuntimePart; + readonly provider: FieldProvider; static create(opts: FieldPrefixOpts) { return new FieldPrefixProvider(opts); } private constructor(opts: FieldPrefixOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fieldMorfo, {}) - .part('prefix', { id: opts.id, ref: opts.ref, owner: this }); - FieldProvider.require(); + this.provider = FieldProvider.require(); + this.runtimePart = this.provider.runtime.part('prefix', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => @@ -397,16 +417,19 @@ interface FieldSuffixOpts extends WithRefOpts {} export class FieldSuffixProvider { readonly opts: FieldSuffixOpts; readonly runtimePart: SomaRuntimePart; + readonly provider: FieldProvider; static create(opts: FieldSuffixOpts) { return new FieldSuffixProvider(opts); } private constructor(opts: FieldSuffixOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fieldMorfo, {}) - .part('suffix', { id: opts.id, ref: opts.ref, owner: this }); - FieldProvider.require(); + this.provider = FieldProvider.require(); + this.runtimePart = this.provider.runtime.part('suffix', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => diff --git a/src/uix/soma/components/form/form-provider.svelte.ts b/src/uix/soma/components/form/form-provider.svelte.ts index 19d7a04ea..9efa0cef4 100644 --- a/src/uix/soma/components/form/form-provider.svelte.ts +++ b/src/uix/soma/components/form/form-provider.svelte.ts @@ -12,7 +12,7 @@ import type { Form } from './form-core.svelte'; import { formMorfo } from '../../../morfo/components/form'; const attrs = createAttrs(formMorfo); -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; // ── Root provider ──────────────────────────────────────────────────────────── @@ -30,6 +30,8 @@ export interface FormProviderOpts export class FormProvider { readonly opts: FormProviderOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('Form'); static get(): FormProvider | undefined { return this.ctx.getOr(undefined) as FormProvider | undefined; @@ -41,13 +43,16 @@ export class FormProvider { return new FormProvider(opts); } - readonly soma = Soma.get(); - private constructor(opts: FormProviderOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(formMorfo, {}) - .part('provider', { id: opts.id, ref: opts.ref, owner: this, context: FormProvider.ctx }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(formMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: FormProvider.ctx + }); } get form(): TForm { @@ -142,14 +147,16 @@ export class FormSubmitProvider { private constructor(opts: FormSubmitOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(formMorfo, {}) - .part('submit', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FormProvider.require(); + this.runtimePart = this.provider.runtime.part('submit', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly resolvedLabel = $derived.by( - () => this.opts.ariaLabel.current ?? this.provider.soma?.langs.ts(FORM_LANGS.SUBMIT) ?? 'Submit' + () => this.opts.ariaLabel.current ?? this.provider.soma.langs.ts(FORM_LANGS.SUBMIT) ?? 'Submit' ); readonly props = $derived.by(() => @@ -182,14 +189,16 @@ export class FormResetProvider { private constructor(opts: FormResetOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(formMorfo, {}) - .part('reset', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FormProvider.require(); + this.runtimePart = this.provider.runtime.part('reset', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly resolvedLabel = $derived.by( - () => this.opts.ariaLabel.current ?? this.provider.soma?.langs.ts(FORM_LANGS.RESET) ?? 'Reset' + () => this.opts.ariaLabel.current ?? this.provider.soma.langs.ts(FORM_LANGS.RESET) ?? 'Reset' ); readonly onclick = () => { @@ -229,10 +238,12 @@ export class FormErrorSummaryProvider { private constructor(opts: FormErrorSummaryOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(formMorfo, {}) - .part('error-summary', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FormProvider.require(); + this.runtimePart = this.provider.runtime.part('error-summary', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly formErrors: string[] = $derived.by(() => this.provider.form.errors[''] ?? []);