feat(color-picker): area drag emits handle-pick/handle-drag/commit-set + refresh demo snippets

User report: 'el selector del cuadro de colores, no tiene semántica, es
un drag, etc como el slider, no?'

You're right — the 2D area is a drag interaction with the same shape
as a slider. It was wiring pointer events to soma state but staying
silent to sema; nothing for sounds/haptics/transient styles to hook
onto. Now it speaks the same vocabulary as the slider.

Morfo additions (color-picker)
------------------------------
- `handle-pick` (family: handle, verb: pick, sequence: coincident,
  target: area) — fires once at pointer-down.
- `handle-drag` (family: handle, verb: drag, sequence: coincident,
  target: area) — fires on each move (pointer or keyboard step).
- `commit-set` already existed — now also emitted on pointer-up and
  on each keyboard step (slider parity).

Soma changes (color-picker-provider)
------------------------------------
- ColorPickerAreaProvider:
  - `onpointerdown` → trigger `handle-pick` then continue to drag.
  - `handlePointerMove` → trigger `handle-drag` after writing the value.
  - `onpointerup` → trigger `commit-set` then `commitChange()` for
    the onValueChangeEnd callback.
- ColorPickerAreaThumbProvider.onkeydown:
  - Each arrow / Home / End / PageUp / PageDown step now triggers
    `handle-drag` + `commit-set` + commitChange(). Matches the slider's
    keyboard semantics (one step = one drag + one commit).

Demo snippet refresh
--------------------
The `somaSnippet` / `eidosSnippet` in `/uix/components/color-picker`
were stuck on the pre-refactor API (`<ColorPicker.Area>` with explicit
Background+Thumb children, no `Control`, no `ValueSwatch`, no
PickerShell footer). Updated both to mirror the current demo body:

- Eidos: `Label / Control[Trigger + ValueText + HiddenInput] / Portal /
  Content[Area / ChannelSlider hue / alpha / ValueSwatch +
  ChannelInput + EyeDropper / SwatchGroup / Footer[Clear/Cancel/
  Close]]`.
- Soma: same shape but uses `PickerShell.Footer / Clear / Cancel /
  Close` explicitly (and the explicit Track/Thumb composition on
  channel sliders for the curious).
- Both snippets honor the live `mode / inline / closeOnSelect /
  enableAlpha / showFooter / showClear / showCancel / showClose`
  flags from the chip controls.

Checks: 67/67 component-audit PASS, 40/40 morfo:check PASS,
svelte-check 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 3e5be857c4
commit da19728cb6

