Consolidate Soma input runtimes

active-uix
dev 5 months ago
parent 6df2f062ad
commit 959439f5fa

@ -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<EditableProvider>('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<HTMLElement | null>(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<HTMLButtonElement>) => {
@ -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<HTMLButtonElement>) => {
@ -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<HTMLButtonElement>) => {
@ -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,

@ -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<RatingGroupProvider>('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<number | null>(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<string | undefined> = 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);

@ -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<SearchFieldProvider>('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<HTMLInputElement | null>(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<HTMLButtonElement>) => {
@ -277,7 +276,7 @@ export class SearchFieldClearTriggerProvider {
readonly resolvedAriaLabel: Active<string | undefined> = readableActive(
() =>
this.opts.ariaLabel.current ||
this.provider.soma?.langs.ts(SEARCH_FIELD_LANGS.CLEAR) ||
this.provider.soma.langs.ts(SEARCH_FIELD_LANGS.CLEAR) ||
undefined
);

@ -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<TagGroupProvider>('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<Direction> = readableActive(
() => this.soma?.prefs.getDir() ?? 'ltr'
() => this.soma.prefs.getDir() ?? 'ltr'
);
readonly resolvedAriaLabel: Active<string | undefined> = 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'
);

Loading…
Cancel
Save

Powered by TurnKey Linux.