Consolidate Soma tree carousel runtimes

active-uix
dev 5 months ago
parent 43552dde82
commit b1b919a8b6

@ -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<CarouselProvider>('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<HTMLElement | null>(null);
itemGroupRef = $state<HTMLElement | null>(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<string | undefined> = 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(

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

@ -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<TreeViewProvider>('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<string | null>(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<HTMLElement>) => {
@ -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(() =>

Loading…
Cancel
Save

Powered by TurnKey Linux.