From 8ddd55309a1fbcbe13ef2f8a4cfe36cc8aee79f1 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 27 Jun 2026 14:59:31 +0200 Subject: [PATCH] =?UTF-8?q?feat(soma):=20GradientBuilder=20soma=20layer=20?= =?UTF-8?q?=E2=80=94=20render-ready=20providers=20+=20wrappers=20(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the headless layer of the GradientBuilder (still ACTIVE_DEV_TRACK): - Providers expose `.props` (renderProps() + handlers) and register per-part sources for the morfo's value-bound aria (aria-valuenow / valuetext / label on each stop) — every stop is a keyboard-accessible role=slider thumb. - Svelte wrappers: Provider (the editor root, bindable `value: Gradient`), Track (click-empty-to-add drag surface), Stop (the slider thumb) + types + barrel. The eidos composes Preview / AddStop / AngleDial / KindSwitch over these via GradientBuilderProvider.require(). check: 0 new errors, type-clean. Co-Authored-By: Claude Opus 4.8 --- .../components/gradient-builder-stop.svelte | 37 ++++++++++++ .../components/gradient-builder-track.svelte | 35 +++++++++++ .../components/gradient-builder.svelte | 54 +++++++++++++++++ .../components/gradient-builder/exports.ts | 15 +++++ .../gradient-builder-provider.svelte.ts | 59 +++++++++++++++---- .../soma/components/gradient-builder/index.ts | 1 + .../soma/components/gradient-builder/types.ts | 28 +++++++++ 7 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 src/uix/soma/components/gradient-builder/components/gradient-builder-stop.svelte create mode 100644 src/uix/soma/components/gradient-builder/components/gradient-builder-track.svelte create mode 100644 src/uix/soma/components/gradient-builder/components/gradient-builder.svelte create mode 100644 src/uix/soma/components/gradient-builder/exports.ts create mode 100644 src/uix/soma/components/gradient-builder/index.ts create mode 100644 src/uix/soma/components/gradient-builder/types.ts diff --git a/src/uix/soma/components/gradient-builder/components/gradient-builder-stop.svelte b/src/uix/soma/components/gradient-builder/components/gradient-builder-stop.svelte new file mode 100644 index 000000000..5488f4f19 --- /dev/null +++ b/src/uix/soma/components/gradient-builder/components/gradient-builder-stop.svelte @@ -0,0 +1,37 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/src/uix/soma/components/gradient-builder/components/gradient-builder-track.svelte b/src/uix/soma/components/gradient-builder/components/gradient-builder-track.svelte new file mode 100644 index 000000000..9081ac4ff --- /dev/null +++ b/src/uix/soma/components/gradient-builder/components/gradient-builder-track.svelte @@ -0,0 +1,35 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/src/uix/soma/components/gradient-builder/components/gradient-builder.svelte b/src/uix/soma/components/gradient-builder/components/gradient-builder.svelte new file mode 100644 index 000000000..da5f3f405 --- /dev/null +++ b/src/uix/soma/components/gradient-builder/components/gradient-builder.svelte @@ -0,0 +1,54 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/src/uix/soma/components/gradient-builder/exports.ts b/src/uix/soma/components/gradient-builder/exports.ts new file mode 100644 index 000000000..cac26a4b3 --- /dev/null +++ b/src/uix/soma/components/gradient-builder/exports.ts @@ -0,0 +1,15 @@ +export { default as Provider } from './components/gradient-builder.svelte'; +export { default as Track } from './components/gradient-builder-track.svelte'; +export { default as Stop } from './components/gradient-builder-stop.svelte'; + +export { + GradientBuilderProvider, + GradientStopProvider, + GradientTrackProvider +} from './gradient-builder-provider.svelte'; + +export type { + GradientBuilderProps as ProviderProps, + GradientTrackProps as TrackProps, + GradientStopProps as StopProps +} from './types'; diff --git a/src/uix/soma/components/gradient-builder/gradient-builder-provider.svelte.ts b/src/uix/soma/components/gradient-builder/gradient-builder-provider.svelte.ts index b1ceb697b..193b14919 100644 --- a/src/uix/soma/components/gradient-builder/gradient-builder-provider.svelte.ts +++ b/src/uix/soma/components/gradient-builder/gradient-builder-provider.svelte.ts @@ -1,5 +1,6 @@ import { context, type WithRefOpts } from '../../provider'; import { type Active, type ActiveProps, type StateProps } from '$libs/reactive'; +import { boolToEmptyStrOrUndef } from '$adom'; import type { OnChangeFn } from '../../types'; import { KEYS } from '../../keyboard'; import type { Gradient, GradientStop, StopColor, GradientKind } from '$libs/gradient'; @@ -211,6 +212,11 @@ export class GradientBuilderProvider { void this.runtime.trigger('commit-reset'); } + /** Provider element attrs — morfo-resolved + `data-kind` (set imperatively). */ + get props() { + return { ...this.runtimePart.renderProps(), 'data-kind': this.kind }; + } + /** Map a pointer x to a position 0..1 along the registered track. */ positionFromPointer(clientX: number): number { const el = this.trackRef; @@ -261,7 +267,14 @@ export class GradientStopProvider { ref: opts.ref, owner: this, context: GradientStopProvider.ctx, - syncAttrs: true + syncAttrs: true, + // Per-stop sources for the morfo's value-bound aria (aria-valuenow / + // aria-valuetext / aria-label) — resolved via `renderProps()`. + props: { + position: () => this.position, + valueText: () => this.valueText, + stopLabel: () => this.stopLabel + } }); } @@ -275,9 +288,30 @@ export class GradientStopProvider { get isSelected(): boolean { return this.provider.selectedIndex === this.index; } + /** Live-announced description: "Stop 2 of 4, 40%". */ + get valueText(): string { + return `Stop ${this.index + 1} of ${this.provider.stops.length}, ${this.position}%`; + } + get stopLabel(): string { + return `Color stop ${this.index + 1}`; + } + + /** Element attrs — morfo-resolved aria + state attrs + the drag/keyboard handlers. */ + get props() { + return { + ...this.runtimePart.renderProps(), + tabindex: 0, + 'data-selected': boolToEmptyStrOrUndef(this.isSelected), + 'data-dragging': boolToEmptyStrOrUndef(this.provider.activeDragIndex === this.index), + onkeydown: this.onkeydown, + onpointerdown: this.onpointerdown, + onpointermove: this.onpointermove, + onpointerup: this.onpointerup + }; + } /** The a11y differentiator: every stop is a real slider with arrow/Home/End/Delete. */ - onkeydown(e: { key: string; shiftKey?: boolean; preventDefault(): void }): void { + readonly onkeydown = (e: KeyboardEvent): void => { const i = this.index; const step = e.shiftKey ? COARSE_STEP : STEP; switch (e.key) { @@ -312,21 +346,22 @@ export class GradientStopProvider { this.provider.selectStop(i); break; } - } + }; - onpointerdown(e: { clientX: number; pointerId: number; currentTarget: EventTarget | null }): void { + readonly onpointerdown = (e: PointerEvent): void => { const el = e.currentTarget as HTMLElement | null; el?.setPointerCapture?.(e.pointerId); this.provider.startStopDrag(this.index); - } - onpointermove(e: { clientX: number }): void { + }; + readonly onpointermove = (e: PointerEvent): void => { + if (this.provider.activeDragIndex == null) return; this.provider.dragStopTo(e.clientX); - } - onpointerup(e: { pointerId: number; currentTarget: EventTarget | null }): void { + }; + readonly onpointerup = (e: PointerEvent): void => { const el = e.currentTarget as HTMLElement | null; el?.releasePointerCapture?.(e.pointerId); this.provider.endStopDrag(); - } + }; } // ── Track (the stop rail / drag surface — click empty space to add a stop) ──── @@ -358,9 +393,13 @@ export class GradientTrackProvider { } /** Click on empty track → add a stop at that position. */ - onpointerdown(e: { clientX: number; target: EventTarget | null; currentTarget: EventTarget | null }): void { + readonly onpointerdown = (e: PointerEvent): void => { // Only the bare track adds — clicks on a stop are handled by the Stop. if (e.target !== e.currentTarget) return; this.provider.addStop(this.provider.positionFromPointer(e.clientX)); + }; + + get props() { + return { ...this.runtimePart.renderProps(), onpointerdown: this.onpointerdown }; } } diff --git a/src/uix/soma/components/gradient-builder/index.ts b/src/uix/soma/components/gradient-builder/index.ts new file mode 100644 index 000000000..8570a426a --- /dev/null +++ b/src/uix/soma/components/gradient-builder/index.ts @@ -0,0 +1 @@ +export * from './exports'; diff --git a/src/uix/soma/components/gradient-builder/types.ts b/src/uix/soma/components/gradient-builder/types.ts new file mode 100644 index 000000000..320866262 --- /dev/null +++ b/src/uix/soma/components/gradient-builder/types.ts @@ -0,0 +1,28 @@ +import type { WithChild, Without, OnChangeFn, PrimitiveDivAttributes } from '../../types'; +import type { Gradient } from '$libs/gradient'; + +export type GradientBuilderProps = WithChild<{ + /** Unique identifier. Auto-generated if omitted. */ + id?: string; + /** The gradient being edited. Bindable. */ + value?: Gradient; + /** Called on every settled change (stop add / move / remove / recolor, kind, angle). */ + onValueCommit?: OnChangeFn; + /** @default false */ + disabled?: boolean; +}> & + Without; + +export type GradientTrackProps = WithChild<{ + /** Unique identifier. Auto-generated if omitted. */ + id?: string; +}> & + Without; + +export type GradientStopProps = WithChild<{ + /** Unique identifier. Auto-generated if omitted. */ + id?: string; + /** This stop's index in the gradient's stop list. @default 0 */ + index?: number; +}> & + Without;