@ -7,6 +7,30 @@ export const colorPickerMorfo = {
scope: ['soma', 'sema'], scope: ['soma', 'sema'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/', apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/',
events: [ events: [
// Area drag — same shape as the slider's `handle-*` events so sema
// consumers can hook the family/verb uniformly (sounds, haptics,
// transient styling) regardless of which dragger emitted.
{
name: 'handle-pick',
semantic: {
family: 'handle',
verb: 'pick',
target: v.partRef('area'),
sequence: 'coincident'
}
},
{
name: 'handle-drag',
semantic: {
family: 'handle',
verb: 'drag',
target: v.partRef('area'),
sequence: 'coincident'
}
},
// Value commit — fires on pointer release (area thumb / channel
// slider / swatch click / eyedropper pick). `commit-set` is the
// canonical "save" signal.
{ {
name: 'commit-set', name: 'commit-set',
semantic: { semantic: {

@ -692,6 +692,8 @@ export class ColorPickerAreaProvider {
const withX = setColorChannel(this.provider.effectiveColor, this.xChannel, xValue); const withX = setColorChannel(this.provider.effectiveColor, this.xChannel, xValue);
const withBoth = setColorChannel(withX, this.yChannel, yValue); const withBoth = setColorChannel(withX, this.yChannel, yValue);
this.provider.opts.value.current = withBoth; this.provider.opts.value.current = withBoth;
// Sema: emit `handle-drag` so consumers can stream tactile feedback.
void this.provider.runtime.trigger('handle-drag');
} }
readonly onpointerdown = (e: PointerEvent): void => { readonly onpointerdown = (e: PointerEvent): void => {
@ -699,6 +701,8 @@ export class ColorPickerAreaProvider {
const target = e.currentTarget as HTMLElement; const target = e.currentTarget as HTMLElement;
target.setPointerCapture(e.pointerId); target.setPointerCapture(e.pointerId);
this.pointerId = e.pointerId; this.pointerId = e.pointerId;
// Sema: emit `handle-pick` at drag start (mirrors slider semantics).
void this.provider.runtime.trigger('handle-pick');
this.handlePointerMove(e.clientX, e.clientY, target.getBoundingClientRect()); this.handlePointerMove(e.clientX, e.clientY, target.getBoundingClientRect());
e.preventDefault(); e.preventDefault();
}; };
@ -712,6 +716,8 @@ export class ColorPickerAreaProvider {
readonly onpointerup = (e: PointerEvent): void => { readonly onpointerup = (e: PointerEvent): void => {
if (this.pointerId !== e.pointerId) return; if (this.pointerId !== e.pointerId) return;
this.pointerId = null; this.pointerId = null;
// Sema: fire `commit-set` on release (canonical save signal).
void this.provider.runtime.trigger('commit-set');
this.provider.commitChange(); this.provider.commitChange();
}; };
@ -837,6 +843,11 @@ export class ColorPickerAreaThumbProvider {
if (nextX !== x) next = setColorChannel(next, this.area.xChannel, nextX); if (nextX !== x) next = setColorChannel(next, this.area.xChannel, nextX);
if (nextY !== y) next = setColorChannel(next, this.area.yChannel, nextY); if (nextY !== y) next = setColorChannel(next, this.area.yChannel, nextY);
this.provider.opts.value.current = next; this.provider.opts.value.current = next;
// Sema: each keypress is one drag-step + commit (slider parity).
void this.provider.runtime.trigger('handle-drag');
void this.provider.runtime.trigger('commit-set');
this.provider.commitChange();
}; };
readonly props = $derived.by(() => { readonly props = $derived.by(() => {

@ -131,6 +131,7 @@
[ [
"<script lang='ts'>", "<script lang='ts'>",
" import * as ColorPicker from '$soma/components/color-picker';", " import * as ColorPicker from '$soma/components/color-picker';",
" import * as PickerShell from '$soma/components/picker-shell';",
" import { colorValueFromRgb, type ColorValue } from '$libs/color';", " import { colorValueFromRgb, type ColorValue } from '$libs/color';",
'', '',
' let value = $state<ColorValue | undefined>(colorValueFromRgb({ r: 30, g: 144, b: 255, a: 1 }));', ' let value = $state<ColorValue | undefined>(colorValueFromRgb({ r: 30, g: 144, b: 255, a: 1 }));',
@ -139,20 +140,39 @@
'</' + 'script>', '</' + 'script>',
'', '',
'<ColorPicker.Provider bind:value bind:format bind:open', '<ColorPicker.Provider bind:value bind:format bind:open',
mode !== 'inline' && ` mode="${mode}"`,
inline && ' inline', inline && ' inline',
closeOnSelect && ' closeOnSelect', closeOnSelect && ' closeOnSelect',
!enableAlpha && ' enableAlpha={false}', !enableAlpha && ' enableAlpha={false}',
'>', '>',
' <ColorPicker.Control>',
' <ColorPicker.Trigger><ColorPicker.ValueText /></ColorPicker.Trigger>', ' <ColorPicker.Trigger><ColorPicker.ValueText /></ColorPicker.Trigger>',
' </ColorPicker.Control>',
' <ColorPicker.Content>', ' <ColorPicker.Content>',
' <ColorPicker.Area><ColorPicker.AreaBackground /><ColorPicker.AreaThumb /></ColorPicker.Area>', ' <ColorPicker.Area>',
' <ColorPicker.AreaBackground />',
' <ColorPicker.AreaThumb />',
' </ColorPicker.Area>',
' <ColorPicker.ChannelSlider channel="hue">', ' <ColorPicker.ChannelSlider channel="hue">',
' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />', ' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
' </ColorPicker.ChannelSlider>', ' </ColorPicker.ChannelSlider>',
enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha">', enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha">',
enableAlpha && ' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />', enableAlpha && ' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
enableAlpha && ' </ColorPicker.ChannelSlider>', enableAlpha && ' </ColorPicker.ChannelSlider>',
' <ColorPicker.SwatchGroup>{#each presets as c}<ColorPicker.SwatchTrigger color={c} />{/each}</ColorPicker.SwatchGroup>', ' <ColorPicker.ValueSwatch />',
' <ColorPicker.ChannelInput />',
' <ColorPicker.EyeDropper />',
' <ColorPicker.SwatchGroup>',
' {#each presets as c}<ColorPicker.SwatchTrigger color={c}>',
' <ColorPicker.Swatch color={c} />',
' <ColorPicker.SwatchIndicator />',
' </ColorPicker.SwatchTrigger>{/each}',
' </ColorPicker.SwatchGroup>',
showFooter && (showClear || showCancel || showClose) && ' <PickerShell.Footer>',
showFooter && showClear && ' <PickerShell.Clear />',
showFooter && showCancel && ' <PickerShell.Cancel />',
showFooter && showClose && ' <PickerShell.Close />',
showFooter && (showClear || showCancel || showClose) && ' </PickerShell.Footer>',
' </ColorPicker.Content>', ' </ColorPicker.Content>',
'</ColorPicker.Provider>' '</ColorPicker.Provider>'
] ]
@ -175,18 +195,33 @@
size !== 'md' && ` size="${size}"`, size !== 'md' && ` size="${size}"`,
variant !== 'surface' && ` variant="${variant}"`, variant !== 'surface' && ` variant="${variant}"`,
color !== 'primary' && ` color="${color}"`, color !== 'primary' && ` color="${color}"`,
mode !== 'inline' && ` mode="${mode}"`,
inline && ' inline', inline && ' inline',
closeOnSelect && ' closeOnSelect', closeOnSelect && ' closeOnSelect',
!enableAlpha && ' enableAlpha={false}', !enableAlpha && ' enableAlpha={false}',
'>', '>',
' <ColorPicker.Label>Brand color</ColorPicker.Label>',
' <ColorPicker.Control>',
' <ColorPicker.Trigger><ColorPicker.ValueText /></ColorPicker.Trigger>', ' <ColorPicker.Trigger><ColorPicker.ValueText /></ColorPicker.Trigger>',
' <ColorPicker.HiddenInput />',
' </ColorPicker.Control>',
' <ColorPicker.Portal>', ' <ColorPicker.Portal>',
' <ColorPicker.Content>', ' <ColorPicker.Content>',
' <ColorPicker.Area />', ' <ColorPicker.Area />',
' <ColorPicker.ChannelSlider channel="hue" />', ' <ColorPicker.ChannelSlider channel="hue" />',
enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha" />', enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha" />',
' <ColorPicker.SwatchGroup>{#each presets as c}<ColorPicker.SwatchTrigger color={c} />{/each}</ColorPicker.SwatchGroup>', ' <!-- compact value row: swatch + channel input + eyedropper -->',
' <ColorPicker.ValueSwatch />',
' <ColorPicker.ChannelInput />',
' <ColorPicker.EyeDropper />', ' <ColorPicker.EyeDropper />',
' <ColorPicker.SwatchGroup>',
' {#each presets as c}<ColorPicker.SwatchTrigger color={c} />{/each}',
' </ColorPicker.SwatchGroup>',
showFooter && (showClear || showCancel || showClose) && ' <ColorPicker.Footer>',
showFooter && showClear && ' <ColorPicker.Clear />',
showFooter && showCancel && ' <ColorPicker.Cancel />',
showFooter && showClose && ' <ColorPicker.Close />',
showFooter && (showClear || showCancel || showClose) && ' </ColorPicker.Footer>',
' </ColorPicker.Content>', ' </ColorPicker.Content>',
' </ColorPicker.Portal>', ' </ColorPicker.Portal>',
'</ColorPicker>' '</ColorPicker>'

Loading…
Cancel
Save

Powered by TurnKey Linux.