feat(soma): GradientBuilder soma layer — render-ready providers + wrappers (WIP)

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 <noreply@anthropic.com>
active-uix
dev 3 months ago
parent c474fc6bae
commit 8ddd55309a

@ -0,0 +1,37 @@
<script lang="ts">
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { GradientStopProvider } from '../gradient-builder-provider.svelte';
import type { GradientStopProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'gradient-builder-stop'),
index = 0,
children,
child,
...restProps
}: GradientStopProps = $props();
const state = GradientStopProvider.create({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
),
index: readableActive(() => index)
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}

@ -0,0 +1,35 @@
<script lang="ts">
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { GradientTrackProvider } from '../gradient-builder-provider.svelte';
import type { GradientTrackProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'gradient-builder-track'),
children,
child,
...restProps
}: GradientTrackProps = $props();
const state = GradientTrackProvider.create({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
)
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}

@ -0,0 +1,54 @@
<script lang="ts">
import { readableActive, writableActive } from '$libs/reactive';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { GradientBuilderProvider } from '../gradient-builder-provider.svelte';
import type { GradientBuilderProps } from '../types';
import type { Gradient } from '$libs/gradient';
const uid = $props.id();
const DEFAULT_GRADIENT: Gradient = {
kind: 'linear',
angle: 90,
stops: [
{ color: { kind: 'css', value: '#000000' } },
{ color: { kind: 'css', value: '#ffffff' } }
]
};
let {
ref = $bindable(null),
id = createId(uid, 'gradient-builder'),
value = $bindable(DEFAULT_GRADIENT),
onValueCommit = () => {},
disabled = false,
children,
child,
...restProps
}: GradientBuilderProps = $props();
const state = GradientBuilderProvider.create({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v)
),
value: writableActive(
() => value,
(v) => (value = v)
),
onValueCommit: readableActive(() => onValueCommit),
disabled: readableActive(() => disabled)
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}

@ -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';

@ -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 };
}
}

@ -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<Gradient>;
/** @default false */
disabled?: boolean;
}> &
Without<PrimitiveDivAttributes, {}>;
export type GradientTrackProps = WithChild<{
/** Unique identifier. Auto-generated if omitted. */
id?: string;
}> &
Without<PrimitiveDivAttributes, {}>;
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<PrimitiveDivAttributes, {}>;
Loading…
Cancel
Save

Powered by TurnKey Linux.