From b1b919a8b68778038df088269ef3c91ad1cf3080 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 15 May 2026 02:45:53 +0200 Subject: [PATCH] Consolidate Soma tree carousel runtimes --- .../carousel/carousel-provider.svelte.ts | 101 ++++++++++-------- .../tree-grid/tree-grid-provider.svelte.ts | 78 ++++++++------ .../tree-view/tree-view-provider.svelte.ts | 82 ++++++++------ 3 files changed, 152 insertions(+), 109 deletions(-) diff --git a/src/uix/soma/components/carousel/carousel-provider.svelte.ts b/src/uix/soma/components/carousel/carousel-provider.svelte.ts index 80e6a2df4..ae897d5f6 100644 --- a/src/uix/soma/components/carousel/carousel-provider.svelte.ts +++ b/src/uix/soma/components/carousel/carousel-provider.svelte.ts @@ -8,7 +8,7 @@ import { Soma } from '../../core/soma.svelte'; import { Gesture } from '../../layers/gesture'; import { CAROUSEL_LANGS } from './langs'; import { carouselMorfo } from '../../../morfo/components/carousel'; -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; const attrs = createAttrs(carouselMorfo); @@ -34,6 +34,8 @@ interface CarouselOpts export class CarouselProvider { readonly opts: CarouselOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('Carousel'); static get(): CarouselProvider | undefined { return this.ctx.getOr(undefined) as CarouselProvider | undefined; @@ -46,8 +48,6 @@ export class CarouselProvider { return new CarouselProvider(opts); } - readonly soma = Soma.get(); - // Refs — set by the respective child providers. viewportRef = $state(null); itemGroupRef = $state(null); @@ -63,9 +63,14 @@ export class CarouselProvider { private constructor(opts: CarouselOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(carouselMorfo, {}) - .part('provider', { id: opts.id, ref: opts.ref, owner: this, context: CarouselProvider.ctx }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(carouselMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: CarouselProvider.ctx + }); this.gesture = Gesture.base({ ref: readableActive(() => this.itemGroupRef), @@ -233,7 +238,7 @@ export class CarouselProvider { })); readonly resolvedAriaLabel: Active = readableActive( - () => this.opts.ariaLabel.current || this.soma?.langs.ts(CAROUSEL_LANGS.LABEL) || undefined + () => this.opts.ariaLabel.current || this.soma.langs.ts(CAROUSEL_LANGS.LABEL) || undefined ); readonly props = $derived.by(() => @@ -271,17 +276,15 @@ export class CarouselViewportProvider { private constructor(opts: CarouselViewportOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(carouselMorfo, {}) - .part('viewport', { - id: opts.id, - ref: opts.ref, - owner: this, - onRefChange: (el) => { - this.provider.viewportRef = el; - } - }); this.provider = CarouselProvider.require(); + this.runtimePart = this.provider.runtime.part('viewport', { + id: opts.id, + ref: opts.ref, + owner: this, + onRefChange: (el) => { + this.provider.viewportRef = el; + } + }); } readonly props = $derived.by(() => @@ -308,17 +311,15 @@ export class CarouselItemGroupProvider { private constructor(opts: CarouselItemGroupOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(carouselMorfo, {}) - .part('item-group', { - id: opts.id, - ref: opts.ref, - owner: this, - onRefChange: (el) => { - this.provider.itemGroupRef = el; - } - }); this.provider = CarouselProvider.require(); + this.runtimePart = this.provider.runtime.part('item-group', { + id: opts.id, + ref: opts.ref, + owner: this, + onRefChange: (el) => { + this.provider.itemGroupRef = el; + } + }); // Drag lifecycle — track offset during drag, commit on release. let wasDragging = false; @@ -369,10 +370,12 @@ export class CarouselItemProvider { private constructor(opts: CarouselItemOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(carouselMorfo, {}) - .part('item', { id: opts.id, ref: opts.ref, owner: this }); this.provider = CarouselProvider.require(); + this.runtimePart = this.provider.runtime.part('item', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly isActive = $derived.by( @@ -417,10 +420,12 @@ export class CarouselPrevTriggerProvider { private constructor(opts: CarouselPrevTriggerOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(carouselMorfo, {}) - .part('prev-trigger', { id: opts.id, ref: opts.ref, owner: this }); this.provider = CarouselProvider.require(); + this.runtimePart = this.provider.runtime.part('prev-trigger', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly isDisabled = $derived.by(() => this.provider.isDisabled || !this.provider.canGoPrev); @@ -434,8 +439,7 @@ export class CarouselPrevTriggerProvider { this.runtimePart.assert({ ...this.runtimePart.props, type: 'button' as const, - 'aria-label': - this.opts.ariaLabel.current || this.provider.soma?.langs.ts(CAROUSEL_LANGS.PREV), + 'aria-label': this.opts.ariaLabel.current || this.provider.soma.langs.ts(CAROUSEL_LANGS.PREV), disabled: this.isDisabled || undefined, 'data-disabled': boolToEmptyStrOrUndef(this.isDisabled), onclick: this.onclick @@ -459,10 +463,12 @@ export class CarouselNextTriggerProvider { private constructor(opts: CarouselNextTriggerOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(carouselMorfo, {}) - .part('next-trigger', { id: opts.id, ref: opts.ref, owner: this }); this.provider = CarouselProvider.require(); + this.runtimePart = this.provider.runtime.part('next-trigger', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly isDisabled = $derived.by(() => this.provider.isDisabled || !this.provider.canGoNext); @@ -476,8 +482,7 @@ export class CarouselNextTriggerProvider { this.runtimePart.assert({ ...this.runtimePart.props, type: 'button' as const, - 'aria-label': - this.opts.ariaLabel.current || this.provider.soma?.langs.ts(CAROUSEL_LANGS.NEXT), + 'aria-label': this.opts.ariaLabel.current || this.provider.soma.langs.ts(CAROUSEL_LANGS.NEXT), disabled: this.isDisabled || undefined, 'data-disabled': boolToEmptyStrOrUndef(this.isDisabled), onclick: this.onclick @@ -500,10 +505,12 @@ export class CarouselIndicatorGroupProvider { private constructor(opts: CarouselIndicatorGroupOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(carouselMorfo, {}) - .part('indicator-group', { id: opts.id, ref: opts.ref, owner: this }); this.provider = CarouselProvider.require(); + this.runtimePart = this.provider.runtime.part('indicator-group', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => @@ -532,10 +539,12 @@ export class CarouselIndicatorProvider { private constructor(opts: CarouselIndicatorOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(carouselMorfo, {}) - .part('indicator', { id: opts.id, ref: opts.ref, owner: this }); this.provider = CarouselProvider.require(); + this.runtimePart = this.provider.runtime.part('indicator', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly isActive = $derived.by( diff --git a/src/uix/soma/components/tree-grid/tree-grid-provider.svelte.ts b/src/uix/soma/components/tree-grid/tree-grid-provider.svelte.ts index 1420dc7c8..1660a8cb9 100644 --- a/src/uix/soma/components/tree-grid/tree-grid-provider.svelte.ts +++ b/src/uix/soma/components/tree-grid/tree-grid-provider.svelte.ts @@ -15,7 +15,7 @@ import { TREE_GRID_LANGS } from './langs'; import type { TreeGridSelectionMode } from './types'; import { treeGridMorfo } from '../../../morfo/components/tree-grid'; -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; const attrs = createAttrs(treeGridMorfo); @@ -44,6 +44,8 @@ interface TreeGridOpts export class TreeGridProvider { readonly opts: TreeGridOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('TreeGrid'); static get(): TreeGridProvider | undefined { return this.ctx.getOr(undefined) as TreeGridProvider | undefined; @@ -56,17 +58,20 @@ export class TreeGridProvider { return new TreeGridProvider(opts); } - readonly soma = Soma.get(); - private typeaheadBuffer = ''; private typeaheadTimer: ReturnType | null = null; private anchor: string | null = null; private constructor(opts: TreeGridOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeGridMorfo, {}) - .part('provider', { id: opts.id, ref: opts.ref, owner: this, context: TreeGridProvider.ctx }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(treeGridMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: TreeGridProvider.ctx + }); $effect(() => { return () => { if (this.typeaheadTimer) clearTimeout(this.typeaheadTimer); @@ -75,13 +80,13 @@ export class TreeGridProvider { } readonly resolvedDir: Active = readableActive( - () => this.soma?.prefs.getDir() ?? 'ltr' + () => this.soma.prefs.getDir() ?? 'ltr' ); readonly resolvedAriaLabel: Active = readableActive(() => this.opts.ariaLabelledby.current ? undefined - : this.opts.ariaLabel.current || this.soma?.langs.ts(TREE_GRID_LANGS.LABEL) || undefined + : this.opts.ariaLabel.current || this.soma.langs.ts(TREE_GRID_LANGS.LABEL) || undefined ); readonly isEmpty = $derived.by(() => this.opts.value.current.length === 0); @@ -448,10 +453,12 @@ export class TreeGridHeaderProvider { private constructor(opts: TreeGridHeaderOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeGridMorfo, {}) - .part('header', { id: opts.id, ref: opts.ref, owner: this }); - TreeGridProvider.require(); + const provider = TreeGridProvider.require(); + this.runtimePart = provider.runtime.part('header', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => @@ -472,10 +479,12 @@ export class TreeGridColumnHeaderProvider { private constructor(opts: TreeGridColumnHeaderOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeGridMorfo, {}) - .part('column-header', { id: opts.id, ref: opts.ref, owner: this }); - TreeGridProvider.require(); + const provider = TreeGridProvider.require(); + this.runtimePart = provider.runtime.part('column-header', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => @@ -519,12 +528,15 @@ export class TreeGridRowProvider { private constructor(opts: TreeGridRowOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeGridMorfo, {}) - .part('row', { id: opts.id, ref: opts.ref, owner: this, context: TreeGridRowProvider.ctx }); this.provider = TreeGridProvider.require(); this.parentRow = TreeGridRowProvider.get(); this.level = (this.parentRow?.level ?? 0) + 1; + this.runtimePart = this.provider.runtime.part('row', { + id: opts.id, + ref: opts.ref, + owner: this, + context: TreeGridRowProvider.ctx + }); } readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current)); @@ -592,10 +604,12 @@ export class TreeGridRowChildrenProvider { private constructor(opts: TreeGridRowChildrenOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeGridMorfo, {}) - .part('row-children', { id: opts.id, ref: opts.ref, owner: this }); this.row = TreeGridRowProvider.require(); + this.runtimePart = this.row.provider.runtime.part('row-children', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly isExpanded = $derived.by(() => this.row.isExpanded); @@ -621,10 +635,12 @@ export class TreeGridCellProvider { private constructor(opts: TreeGridCellOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeGridMorfo, {}) - .part('cell', { id: opts.id, ref: opts.ref, owner: this }); - TreeGridProvider.require(); + const provider = TreeGridProvider.require(); + this.runtimePart = provider.runtime.part('cell', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => @@ -652,17 +668,19 @@ export class TreeGridExpandTriggerProvider { private constructor(opts: TreeGridExpandTriggerOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeGridMorfo, {}) - .part('expand-trigger', { id: opts.id, ref: opts.ref, owner: this }); this.provider = TreeGridProvider.require(); this.row = TreeGridRowProvider.require(); + this.runtimePart = this.provider.runtime.part('expand-trigger', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly resolvedAriaLabel = $derived.by(() => { if (this.opts.ariaLabel.current) return this.opts.ariaLabel.current; const key = this.row.isExpanded ? TREE_GRID_LANGS.COLLAPSE : TREE_GRID_LANGS.EXPAND; - return this.provider.soma?.langs.ts(key); + return this.provider.soma.langs.ts(key); }); readonly onclick = (e: MouseEvent) => { diff --git a/src/uix/soma/components/tree-view/tree-view-provider.svelte.ts b/src/uix/soma/components/tree-view/tree-view-provider.svelte.ts index 149c9b5d1..f21d6d59c 100644 --- a/src/uix/soma/components/tree-view/tree-view-provider.svelte.ts +++ b/src/uix/soma/components/tree-view/tree-view-provider.svelte.ts @@ -8,7 +8,7 @@ import { Soma } from '../../core/soma.svelte'; import { TREE_VIEW_LANGS } from './langs'; import { treeViewMorfo } from '../../../morfo/components/tree-view'; -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; const attrs = createAttrs(treeViewMorfo); @@ -35,6 +35,8 @@ interface TreeViewOpts export class TreeViewProvider { readonly opts: TreeViewOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('TreeView'); static get(): TreeViewProvider | undefined { return this.ctx.getOr(undefined) as TreeViewProvider | undefined; @@ -47,8 +49,6 @@ export class TreeViewProvider { return new TreeViewProvider(opts); } - readonly soma = Soma.get(); - // Focus tracking focusedValue = $state(null); @@ -72,9 +72,14 @@ export class TreeViewProvider { private constructor(opts: TreeViewOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeViewMorfo, {}) - .part('provider', { id: opts.id, ref: opts.ref, owner: this, context: TreeViewProvider.ctx }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(treeViewMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: TreeViewProvider.ctx + }); // Cleanup typeahead timer on unmount $effect(() => { @@ -281,7 +286,7 @@ export class TreeViewProvider { // Consumer override > labelledby reference > default translation. 'aria-label': this.opts.ariaLabelledby.current ? undefined - : (this.opts.ariaLabel.current ?? this.soma?.langs.ts(TREE_VIEW_LANGS.LABEL)), + : (this.opts.ariaLabel.current ?? this.soma.langs.ts(TREE_VIEW_LANGS.LABEL)), 'aria-labelledby': this.opts.ariaLabelledby.current, onkeydown: this.onkeydown } as const) @@ -318,15 +323,13 @@ export class TreeViewBranchProvider { private constructor(opts: TreeViewBranchOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeViewMorfo, {}) - .part('branch', { - id: opts.id, - ref: opts.ref, - owner: this, - context: TreeViewBranchProvider.ctx - }); this.provider = TreeViewProvider.require(); + this.runtimePart = this.provider.runtime.part('branch', { + id: opts.id, + ref: opts.ref, + owner: this, + context: TreeViewBranchProvider.ctx + }); } readonly isExpanded = $derived.by(() => this.provider.isExpanded(this.opts.value.current)); @@ -371,11 +374,13 @@ export class TreeViewBranchControlProvider { private constructor(opts: TreeViewBranchControlOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeViewMorfo, {}) - .part('branch-control', { id: opts.id, ref: opts.ref, owner: this }); this.branch = TreeViewBranchProvider.require(); this.provider = this.branch.provider; + this.runtimePart = this.provider.runtime.part('branch-control', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly onclick = (_e: SomaMouseEvent) => { @@ -415,10 +420,12 @@ export class TreeViewBranchContentProvider { private constructor(opts: TreeViewBranchContentOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeViewMorfo, {}) - .part('branch-content', { id: opts.id, ref: opts.ref, owner: this }); this.branch = TreeViewBranchProvider.require(); + this.runtimePart = this.branch.provider.runtime.part('branch-content', { + id: opts.id, + ref: opts.ref, + owner: this + }); } /** Proxy to `branch.isExpanded` so the wrapper doesn't need `ctx.get()`. */ @@ -449,10 +456,12 @@ export class TreeViewBranchIndicatorProvider { private constructor(opts: TreeViewBranchIndicatorOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeViewMorfo, {}) - .part('branch-indicator', { id: opts.id, ref: opts.ref, owner: this }); this.branch = TreeViewBranchProvider.require(); + this.runtimePart = this.branch.provider.runtime.part('branch-indicator', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly onclick = (e: MouseEvent) => { @@ -486,10 +495,12 @@ export class TreeViewBranchIndentGuideProvider { private constructor(opts: TreeViewBranchIndentGuideOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeViewMorfo, {}) - .part('branch-indent-guide', { id: opts.id, ref: opts.ref, owner: this }); this.branch = TreeViewBranchProvider.require(); + this.runtimePart = this.branch.provider.runtime.part('branch-indent-guide', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => @@ -523,10 +534,12 @@ export class TreeViewItemProvider { private constructor(opts: TreeViewItemOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeViewMorfo, {}) - .part('item', { id: opts.id, ref: opts.ref, owner: this }); this.provider = TreeViewProvider.require(); + this.runtimePart = this.provider.runtime.part('item', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly isSelected = $derived.by(() => this.provider.isSelected(this.opts.value.current)); @@ -571,9 +584,12 @@ export class TreeViewLabelProvider { private constructor(opts: TreeViewLabelOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(treeViewMorfo, {}) - .part('label', { id: opts.id, ref: opts.ref, owner: this }); + const provider = TreeViewProvider.require(); + this.runtimePart = provider.runtime.part('label', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() =>