From 959439f5faa1180f9162e22432c14f87792686be Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 15 May 2026 02:22:00 +0200 Subject: [PATCH] Consolidate Soma input runtimes --- .../editable/editable-provider.svelte.ts | 81 +++++++++++-------- .../rating-group-provider.svelte.ts | 31 +++---- .../search-field-provider.svelte.ts | 53 ++++++------ .../tag-group/tag-group-provider.svelte.ts | 56 ++++++++----- 4 files changed, 125 insertions(+), 96 deletions(-) diff --git a/src/uix/soma/components/editable/editable-provider.svelte.ts b/src/uix/soma/components/editable/editable-provider.svelte.ts index 55be15d12..53a484904 100644 --- a/src/uix/soma/components/editable/editable-provider.svelte.ts +++ b/src/uix/soma/components/editable/editable-provider.svelte.ts @@ -10,7 +10,7 @@ import { EDITABLE_LANGS } from './langs'; import { editableMorfo } from '../../../morfo/components/editable'; const attrs = createAttrs(editableMorfo); -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; export type ActivationMode = 'focus' | 'click' | 'dblclick' | 'none'; @@ -41,6 +41,8 @@ interface EditableOpts export class EditableProvider { readonly opts: EditableOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('Editable'); static get(): EditableProvider | undefined { return this.ctx.getOr(undefined) as EditableProvider | undefined; @@ -53,8 +55,6 @@ export class EditableProvider { return new EditableProvider(opts); } - readonly soma = Soma.get(); - // Draft value (in-progress edit) draft = $state(''); inputRef = state(null); @@ -62,9 +62,14 @@ export class EditableProvider { private constructor(opts: EditableOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(editableMorfo, {}) - .part('provider', { id: opts.id, ref: opts.ref, owner: this, context: EditableProvider.ctx }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(editableMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: EditableProvider.ctx + }); this.draft = opts.value.current; @@ -149,10 +154,12 @@ export class EditableAreaProvider { private constructor(opts: EditableAreaOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(editableMorfo, {}) - .part('area', { id: opts.id, ref: opts.ref, owner: this }); this.provider = EditableProvider.require(); + this.runtimePart = this.provider.runtime.part('area', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => @@ -180,10 +187,12 @@ export class EditablePreviewProvider { private constructor(opts: EditablePreviewOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(editableMorfo, {}) - .part('preview', { id: opts.id, ref: opts.ref, owner: this }); this.provider = EditableProvider.require(); + this.runtimePart = this.provider.runtime.part('preview', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly displayValue = $derived.by( @@ -244,17 +253,15 @@ export class EditableInputProvider { private constructor(opts: EditableInputOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(editableMorfo, {}) - .part('input', { - id: opts.id, - ref: opts.ref, - owner: this, - onRefChange: (el) => { - this.provider.inputRef.current = el; - } - }); this.provider = EditableProvider.require(); + this.runtimePart = this.provider.runtime.part('input', { + id: opts.id, + ref: opts.ref, + owner: this, + onRefChange: (el) => { + this.provider.inputRef.current = el; + } + }); } readonly oninput = (e: Event) => { @@ -335,10 +342,12 @@ export class EditableEditTriggerProvider { private constructor(opts: EditableEditTriggerOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(editableMorfo, {}) - .part('edit-trigger', { id: opts.id, ref: opts.ref, owner: this }); this.provider = EditableProvider.require(); + this.runtimePart = this.provider.runtime.part('edit-trigger', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly onclick = (_e: SomaMouseEvent) => { @@ -350,7 +359,7 @@ export class EditableEditTriggerProvider { ...this.runtimePart.props, type: 'button' as const, hidden: this.provider.isEditing || undefined, - 'aria-label': this.provider.soma?.langs.ts(EDITABLE_LANGS.EDIT), + 'aria-label': this.provider.soma.langs.ts(EDITABLE_LANGS.EDIT), 'data-disabled': boolToEmptyStrOrUndef(this.provider.opts.disabled.current), 'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current), disabled: @@ -375,10 +384,12 @@ export class EditableSubmitTriggerProvider { private constructor(opts: EditableSubmitTriggerOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(editableMorfo, {}) - .part('submit-trigger', { id: opts.id, ref: opts.ref, owner: this }); this.provider = EditableProvider.require(); + this.runtimePart = this.provider.runtime.part('submit-trigger', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly onclick = (_e: SomaMouseEvent) => { @@ -390,7 +401,7 @@ export class EditableSubmitTriggerProvider { ...this.runtimePart.props, type: 'button' as const, hidden: !this.provider.isEditing || undefined, - 'aria-label': this.provider.soma?.langs.ts(EDITABLE_LANGS.SUBMIT), + 'aria-label': this.provider.soma.langs.ts(EDITABLE_LANGS.SUBMIT), 'data-disabled': boolToEmptyStrOrUndef(this.provider.opts.disabled.current), 'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current), disabled: @@ -415,10 +426,12 @@ export class EditableCancelTriggerProvider { private constructor(opts: EditableCancelTriggerOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(editableMorfo, {}) - .part('cancel-trigger', { id: opts.id, ref: opts.ref, owner: this }); this.provider = EditableProvider.require(); + this.runtimePart = this.provider.runtime.part('cancel-trigger', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly onclick = (_e: SomaMouseEvent) => { @@ -430,7 +443,7 @@ export class EditableCancelTriggerProvider { ...this.runtimePart.props, type: 'button' as const, hidden: !this.provider.isEditing || undefined, - 'aria-label': this.provider.soma?.langs.ts(EDITABLE_LANGS.CANCEL), + 'aria-label': this.provider.soma.langs.ts(EDITABLE_LANGS.CANCEL), 'data-disabled': boolToEmptyStrOrUndef(this.provider.opts.disabled.current), 'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current), disabled: this.provider.opts.disabled.current || undefined, 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 8d86543e6..1f105a7ab 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 @@ -16,7 +16,7 @@ import { RATING_GROUP_LANGS } from './langs'; import type { RatingItemDescriptor, RatingItemState } from './types'; import { ratingGroupMorfo } from '../../../morfo/components/rating-group'; -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; // ── Helpers ──────────────────────────────────────────────────────────────── @@ -56,6 +56,8 @@ interface RatingGroupOpts export class RatingGroupProvider { readonly opts: RatingGroupOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('RatingGroup'); static get(): RatingGroupProvider | undefined { return this.ctx.getOr(undefined) as RatingGroupProvider | undefined; @@ -68,7 +70,6 @@ export class RatingGroupProvider { return new RatingGroupProvider(opts); } - readonly soma = Soma.get(); readonly field = FieldProvider.get(); hoverValue = $state(null); @@ -76,14 +77,14 @@ export class RatingGroupProvider { private constructor(opts: RatingGroupOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(ratingGroupMorfo, {}) - .part('provider', { - id: opts.id, - ref: opts.ref, - owner: this, - context: RatingGroupProvider.ctx - }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(ratingGroupMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: RatingGroupProvider.ctx + }); } // ── Field-aware flags (OR-merge) ───────────────────────────────────────── @@ -121,7 +122,7 @@ export class RatingGroupProvider { }); readonly resolvedAriaLabel: Active = readableActive( - () => this.opts.ariaLabel.current || this.soma?.langs.ts(RATING_GROUP_LANGS.LABEL) || undefined + () => this.opts.ariaLabel.current || this.soma.langs.ts(RATING_GROUP_LANGS.LABEL) || undefined ); // ── Mutations ──────────────────────────────────────────────────────────── @@ -298,10 +299,12 @@ export class RatingGroupItemProvider { private constructor(opts: RatingGroupItemOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(ratingGroupMorfo, {}) - .part('item', { id: opts.id, ref: opts.ref, owner: this }); this.provider = RatingGroupProvider.require(); + this.runtimePart = this.provider.runtime.part('item', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly isDisabled = $derived.by(() => this.opts.disabled.current || this.provider.isDisabled); diff --git a/src/uix/soma/components/search-field/search-field-provider.svelte.ts b/src/uix/soma/components/search-field/search-field-provider.svelte.ts index f6a886b2c..e210971ab 100644 --- a/src/uix/soma/components/search-field/search-field-provider.svelte.ts +++ b/src/uix/soma/components/search-field/search-field-provider.svelte.ts @@ -20,7 +20,7 @@ import { FieldProvider } from '../field/field-provider.svelte'; import { SEARCH_FIELD_LANGS } from './langs'; import { searchFieldMorfo } from '../../../morfo/components/search-field'; -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; // ── Root provider ────────────────────────────────────────────────────────── @@ -47,6 +47,8 @@ interface SearchFieldOpts export class SearchFieldProvider { readonly opts: SearchFieldOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('SearchField'); static get(): SearchFieldProvider | undefined { return this.ctx.getOr(undefined) as SearchFieldProvider | undefined; @@ -59,7 +61,6 @@ export class SearchFieldProvider { return new SearchFieldProvider(opts); } - readonly soma = Soma.get(); readonly field = FieldProvider.get(); inputRef = state(null); @@ -67,14 +68,14 @@ export class SearchFieldProvider { private constructor(opts: SearchFieldOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(searchFieldMorfo, {}) - .part('provider', { - id: opts.id, - ref: opts.ref, - owner: this, - context: SearchFieldProvider.ctx - }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(searchFieldMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: SearchFieldProvider.ctx + }); // Register the Input's id with the parent Field so `Field.Label`'s // `for` attribute lands on the interactive element. Direct assign — // NOT $effect (A30). @@ -104,9 +105,7 @@ export class SearchFieldProvider { // If a Field.Label is present (labelId registered), it takes over via // aria-labelledby — no need for aria-label on the input. if (this.field?.labelId.current) return undefined; - return ( - this.opts.ariaLabel.current || this.soma?.langs.ts(SEARCH_FIELD_LANGS.LABEL) || undefined - ); + return this.opts.ariaLabel.current || this.soma.langs.ts(SEARCH_FIELD_LANGS.LABEL) || undefined; }); setInputRef(el: HTMLInputElement | null) { @@ -205,17 +204,15 @@ export class SearchFieldInputProvider { private constructor(opts: SearchFieldInputOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(searchFieldMorfo, {}) - .part('input', { - id: opts.id, - ref: opts.ref, - owner: this, - onRefChange: (el) => { - this.provider.setInputRef(el as HTMLInputElement | null); - } - }); this.provider = SearchFieldProvider.require(); + this.runtimePart = this.provider.runtime.part('input', { + id: opts.id, + ref: opts.ref, + owner: this, + onRefChange: (el) => { + this.provider.setInputRef(el as HTMLInputElement | null); + } + }); } readonly props = $derived.by(() => @@ -264,10 +261,12 @@ export class SearchFieldClearTriggerProvider { private constructor(opts: SearchFieldClearTriggerOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(searchFieldMorfo, {}) - .part('clear-trigger', { id: opts.id, ref: opts.ref, owner: this }); this.provider = SearchFieldProvider.require(); + this.runtimePart = this.provider.runtime.part('clear-trigger', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly onclick = (_e: SomaMouseEvent) => { @@ -277,7 +276,7 @@ export class SearchFieldClearTriggerProvider { readonly resolvedAriaLabel: Active = readableActive( () => this.opts.ariaLabel.current || - this.provider.soma?.langs.ts(SEARCH_FIELD_LANGS.CLEAR) || + this.provider.soma.langs.ts(SEARCH_FIELD_LANGS.CLEAR) || undefined ); 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 644a34688..f011f4c40 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 @@ -15,7 +15,7 @@ import { TAG_GROUP_LANGS } from './langs'; import type { TagGroupSelectionMode } from './types'; import { tagGroupMorfo } from '../../../morfo/components/tag-group'; -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; const attrs = createAttrs(tagGroupMorfo); @@ -38,6 +38,8 @@ interface TagGroupOpts export class TagGroupProvider { readonly opts: TagGroupOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('TagGroup'); static get(): TagGroupProvider | undefined { return this.ctx.getOr(undefined) as TagGroupProvider | undefined; @@ -50,25 +52,28 @@ export class TagGroupProvider { return new TagGroupProvider(opts); } - readonly soma = Soma.get(); - /** Registered by `TagGroup.Label` on mount. */ labelId = state(''); private constructor(opts: TagGroupOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(tagGroupMorfo, {}) - .part('provider', { id: opts.id, ref: opts.ref, owner: this, context: TagGroupProvider.ctx }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(tagGroupMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: TagGroupProvider.ctx + }); } readonly resolvedDir: Active = readableActive( - () => this.soma?.prefs.getDir() ?? 'ltr' + () => this.soma.prefs.getDir() ?? 'ltr' ); readonly resolvedAriaLabel: Active = readableActive(() => { if (this.opts.ariaLabelledby.current || this.labelId.current) return undefined; - return this.opts.ariaLabel.current || this.soma?.langs.ts(TAG_GROUP_LANGS.LABEL) || undefined; + return this.opts.ariaLabel.current || this.soma.langs.ts(TAG_GROUP_LANGS.LABEL) || undefined; }); readonly resolvedAriaLabelledby = $derived.by( @@ -246,10 +251,12 @@ export class TagGroupLabelProvider { private constructor(opts: TagGroupLabelOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(tagGroupMorfo, {}) - .part('label', { id: opts.id, ref: opts.ref, owner: this }); this.provider = TagGroupProvider.require(); + this.runtimePart = this.provider.runtime.part('label', { + id: opts.id, + ref: opts.ref, + owner: this + }); // A30: direct assignment in constructor, not $effect. this.provider.labelId.current = opts.id.current; } @@ -291,10 +298,13 @@ export class TagGroupItemProvider { private constructor(opts: TagGroupItemOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(tagGroupMorfo, {}) - .part('item', { id: opts.id, ref: opts.ref, owner: this, context: TagGroupItemProvider.ctx }); this.provider = TagGroupProvider.require(); + this.runtimePart = this.provider.runtime.part('item', { + id: opts.id, + ref: opts.ref, + owner: this, + context: TagGroupItemProvider.ctx + }); } readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current)); @@ -370,10 +380,12 @@ export class TagGroupLinkProvider { // the root's selector `[${attrs.item}]` picks it up too — we add a // DOM-query bridge via `data-tag-group-item` + 'data-tag-group-link'. this.opts = opts; - this.runtimePart = Soma.require() - .runtime(tagGroupMorfo, {}) - .part('link', { id: opts.id, ref: opts.ref, owner: this }); this.provider = TagGroupProvider.require(); + this.runtimePart = this.provider.runtime.part('link', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current)); @@ -428,17 +440,19 @@ export class TagGroupRemoveButtonProvider { private constructor(opts: TagGroupRemoveButtonOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(tagGroupMorfo, {}) - .part('remove-button', { id: opts.id, ref: opts.ref, owner: this }); this.provider = TagGroupProvider.require(); this.item = TagGroupItemProvider.require(); + this.runtimePart = this.provider.runtime.part('remove-button', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly resolvedAriaLabel = $derived.by(() => { if (this.opts.ariaLabel.current) return this.opts.ariaLabel.current; return ( - this.provider.soma?.langs.t(TAG_GROUP_LANGS.REMOVE, { + this.provider.soma.langs.t(TAG_GROUP_LANGS.REMOVE, { tag: this.item.opts.value.current }) || 'Remove' );