From 5d007ed9f9c3b9cefccdd757e867511fb400e3be Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 21 May 2026 20:29:21 +0200 Subject: [PATCH] refactor(color-picker): channel slider composes generic SliderProvider + N-8 norm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops ColorPickerChannelSliderProvider / TrackProvider / ThumbProvider (~200 lines of duplicated pointer/keyboard/ARIA handling) and rewrites `components/color-picker-channel-slider.svelte` to compose SliderProvider underneath. The channel slider now keeps only the channel-aware bridge: setChannel writes the value, channelGradient paints the track. What the picker gains by reusing Slider --------------------------------------- - sema events: `slider:handle-pick`, `slider:handle-drag`, `slider:commit-set` now flow through the channel sliders (they were silent before — the Sema layer had no perceptual hook on channel drag/commit). - single pointer/keyboard implementation: Arrow/Home/End/PageUp/PageDown, multi-touch, focus trap, RTL, step-snap — all from SliderProvider. - ARIA: role=slider, aria-valuenow/min/max, aria-orientation, tabindex managed by the slider's thumb provider. - recipe tokens: channel sliders consume `--slider-*` (sizes, radii, shadows) so they stay in lockstep with the standalone Slider and with TimePicker.HourSlider / .MinuteSlider / .SecondSlider. The `--color-picker-channel-slider-*` tokens are removed. Public API impact ----------------- - `` and `` remain as part names but now re-export `Slider.Range` / `Slider.Thumb`. Consumers that compose them inline keep working. - The inner DOM changes: `[data-slider]` / `[data-slider-thumb]` / `[data-slider-range]` (governed by the slider morfo) replace `[data-color-picker-channel-slider-track]` / `[data-color-picker-channel-slider-thumb]`. The wrapper still emits `[data-color-picker-channel-slider][data-channel]` so per-channel CSS targeting works; the gradient is exposed via `--cp-channel-gradient` and routed onto the slider's track ::before. - Morfo: drops `channel-slider-track` and `channel-slider-thumb` parts (the slider morfo owns those contracts now). `channel-slider` remains as the wrapper marker with `data-channel`. - Eidos: deletes `color-picker-channel-slider-track.svelte` and `color-picker-channel-slider-thumb.svelte`; `index.ts` re-points `ChannelSliderTrack` / `ChannelSliderThumb` at the slider eidos components. New norm N-8 added to PENDIENTES.md ----------------------------------- "Reutilizar componentes ya definidos en componentes complejos siempre que no supongan una pérdida de características demostrable." Documented with this refactor as the worked example. 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) --- src/uix/PENDIENTES.md | 32 ++ .../color-picker-channel-slider-thumb.svelte | 8 - .../color-picker-channel-slider-track.svelte | 8 - .../color-picker-channel-slider.svelte | 13 +- .../components/color-picker/color-picker.css | 346 +++++++++++------- .../eidos/components/color-picker/index.ts | 7 +- .../eidos/components/color-picker/types.ts | 2 +- src/uix/eidos/generated/base.css | 35 +- src/uix/eidos/lib/recipes/base.ts | 42 +-- src/uix/morfo/components/color-picker.ts | 29 +- .../color-picker-provider.svelte.test.ts | 53 +-- .../color-picker-provider.svelte.ts | 230 +----------- .../color-picker-channel-slider-thumb.svelte | 36 -- .../color-picker-channel-slider-track.svelte | 36 -- .../color-picker-channel-slider.svelte | 85 ++++- .../soma/components/color-picker/exports.ts | 21 +- src/uix/soma/components/color-picker/types.ts | 35 +- 17 files changed, 436 insertions(+), 582 deletions(-) delete mode 100644 src/uix/eidos/components/color-picker/color-picker-channel-slider-thumb.svelte delete mode 100644 src/uix/eidos/components/color-picker/color-picker-channel-slider-track.svelte delete mode 100644 src/uix/soma/components/color-picker/components/color-picker-channel-slider-thumb.svelte delete mode 100644 src/uix/soma/components/color-picker/components/color-picker-channel-slider-track.svelte diff --git a/src/uix/PENDIENTES.md b/src/uix/PENDIENTES.md index b6f3241c2..cb320948e 100644 --- a/src/uix/PENDIENTES.md +++ b/src/uix/PENDIENTES.md @@ -168,3 +168,35 @@ Footer, ToolbarItem, etc.). Demos: las parts se envuelven en `{#if showX}` con state local del demo para dar el control UI, pero las parts en sí no leen ese state. + +### N-8 · Reutilizar componentes ya definidos en componentes complejos + +Cuando un componente complejo (picker, combobox, color-picker, etc.) +necesita un sub-control que ya existe como componente independiente +(Slider, Popover, Calendar, Field, etc.), **debe componerlo en vez de +reimplementarlo**. Sólo se justifica un fork si reusarlo causaría una +pérdida de características concreta y demostrable. + +Beneficios: + +1. **Una sola fuente de eventos sema**. El sub-control emite sus + eventos canónicos (`slider:handle-pick`, `slider:commit-set`, + `popover:close-dismiss`, etc.) que el composite hereda + automáticamente — sin reimplementar la dimensión perceptiva. +2. **Una sola implementación de pointer/keyboard/ARIA**. Bugs + corregidos en el componente raíz se propagan a todos los composites + que lo usan. Sin duplicación de drag, focus trap, etc. +3. **Tokens del recipe unificados**. `--slider-track-size`, + `--popover-content-padding`, etc. se setean en un único recipe; + todos los composites consumen los mismos. +4. **Superficie pública estable**. La API del composite (parts + + eventos) se reduce a su lógica diferenciadora; el resto cae del + sub-control. + +Caso aplicado 2026-05-21: ColorPicker.ChannelSlider deja de tener su +propio `ColorPickerChannelSliderProvider` con pointer handlers +duplicados; ahora compone `SliderProvider` por debajo. Channel-aware +behavior queda en una sola función bridge (`setChannel` + gradient +inline en `--cp-channel-gradient`). Resultado: −200 líneas en soma, +eventos `slider:handle-pick/drag/commit-set` propagados, tokens +unificados con time-picker / standalone Slider. diff --git a/src/uix/eidos/components/color-picker/color-picker-channel-slider-thumb.svelte b/src/uix/eidos/components/color-picker/color-picker-channel-slider-thumb.svelte deleted file mode 100644 index f824ca345..000000000 --- a/src/uix/eidos/components/color-picker/color-picker-channel-slider-thumb.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-channel-slider-track.svelte b/src/uix/eidos/components/color-picker/color-picker-channel-slider-track.svelte deleted file mode 100644 index 54480690a..000000000 --- a/src/uix/eidos/components/color-picker/color-picker-channel-slider-track.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-channel-slider.svelte b/src/uix/eidos/components/color-picker/color-picker-channel-slider.svelte index b6eff2aab..5a0eb4cb9 100644 --- a/src/uix/eidos/components/color-picker/color-picker-channel-slider.svelte +++ b/src/uix/eidos/components/color-picker/color-picker-channel-slider.svelte @@ -1,5 +1,6 @@ - -{#if child} - {@render child({ props: mergedProps })} -{:else} -
{@render children?.()}
-{/if} diff --git a/src/uix/soma/components/color-picker/components/color-picker-channel-slider-track.svelte b/src/uix/soma/components/color-picker/components/color-picker-channel-slider-track.svelte deleted file mode 100644 index 5f1c33d15..000000000 --- a/src/uix/soma/components/color-picker/components/color-picker-channel-slider-track.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -{#if child} - {@render child({ props: mergedProps })} -{:else} -
{@render children?.()}
-{/if} diff --git a/src/uix/soma/components/color-picker/components/color-picker-channel-slider.svelte b/src/uix/soma/components/color-picker/components/color-picker-channel-slider.svelte index 1abe53f64..77cf67b37 100644 --- a/src/uix/soma/components/color-picker/components/color-picker-channel-slider.svelte +++ b/src/uix/soma/components/color-picker/components/color-picker-channel-slider.svelte @@ -1,11 +1,14 @@ {#if child} - {@render child({ props: mergedProps, ...snippetProps })} + {@render child({ props: mergedProps, channel })} {:else}
- {@render children?.(snippetProps)} + {@render children?.({ channel })}
{/if} diff --git a/src/uix/soma/components/color-picker/exports.ts b/src/uix/soma/components/color-picker/exports.ts index 20b949fa4..46d45a503 100644 --- a/src/uix/soma/components/color-picker/exports.ts +++ b/src/uix/soma/components/color-picker/exports.ts @@ -16,10 +16,17 @@ export { default as AreaBackground } from './components/color-picker-area-backgr export { default as AreaThumb } from './components/color-picker-area-thumb.svelte'; export { default as TransparencyGrid } from './components/color-picker-transparency-grid.svelte'; -// Stage 3 — Channel sliders (1D slider per channel: hue / saturation / alpha / red / …) +// Stage 3 — Channel sliders. +// The ChannelSlider composes the generic SliderProvider underneath (see +// `components/color-picker-channel-slider.svelte`). The Track / Thumb roles +// are filled by the slider's `Range` / `Thumb` parts directly — re-exported +// here so the public API stays `ColorPicker.ChannelSliderTrack` / +// `ColorPicker.ChannelSliderThumb` for consumers that compose them inline. export { default as ChannelSlider } from './components/color-picker-channel-slider.svelte'; -export { default as ChannelSliderTrack } from './components/color-picker-channel-slider-track.svelte'; -export { default as ChannelSliderThumb } from './components/color-picker-channel-slider-thumb.svelte'; +export { + Range as ChannelSliderTrack, + Thumb as ChannelSliderThumb +} from '../slider/exports'; // Stage 5 — Swatches (radiogroup of preset colors) export { default as SwatchGroup } from './components/color-picker-swatch-group.svelte'; @@ -57,8 +64,6 @@ export type { ColorPickerAreaSnippetProps as AreaSnippetProps, ColorPickerTransparencyGridProps as TransparencyGridProps, ColorPickerChannelSliderProps as ChannelSliderProps, - ColorPickerChannelSliderTrackProps as ChannelSliderTrackProps, - ColorPickerChannelSliderThumbProps as ChannelSliderThumbProps, ColorPickerChannelSliderSnippetProps as ChannelSliderSnippetProps, ColorPickerSwatchGroupProps as SwatchGroupProps, ColorPickerSwatchTriggerProps as SwatchTriggerProps, @@ -74,3 +79,9 @@ export type { ColorPickerFormatSelectProps as FormatSelectProps, ColorChannel } from './types'; + +// Re-export slider props for the inner thumb/track tags. +export type { + SliderRangeProps as ChannelSliderTrackProps, + SliderThumbProps as ChannelSliderThumbProps +} from '../slider/types'; diff --git a/src/uix/soma/components/color-picker/types.ts b/src/uix/soma/components/color-picker/types.ts index 07efd40fc..cccf5630f 100644 --- a/src/uix/soma/components/color-picker/types.ts +++ b/src/uix/soma/components/color-picker/types.ts @@ -37,14 +37,17 @@ export type ColorPickerAreaSnippetProps = { thumbStyle: { left: string; top: string }; }; -/** Snippet props exposed by `ColorPicker.ChannelSlider`. */ +/** + * Snippet props exposed by `ColorPicker.ChannelSlider`. + * + * As of 2026-05-21 the channel slider composes the generic `SliderProvider` + * internally; the gradient + thumb position are wired automatically through + * the slider's own DOM ([data-slider]::before for the track, the gradient + * lives on the wrapper as `--cp-channel-gradient`). Consumers only need + * the `channel` discriminator to label / branch on. + */ export type ColorPickerChannelSliderSnippetProps = { - /** Current normalized value (0..1) along the slider. */ - progress: number; - /** CSS thumb position (left for horizontal, top for vertical). */ - thumbStyle: { left: string } | { top: string }; - /** Gradient string representing the channel's full range under the current color. */ - backgroundStyle: string; + channel: ColorChannel; }; // ── Root provider ────────────────────────────────────────────────────────── @@ -203,7 +206,15 @@ export type ColorPickerAreaThumbProps = WithChild<{ id?: string }> & // ── ChannelSlider ────────────────────────────────────────────────────────── -/** Props for `ColorPicker.ChannelSlider` — 1D slider bound to a single channel. */ +/** + * Props for `ColorPicker.ChannelSlider` — 1D slider bound to a single channel. + * + * Internally composes the generic `SliderProvider`. Track / Thumb are + * re-exported from `Slider` as `ChannelSliderTrack` / `ChannelSliderThumb` + * — they carry the canonical `[data-slider-range]` / `[data-slider-thumb]` + * markers and emit the slider's sema events (`handle-pick`, `handle-drag`, + * `commit-set`). + */ export type ColorPickerChannelSliderProps = WithChild< { id?: string; @@ -212,16 +223,10 @@ export type ColorPickerChannelSliderProps = WithChild< /** Orientation. @default 'horizontal' */ orientation?: Orientation; }, - ColorPickerChannelSliderSnippetProps + { channel: ColorChannel } > & Without; -export type ColorPickerChannelSliderTrackProps = WithChild<{ id?: string }> & - Without; - -export type ColorPickerChannelSliderThumbProps = WithChild<{ id?: string }> & - Without; - // ── TransparencyGrid ─────────────────────────────────────────────────────── /** Props for `ColorPicker.TransparencyGrid` — decorative checkerboard under alpha. */