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;