From 2731ee567b4f114367e315eb0e9d05578018e6d4 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 21 May 2026 19:28:45 +0200 Subject: [PATCH] feat(eidos): add ColorPicker wrapper + demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composite popover: 2D saturation/brightness area, hue + alpha channel sliders, preset swatches, eye-dropper, and a segmented channel input composed from ColorField. Builds on the existing soma color-picker (23 parts) — eidos layer is mostly thin pass-throughs adding visual chrome. What landed ----------- - New `src/uix/eidos/components/color-picker/`: - Root visual (Provider) + visual-context (size/variant/color) - Trigger with a default swatch dot tinted from `--cp-trigger-color` (the air pattern from morfo-runtime) - Content (popover wrapper), Area (+ Background + Thumb), TransparencyGrid - ChannelSlider (+ Track + Thumb) — generic per-channel slider; default-composes Track+Thumb when no children - SwatchGroup + SwatchTrigger + Swatch + SwatchIndicator - EyeDropper (Pipette icon by default) - ChannelInput + ChannelSegment + FormatSelect (composed from ColorField parts; same provider injected automatically) - Label, Control, ValueSwatch, ValueText, HiddenInput - Recipe section `color-picker` in `recipes/base.ts` (regenerated `generated/base.css`) — trigger, content, area, channel slider, swatches, eye-dropper tokens. - CSS reuses color-field tokens for the segmented input portion and adds picker-specific visuals. Transparency checker on trigger swatch + value swatch + alpha slider track via conic-gradient pattern. - Morfo updates: scope `['soma','sema']`, `apg: dialog-modal`, added `commit-set` event so audit classifies it as interactive. - Soma exports: ColorPickerProvider class so eidos Trigger can read the current value for the swatch tint. - Demo at `/uix/components/color-picker` with the standard 6 tabs; live stage exercises area drag + channel sliders + preset swatches + eye-dropper + format switch. - README documents the surface, comparativa vs air/Ark/Chakra, and flags the picker contract (mode/Footer/commit/cancel) as gap to align with date/time-picker in a follow-up. Checks ------ - 67/67 component-audit PASS - 40/40 morfo:check PASS (new color-picker demo validates against morfo DOM) - svelte-check 0 errors / 0 warnings Co-Authored-By: Claude Opus 4.7 (1M context) --- .../eidos/components/color-picker/README.md | 127 ++++ .../color-picker-area-background.svelte | 8 + .../color-picker-area-thumb.svelte | 8 + .../color-picker/color-picker-area.svelte | 17 + .../color-picker-channel-input.svelte | 15 + .../color-picker-channel-segment.svelte | 8 + .../color-picker-channel-slider-thumb.svelte | 8 + .../color-picker-channel-slider-track.svelte | 8 + .../color-picker-channel-slider.svelte | 20 + .../color-picker/color-picker-content.svelte | 32 + .../color-picker/color-picker-control.svelte | 8 + .../color-picker-eye-dropper.svelte | 15 + .../color-picker-format-select.svelte | 8 + .../color-picker-hidden-input.svelte | 5 + .../color-picker/color-picker-label.svelte | 8 + .../color-picker-swatch-group.svelte | 8 + .../color-picker-swatch-indicator.svelte | 8 + .../color-picker-swatch-trigger.svelte | 15 + .../color-picker/color-picker-swatch.svelte | 8 + .../color-picker-transparency-grid.svelte | 8 + .../color-picker/color-picker-trigger.svelte | 52 ++ .../color-picker-value-swatch.svelte | 8 + .../color-picker-value-text.svelte | 8 + .../components/color-picker/color-picker.css | 388 +++++++++++ .../color-picker/color-picker.svelte | 40 ++ .../components/color-picker/context.svelte.ts | 19 + .../eidos/components/color-picker/index.ts | 124 ++++ .../eidos/components/color-picker/types.ts | 91 +++ src/uix/eidos/generated/base.css | 68 ++ src/uix/eidos/index.css | 1 + src/uix/eidos/lib/recipes/base.ts | 88 +++ src/uix/morfo/components/color-picker.ts | 15 +- .../soma/components/color-picker/exports.ts | 3 + web/routes/uix/+layout@.svelte | 1 + .../uix/components/color-picker/+page.svelte | 612 ++++++++++++++++++ 35 files changed, 1859 insertions(+), 1 deletion(-) create mode 100644 src/uix/eidos/components/color-picker/README.md create mode 100644 src/uix/eidos/components/color-picker/color-picker-area-background.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-area-thumb.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-area.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-channel-input.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-channel-segment.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-channel-slider-thumb.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-channel-slider-track.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-channel-slider.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-content.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-control.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-eye-dropper.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-format-select.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-hidden-input.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-label.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-swatch-group.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-swatch-indicator.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-swatch-trigger.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-swatch.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-transparency-grid.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-trigger.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-value-swatch.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker-value-text.svelte create mode 100644 src/uix/eidos/components/color-picker/color-picker.css create mode 100644 src/uix/eidos/components/color-picker/color-picker.svelte create mode 100644 src/uix/eidos/components/color-picker/context.svelte.ts create mode 100644 src/uix/eidos/components/color-picker/index.ts create mode 100644 src/uix/eidos/components/color-picker/types.ts create mode 100644 web/routes/uix/components/color-picker/+page.svelte diff --git a/src/uix/eidos/components/color-picker/README.md b/src/uix/eidos/components/color-picker/README.md new file mode 100644 index 000000000..120328944 --- /dev/null +++ b/src/uix/eidos/components/color-picker/README.md @@ -0,0 +1,127 @@ +# Eidos ColorPicker + +Composite popover with a 2D saturation/brightness area, hue + alpha +channel sliders, optional preset swatches, eye-dropper, and a segmented +channel input (composed from `ColorField`). Soma owns pointer/keyboard +behavior, color math and value coordination; Eidos adds visual +treatment, the popover content shell and recipe styling. + +## Baseline + +Air en la rama anterior (`morfo-runtime`, `src/uix/air/components/ +color-picker/`) ya implementaba una superficie de color-picker con +Provider · Trigger · Content · Area + AreaThumb · HueSlider · +AlphaSlider · Input · Swatches/Swatch · EyeDropper. Esta receta sigue +las mismas líneas pero monta sobre la implementación Soma actual, que +es más rica: 23 partes públicas (incluyendo `ValueSwatch`, `ValueText`, +`TransparencyGrid`, `SwatchIndicator`, control sliders por canal RGB +además del par HSV, `ChannelInput`+`ChannelSegment`+`FormatSelect` +compuestos desde `ColorField`). + +## Superficie + +```svelte + + Brand color + + + + + + + + + + + + + + + + + {#snippet children({ segments })} + {#each segments as { part, value } (part + value)} + {value} + {/each} + + {/snippet} + + + + {#each presets as color} + + {/each} + + + + + + +``` + +Partes públicas: `Label`, `Control`, `Trigger`, `ValueSwatch`, +`ValueText`, `HiddenInput`, `Content` (+ `Portal`/`Anchor`/`Overlay`/ +`Arrow`/`Close`), `Area` + `AreaBackground` + `AreaThumb`, +`TransparencyGrid`, `ChannelSlider` + `ChannelSliderTrack` + +`ChannelSliderThumb`, `EyeDropper`, `SwatchGroup` + `SwatchTrigger` + +`Swatch` + `SwatchIndicator`, `ChannelInput` + `ChannelSegment` + +`FormatSelect`. + +## Comparativa + +| Capacidad | Soma/Eidos | Air (morfo-runtime) | Ark UI | Chakra UI | +| --- | --- | --- | --- | --- | +| 2D area (sat/bright) | Sí | Sí | Sí | Sí | +| Hue slider | Sí (`ChannelSlider channel='hue'`) | Sí | Sí | Sí | +| Alpha slider | Sí (`ChannelSlider channel='alpha'`) | Sí | Sí | Sí | +| RGB sliders | Sí (red/green/blue channels) | No | No | No | +| Saturation/brightness sliders | Sí | No | No | No | +| Channel input (segmented) | Sí (compone ColorField) | Input simple | Input simple | Input simple | +| Format switch (hex/rgb/hsl) | Sí (`FormatSelect`) | Sí | Sí | Sí | +| `allowedFormats` whitelist | Sí | No | No | No | +| Eye dropper | Sí (native API) | Sí | Sí | Sí | +| Preset swatches | Sí (`SwatchGroup`) | Sí | Sí | Sí | +| Hidden input para forms | Sí | No | Via picker | Via picker | +| `closeOnSelect` | Sí | No | Sí | Sí | +| Inline mode | Sí (`inline={true}`) | No | Sí | Sí | +| Variante / tamaño / paleta visual | Sí, Eidos | CSS air-specific | CSS externo | CSS externo | + +## Decisiones + +- No se exporta `Provider` ni `Root` público. La forma Eidos es + `` con hijos attached. +- `Trigger` tiene un swatch dot por defecto (color del valor actual) + vía la CSS var `--cp-trigger-color`. Se desactiva con + ``. +- `Area` y `ChannelSlider` por defecto componen su sub-tree + (`AreaBackground + AreaThumb`, `ChannelSliderTrack + + ChannelSliderThumb`). El consumer puede sobreescribir pasando + children al snippet. +- `Content` reutiliza la receta de `Popover` con un anchor + configurable y bordes/padding propios del picker. +- `ChannelInput` / `ChannelSegment` / `FormatSelect` re-exportan + `ColorField` parts: el provider del ColorField se inyecta + automáticamente vía contexto, no hay que componer `ColorField` + aparte. +- `SwatchTrigger` recibe `color="…"` como string CSS — internamente lo + parsea para emparejarlo con el valor actual y pintar + `data-checked`. + +## Gaps + +| Gap | Disposición | Detalle | +| --- | --- | --- | +| Picker contract (`mode`/`Footer`/`Clear`/`Cancel`/`Close` con commit/cancel snapshot) | **diferir** | El soma actual implementa `closeOnSelect` + `inline`; el contrato canónico (P-1..P-5) se aplicará en una pasada follow-up alineando con date-picker / time-picker. | +| Tests browser-level (Playwright) | **implementar** | Cobertura visual + interacciones: drag de area-thumb, drag de channel-slider-thumb, swatch click, eyedropper API, format switch. | +| Saturation/brightness sliders en el demo | **implementar** | Soma lo soporta; demo lo dejará seleccionable cuando se walk visual del componente. | +| Documentación per-prop exhaustiva | **diferir** | Cubierto por la tabla del demo (`API` tab). | + +## Referencias + +- Soma ColorPicker: `src/uix/soma/components/color-picker/README.md` +- Soma ColorField: `src/uix/soma/components/color-field/README.md` +- Air ColorPicker (referencia histórica): rama `morfo-runtime`, + `src/uix/air/components/color-picker/` +- Ark UI Color Picker: https://ark-ui.com/docs/components/color-picker +- Chakra UI Color Picker: https://chakra-ui.com/docs/components/color-picker +- Bits UI Color Picker: https://bits-ui.com/docs/components/color-picker diff --git a/src/uix/eidos/components/color-picker/color-picker-area-background.svelte b/src/uix/eidos/components/color-picker/color-picker-area-background.svelte new file mode 100644 index 000000000..0bb440e20 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-area-background.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-area-thumb.svelte b/src/uix/eidos/components/color-picker/color-picker-area-thumb.svelte new file mode 100644 index 000000000..f9c6ac409 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-area-thumb.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-area.svelte b/src/uix/eidos/components/color-picker/color-picker-area.svelte new file mode 100644 index 000000000..3046bf127 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-area.svelte @@ -0,0 +1,17 @@ + + + + {#snippet children(snippetProps: ColorPickerAreaSnippetProps)} + {#if bodyContent} + {@render bodyContent(snippetProps)} + {:else} + + + {/if} + {/snippet} + diff --git a/src/uix/eidos/components/color-picker/color-picker-channel-input.svelte b/src/uix/eidos/components/color-picker/color-picker-channel-input.svelte new file mode 100644 index 000000000..c1ab1c8b1 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-channel-input.svelte @@ -0,0 +1,15 @@ + + + + {#snippet children(args: ColorPickerChannelInputSnippetProps)} + {@render bodyContent?.(args)} + {/snippet} + diff --git a/src/uix/eidos/components/color-picker/color-picker-channel-segment.svelte b/src/uix/eidos/components/color-picker/color-picker-channel-segment.svelte new file mode 100644 index 000000000..5ee35f8f1 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-channel-segment.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} 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 new file mode 100644 index 000000000..f824ca345 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-channel-slider-thumb.svelte @@ -0,0 +1,8 @@ + + +{@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 new file mode 100644 index 000000000..54480690a --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-channel-slider-track.svelte @@ -0,0 +1,8 @@ + + +{@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 new file mode 100644 index 000000000..b6eff2aab --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-channel-slider.svelte @@ -0,0 +1,20 @@ + + + + {#snippet children(snippetProps: ColorPickerChannelSliderSnippetProps)} + {#if bodyContent} + {@render bodyContent(snippetProps)} + {:else} + + + {/if} + {/snippet} + diff --git a/src/uix/eidos/components/color-picker/color-picker-content.svelte b/src/uix/eidos/components/color-picker/color-picker-content.svelte new file mode 100644 index 000000000..d92975667 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-content.svelte @@ -0,0 +1,32 @@ + + + + {#snippet children(snippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/color-picker/color-picker-control.svelte b/src/uix/eidos/components/color-picker/color-picker-control.svelte new file mode 100644 index 000000000..32fedae10 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-control.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-eye-dropper.svelte b/src/uix/eidos/components/color-picker/color-picker-eye-dropper.svelte new file mode 100644 index 000000000..443d80389 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-eye-dropper.svelte @@ -0,0 +1,15 @@ + + + + {#if children} + {@render children()} + {:else} + + {/if} + diff --git a/src/uix/eidos/components/color-picker/color-picker-format-select.svelte b/src/uix/eidos/components/color-picker/color-picker-format-select.svelte new file mode 100644 index 000000000..6b7d5f988 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-format-select.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-hidden-input.svelte b/src/uix/eidos/components/color-picker/color-picker-hidden-input.svelte new file mode 100644 index 000000000..3439aa87f --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-hidden-input.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/uix/eidos/components/color-picker/color-picker-label.svelte b/src/uix/eidos/components/color-picker/color-picker-label.svelte new file mode 100644 index 000000000..5b25b01f5 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-label.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-swatch-group.svelte b/src/uix/eidos/components/color-picker/color-picker-swatch-group.svelte new file mode 100644 index 000000000..fc26c76b2 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-swatch-group.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-swatch-indicator.svelte b/src/uix/eidos/components/color-picker/color-picker-swatch-indicator.svelte new file mode 100644 index 000000000..266bae8f6 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-swatch-indicator.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-swatch-trigger.svelte b/src/uix/eidos/components/color-picker/color-picker-swatch-trigger.svelte new file mode 100644 index 000000000..49bf4174c --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-swatch-trigger.svelte @@ -0,0 +1,15 @@ + + + + {#if children} + {@render children()} + {:else} + + + {/if} + diff --git a/src/uix/eidos/components/color-picker/color-picker-swatch.svelte b/src/uix/eidos/components/color-picker/color-picker-swatch.svelte new file mode 100644 index 000000000..4f7501c25 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-swatch.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-transparency-grid.svelte b/src/uix/eidos/components/color-picker/color-picker-transparency-grid.svelte new file mode 100644 index 000000000..44c0d922e --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-transparency-grid.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-trigger.svelte b/src/uix/eidos/components/color-picker/color-picker-trigger.svelte new file mode 100644 index 000000000..6fca5aa96 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-trigger.svelte @@ -0,0 +1,52 @@ + + + + {#if showSwatch} + + {/if} + {#if children} + {@render children()} + {:else if !showSwatch} + Pick color + {/if} + diff --git a/src/uix/eidos/components/color-picker/color-picker-value-swatch.svelte b/src/uix/eidos/components/color-picker/color-picker-value-swatch.svelte new file mode 100644 index 000000000..382d5f5c7 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-value-swatch.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker-value-text.svelte b/src/uix/eidos/components/color-picker/color-picker-value-text.svelte new file mode 100644 index 000000000..f5832811e --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker-value-text.svelte @@ -0,0 +1,8 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/color-picker/color-picker.css b/src/uix/eidos/components/color-picker/color-picker.css new file mode 100644 index 000000000..402bdbfc2 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker.css @@ -0,0 +1,388 @@ +/* ColorPicker — visual layer. + * + * Composes the segmented `color-field` recipe for ChannelInput / Segment / + * FormatSelect and adds picker-specific surfaces: trigger, popover content, + * 2D area, channel sliders, swatches, eye-dropper. + */ + +[data-color-picker] { + --_color-picker-content-padding: var(--color-picker-content-padding-md); + --_color-picker-content-width: var(--color-picker-content-width-md); + --_color-picker-trigger-height: var(--color-picker-trigger-height-md); + --_color-picker-trigger-px: var(--color-picker-trigger-px-md); + --_color-picker-trigger-swatch-size: var(--color-picker-trigger-swatch-size-md); + --_color-picker-swatch-size: var(--color-picker-swatch-size-md); + --_color-picker-accent: var(--color-primary-border); + + display: inline-flex; + flex-direction: column; + gap: var(--space-1-5); + inline-size: 100%; + min-inline-size: 0; +} + +[data-color-picker][data-size='sm'] { + --_color-picker-content-padding: var(--color-picker-content-padding-sm); + --_color-picker-content-width: var(--color-picker-content-width-sm); + --_color-picker-trigger-height: var(--color-picker-trigger-height-sm); + --_color-picker-trigger-px: var(--color-picker-trigger-px-sm); + --_color-picker-trigger-swatch-size: var(--color-picker-trigger-swatch-size-sm); + --_color-picker-swatch-size: var(--color-picker-swatch-size-sm); +} + +[data-color-picker][data-size='lg'] { + --_color-picker-content-padding: var(--color-picker-content-padding-lg); + --_color-picker-content-width: var(--color-picker-content-width-lg); + --_color-picker-trigger-height: var(--color-picker-trigger-height-lg); + --_color-picker-trigger-px: var(--color-picker-trigger-px-lg); + --_color-picker-trigger-swatch-size: var(--color-picker-trigger-swatch-size-lg); + --_color-picker-swatch-size: var(--color-picker-swatch-size-lg); +} + +[data-color-picker][data-color='secondary'] { + --_color-picker-accent: var(--color-secondary-border); +} +[data-color-picker][data-color='neutral'] { + --_color-picker-accent: var(--color-neutral-border); +} +[data-color-picker][data-color='affirm'] { + --_color-picker-accent: var(--color-affirm-border); +} +[data-color-picker][data-color='fulfill'] { + --_color-picker-accent: var(--color-fulfill-border); +} +[data-color-picker][data-color='risk'] { + --_color-picker-accent: var(--color-risk-border); +} +[data-color-picker][data-color='threat'] { + --_color-picker-accent: var(--color-threat-border); +} +[data-color-picker][data-color='loss'] { + --_color-picker-accent: var(--color-loss-border); +} + +[data-color-picker-label] { + color: var(--color-content-secondary); + font: inherit; + font-weight: var(--font-weight-medium); +} + +[data-color-picker-control] { + display: inline-flex; + align-items: center; + gap: var(--space-2); + min-inline-size: 0; +} + +/* ── Trigger ─────────────────────────────────────────────────────────────── */ + +[data-color-picker-trigger] { + display: inline-flex; + align-items: center; + gap: var(--color-picker-trigger-gap); + min-block-size: var(--_color-picker-trigger-height); + padding-inline: var(--_color-picker-trigger-px); + border: var(--color-picker-trigger-border-width) solid var(--color-picker-trigger-border); + border-radius: var(--color-picker-trigger-radius); + background: var(--color-picker-trigger-bg); + color: var(--color-picker-trigger-color); + font: inherit; + cursor: pointer; + transition: + background var(--color-picker-transition-duration) var(--color-picker-transition-ease), + border-color var(--color-picker-transition-duration) var(--color-picker-transition-ease), + box-shadow var(--color-picker-transition-duration) var(--color-picker-transition-ease); +} + +[data-color-picker-trigger]:hover:not([data-disabled]) { + background: var(--color-picker-trigger-hover-bg); + border-color: var(--color-picker-trigger-hover-border); +} + +[data-color-picker-trigger]:focus-visible { + outline: none; + border-color: var(--_color-picker-accent); + box-shadow: var(--color-picker-trigger-focus-shadow); +} + +[data-color-picker-trigger][data-disabled] { + cursor: not-allowed; + opacity: var(--color-picker-disabled-opacity); +} + +[data-color-picker-trigger] .color-picker-trigger-swatch { + display: inline-block; + flex: 0 0 auto; + inline-size: var(--_color-picker-trigger-swatch-size); + block-size: var(--_color-picker-trigger-swatch-size); + border-radius: var(--color-picker-trigger-swatch-radius); + border: 1px solid var(--color-picker-trigger-swatch-border); + background: + linear-gradient(var(--cp-trigger-color, transparent), var(--cp-trigger-color, transparent)), + conic-gradient( + var(--color-picker-transparency-cell) 0% 25%, + var(--color-picker-transparency-cell-alt) 0% 50%, + var(--color-picker-transparency-cell) 0% 75%, + var(--color-picker-transparency-cell-alt) 0% 100% + ); + background-size: + 100% 100%, + 8px 8px; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-surface-default) 40%, transparent); +} + +/* ── ValueSwatch / ValueText ─────────────────────────────────────────────── */ + +[data-color-picker-value-swatch] { + display: inline-block; + inline-size: var(--_color-picker-swatch-size); + block-size: var(--_color-picker-swatch-size); + border-radius: var(--color-picker-swatch-radius); + border: 1px solid var(--color-picker-swatch-border); + background: + linear-gradient(var(--cp-trigger-color, currentColor), var(--cp-trigger-color, currentColor)), + conic-gradient( + var(--color-picker-transparency-cell) 0% 25%, + var(--color-picker-transparency-cell-alt) 0% 50%, + var(--color-picker-transparency-cell) 0% 75%, + var(--color-picker-transparency-cell-alt) 0% 100% + ); + background-size: + 100% 100%, + 8px 8px; +} + +[data-color-picker-value-text] { + font-family: var(--font-mono, var(--font-ui)); + font-size: var(--font-size-sm); + color: var(--color-content-secondary); +} + +/* ── Popover content ─────────────────────────────────────────────────────── */ + +[data-popover-content][data-color-picker-content] { + --_popover-content-width-override: var(--_color-picker-content-width, var(--color-picker-content-width-md)); + --_popover-content-min-width-override: var(--_color-picker-content-width, var(--color-picker-content-width-md)); + --_popover-content-max-width-override: calc(100vw - var(--space-4)); +} + +[data-popover-content][data-color-picker-content] { + display: flex; + flex-direction: column; + gap: var(--color-picker-content-gap); + padding: var(--_color-picker-content-padding, var(--color-picker-content-padding-md)); +} + +/* ── 2D Area (saturation/brightness gradient field) ──────────────────────── */ + +[data-color-picker-area] { + position: relative; + inline-size: 100%; + block-size: var(--color-picker-area-height); + border: var(--color-picker-area-border-width) solid var(--color-picker-area-border); + border-radius: var(--color-picker-area-radius); + overflow: hidden; + cursor: crosshair; + touch-action: none; + user-select: none; +} + +[data-color-picker-area-background] { + position: absolute; + inset: 0; + border-radius: inherit; +} + +[data-color-picker-area-thumb] { + position: absolute; + inline-size: var(--color-picker-area-thumb-size); + block-size: var(--color-picker-area-thumb-size); + border: var(--color-picker-area-thumb-border-width) solid + var(--color-picker-area-thumb-border); + border-radius: var(--color-picker-area-thumb-radius); + box-shadow: var(--color-picker-area-thumb-shadow); + transform: translate(-50%, -50%); + cursor: grab; + background: transparent; +} + +[data-color-picker-area-thumb][data-dragging], +[data-color-picker-area-thumb]:active { + cursor: grabbing; +} + +/* ── Channel sliders ─────────────────────────────────────────────────────── */ + +[data-color-picker-channel-slider] { + position: relative; + inline-size: 100%; + block-size: var(--color-picker-channel-slider-thumb-size); + display: flex; + align-items: center; + cursor: pointer; + touch-action: none; + user-select: none; +} + +[data-color-picker-channel-slider-track] { + position: relative; + inline-size: 100%; + block-size: var(--color-picker-channel-slider-track-size); + border-radius: var(--color-picker-channel-slider-track-radius); + border: var(--color-picker-channel-slider-track-border-width) solid + var(--color-picker-channel-slider-track-border); + background: + linear-gradient( + to right, + transparent, + transparent + ), + conic-gradient( + var(--color-picker-transparency-cell) 0% 25%, + var(--color-picker-transparency-cell-alt) 0% 50%, + var(--color-picker-transparency-cell) 0% 75%, + var(--color-picker-transparency-cell-alt) 0% 100% + ); + background-size: + 100% 100%, + 8px 8px; + overflow: hidden; +} + +[data-color-picker-channel-slider-thumb] { + position: absolute; + inline-size: var(--color-picker-channel-slider-thumb-size); + block-size: var(--color-picker-channel-slider-thumb-size); + border: var(--color-picker-channel-slider-thumb-border-width) solid + var(--color-picker-channel-slider-thumb-border); + border-radius: var(--color-picker-channel-slider-thumb-radius); + box-shadow: var(--color-picker-channel-slider-thumb-shadow); + transform: translateX(-50%); + background: transparent; + cursor: grab; +} + +[data-color-picker-channel-slider-thumb][data-dragging], +[data-color-picker-channel-slider-thumb]:active { + cursor: grabbing; +} + +/* ── Transparency grid (decorative checker under alpha channels) ─────────── */ + +[data-color-picker-transparency-grid] { + background-color: var(--color-picker-transparency-cell-alt); + background-image: + linear-gradient(45deg, var(--color-picker-transparency-cell) 25%, transparent 25%), + linear-gradient(-45deg, var(--color-picker-transparency-cell) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, var(--color-picker-transparency-cell) 75%), + linear-gradient(-45deg, transparent 75%, var(--color-picker-transparency-cell) 75%); + background-size: 8px 8px; + background-position: + 0 0, + 0 4px, + 4px -4px, + -4px 0px; +} + +/* ── Swatches ─────────────────────────────────────────────────────────────── */ + +[data-color-picker-swatch-group] { + display: flex; + flex-wrap: wrap; + gap: var(--color-picker-swatch-gap); +} + +[data-color-picker-swatch-trigger] { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: var(--_color-picker-swatch-size); + block-size: var(--_color-picker-swatch-size); + padding: 0; + border: 1px solid var(--color-picker-swatch-border); + border-radius: var(--color-picker-swatch-radius); + background: transparent; + cursor: pointer; + transition: + box-shadow var(--color-picker-transition-duration) var(--color-picker-transition-ease), + transform var(--color-picker-transition-duration) var(--color-picker-transition-ease); +} + +[data-color-picker-swatch-trigger]:hover:not([data-disabled]) { + transform: scale(1.06); +} + +[data-color-picker-swatch-trigger]:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--color-picker-swatch-checked-ring); +} + +[data-color-picker-swatch-trigger][data-checked] { + box-shadow: 0 0 0 2px var(--color-picker-swatch-checked-ring); +} + +[data-color-picker-swatch] { + position: absolute; + inset: 0; + border-radius: inherit; +} + +[data-color-picker-swatch-indicator] { + position: absolute; + inset-inline-end: -2px; + inset-block-start: -2px; + display: none; + inline-size: 0.5rem; + block-size: 0.5rem; + border-radius: var(--color-picker-trigger-swatch-radius); + background: var(--color-picker-swatch-checked-ring); +} + +[data-color-picker-swatch-indicator][data-checked] { + display: block; +} + +/* ── Eye dropper ─────────────────────────────────────────────────────────── */ + +[data-color-picker-eye-dropper] { + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: var(--color-picker-eye-dropper-size); + block-size: var(--color-picker-eye-dropper-size); + border: var(--border-width) solid transparent; + border-radius: var(--color-picker-eye-dropper-radius); + background: var(--color-picker-eye-dropper-bg); + color: var(--color-picker-eye-dropper-color); + cursor: pointer; + transition: + background var(--color-picker-transition-duration) var(--color-picker-transition-ease), + color var(--color-picker-transition-duration) var(--color-picker-transition-ease); +} + +[data-color-picker-eye-dropper]:hover:not([data-disabled]):not([data-unsupported]) { + background: var(--color-picker-eye-dropper-hover-bg); + color: var(--color-picker-eye-dropper-hover-color); +} + +[data-color-picker-eye-dropper]:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--_color-picker-accent); +} + +[data-color-picker-eye-dropper][data-unsupported], +[data-color-picker-eye-dropper][data-disabled] { + cursor: not-allowed; + opacity: var(--color-picker-disabled-opacity); +} + +@media (prefers-reduced-motion: reduce) { + [data-color-picker-trigger], + [data-color-picker-area-thumb], + [data-color-picker-channel-slider-thumb], + [data-color-picker-swatch-trigger], + [data-color-picker-eye-dropper] { + transition: none; + } +} diff --git a/src/uix/eidos/components/color-picker/color-picker.svelte b/src/uix/eidos/components/color-picker/color-picker.svelte new file mode 100644 index 000000000..9272ded17 --- /dev/null +++ b/src/uix/eidos/components/color-picker/color-picker.svelte @@ -0,0 +1,40 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/color-picker/context.svelte.ts b/src/uix/eidos/components/color-picker/context.svelte.ts new file mode 100644 index 000000000..68c7dc503 --- /dev/null +++ b/src/uix/eidos/components/color-picker/context.svelte.ts @@ -0,0 +1,19 @@ +import { getContext, setContext } from 'svelte'; +import type { ColorPickerColor, ColorPickerSize, ColorPickerVariant } from './types'; + +const COLOR_PICKER_VISUAL_CONTEXT = Symbol('uix.eidos.color-picker.visual'); + +export interface ColorPickerVisualContext { + getSize(): ColorPickerSize; + getVariant(): ColorPickerVariant; + getColor(): ColorPickerColor; +} + +export function setColorPickerVisualContext(ctx: ColorPickerVisualContext): ColorPickerVisualContext { + setContext(COLOR_PICKER_VISUAL_CONTEXT, ctx); + return ctx; +} + +export function getColorPickerVisualContext(): ColorPickerVisualContext | undefined { + return getContext(COLOR_PICKER_VISUAL_CONTEXT); +} diff --git a/src/uix/eidos/components/color-picker/index.ts b/src/uix/eidos/components/color-picker/index.ts new file mode 100644 index 000000000..ced3e9888 --- /dev/null +++ b/src/uix/eidos/components/color-picker/index.ts @@ -0,0 +1,124 @@ +import ColorPickerComponent from './color-picker.svelte' +import Label from './color-picker-label.svelte' +import Control from './color-picker-control.svelte' +import Trigger from './color-picker-trigger.svelte' +import ValueSwatch from './color-picker-value-swatch.svelte' +import ValueText from './color-picker-value-text.svelte' +import HiddenInput from './color-picker-hidden-input.svelte' +import Content from './color-picker-content.svelte' +import Area from './color-picker-area.svelte' +import AreaBackground from './color-picker-area-background.svelte' +import AreaThumb from './color-picker-area-thumb.svelte' +import TransparencyGrid from './color-picker-transparency-grid.svelte' +import ChannelSlider from './color-picker-channel-slider.svelte' +import ChannelSliderTrack from './color-picker-channel-slider-track.svelte' +import ChannelSliderThumb from './color-picker-channel-slider-thumb.svelte' +import EyeDropper from './color-picker-eye-dropper.svelte' +import SwatchGroup from './color-picker-swatch-group.svelte' +import SwatchTrigger from './color-picker-swatch-trigger.svelte' +import Swatch from './color-picker-swatch.svelte' +import SwatchIndicator from './color-picker-swatch-indicator.svelte' +import ChannelInput from './color-picker-channel-input.svelte' +import ChannelSegment from './color-picker-channel-segment.svelte' +import FormatSelect from './color-picker-format-select.svelte' +import Anchor from '../popover/popover-anchor.svelte' +import Overlay from '../popover/popover-overlay.svelte' +import Arrow from '../popover/popover-arrow.svelte' +import PopoverClose from '../popover/popover-close.svelte' +import { Portal } from '$soma/components/internal' + +type ColorPickerNamespace = typeof ColorPickerComponent & { + Label: typeof Label + Control: typeof Control + Trigger: typeof Trigger + ValueSwatch: typeof ValueSwatch + ValueText: typeof ValueText + HiddenInput: typeof HiddenInput + Portal: typeof Portal + Anchor: typeof Anchor + Overlay: typeof Overlay + Content: typeof Content + Arrow: typeof Arrow + Close: typeof PopoverClose + Area: typeof Area + AreaBackground: typeof AreaBackground + AreaThumb: typeof AreaThumb + TransparencyGrid: typeof TransparencyGrid + ChannelSlider: typeof ChannelSlider + ChannelSliderTrack: typeof ChannelSliderTrack + ChannelSliderThumb: typeof ChannelSliderThumb + EyeDropper: typeof EyeDropper + SwatchGroup: typeof SwatchGroup + SwatchTrigger: typeof SwatchTrigger + Swatch: typeof Swatch + SwatchIndicator: typeof SwatchIndicator + ChannelInput: typeof ChannelInput + ChannelSegment: typeof ChannelSegment + FormatSelect: typeof FormatSelect +} + +const ColorPicker = ColorPickerComponent as ColorPickerNamespace +ColorPicker.Label = Label +ColorPicker.Control = Control +ColorPicker.Trigger = Trigger +ColorPicker.ValueSwatch = ValueSwatch +ColorPicker.ValueText = ValueText +ColorPicker.HiddenInput = HiddenInput +ColorPicker.Portal = Portal +ColorPicker.Anchor = Anchor +ColorPicker.Overlay = Overlay +ColorPicker.Content = Content +ColorPicker.Arrow = Arrow +ColorPicker.Close = PopoverClose +ColorPicker.Area = Area +ColorPicker.AreaBackground = AreaBackground +ColorPicker.AreaThumb = AreaThumb +ColorPicker.TransparencyGrid = TransparencyGrid +ColorPicker.ChannelSlider = ChannelSlider +ColorPicker.ChannelSliderTrack = ChannelSliderTrack +ColorPicker.ChannelSliderThumb = ChannelSliderThumb +ColorPicker.EyeDropper = EyeDropper +ColorPicker.SwatchGroup = SwatchGroup +ColorPicker.SwatchTrigger = SwatchTrigger +ColorPicker.Swatch = Swatch +ColorPicker.SwatchIndicator = SwatchIndicator +ColorPicker.ChannelInput = ChannelInput +ColorPicker.ChannelSegment = ChannelSegment +ColorPicker.FormatSelect = FormatSelect + +export { ColorPicker } + +export default ColorPicker + +export type { + ColorPickerProps, + ColorPickerLabelProps as LabelProps, + ColorPickerControlProps as ControlProps, + ColorPickerTriggerProps as TriggerProps, + ColorPickerValueSwatchProps as ValueSwatchProps, + ColorPickerValueTextProps as ValueTextProps, + ColorPickerHiddenInputProps as HiddenInputProps, + ColorPickerContentProps as ContentProps, + ColorPickerAreaProps as AreaProps, + ColorPickerAreaBackgroundProps as AreaBackgroundProps, + ColorPickerAreaThumbProps as AreaThumbProps, + ColorPickerTransparencyGridProps as TransparencyGridProps, + ColorPickerChannelSliderProps as ChannelSliderProps, + ColorPickerChannelSliderTrackProps as ChannelSliderTrackProps, + ColorPickerChannelSliderThumbProps as ChannelSliderThumbProps, + ColorPickerEyeDropperProps as EyeDropperProps, + ColorPickerSwatchGroupProps as SwatchGroupProps, + ColorPickerSwatchTriggerProps as SwatchTriggerProps, + ColorPickerSwatchProps as SwatchProps, + ColorPickerSwatchIndicatorProps as SwatchIndicatorProps, + ColorPickerChannelInputProps as ChannelInputProps, + ColorPickerChannelSegmentProps as ChannelSegmentProps, + ColorPickerFormatSelectProps as FormatSelectProps, + AreaSnippetProps, + ChannelSliderSnippetProps, + ChannelInputSnippetProps, + ColorChannel, + ColorPickerSize, + ColorPickerVariant, + ColorPickerColor +} from './types' diff --git a/src/uix/eidos/components/color-picker/types.ts b/src/uix/eidos/components/color-picker/types.ts new file mode 100644 index 000000000..46bcd7427 --- /dev/null +++ b/src/uix/eidos/components/color-picker/types.ts @@ -0,0 +1,91 @@ +import type { + AreaBackgroundProps as SomaAreaBackgroundProps, + AreaProps as SomaAreaProps, + AreaSnippetProps as ColorPickerAreaSnippetProps, + AreaThumbProps as SomaAreaThumbProps, + ChannelInputProps as SomaChannelInputProps, + ChannelInputSnippetProps as ColorPickerChannelInputSnippetProps, + ChannelSegmentProps as SomaChannelSegmentProps, + ChannelSliderProps as SomaChannelSliderProps, + ChannelSliderSnippetProps as ColorPickerChannelSliderSnippetProps, + ChannelSliderThumbProps as SomaChannelSliderThumbProps, + ChannelSliderTrackProps as SomaChannelSliderTrackProps, + ColorChannel, + ControlProps as SomaControlProps, + EyeDropperProps as SomaEyeDropperProps, + FormatSelectProps as SomaFormatSelectProps, + HiddenInputProps as SomaHiddenInputProps, + LabelProps as SomaLabelProps, + ProviderProps, + SwatchGroupProps as SomaSwatchGroupProps, + SwatchIndicatorProps as SomaSwatchIndicatorProps, + SwatchProps as SomaSwatchProps, + SwatchTriggerProps as SomaSwatchTriggerProps, + TransparencyGridProps as SomaTransparencyGridProps, + TriggerProps as SomaTriggerProps, + ValueSwatchProps as SomaValueSwatchProps, + ValueTextProps as SomaValueTextProps +} from '$soma/components/color-picker' +import type { PopoverContentProps } from '../popover/types' +import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types' + +export type ColorPickerSize = Extract +export type ColorPickerVariant = ControlVariant +export type ColorPickerColor = ColorRole + +export type ColorPickerProps = ProviderProps & { + /** Visual size for the trigger, popover and child controls. @default 'md' */ + size?: ResponsiveProp + /** Visual treatment of the trigger. @default 'surface' */ + variant?: ColorPickerVariant + /** Intent palette for focus and accent affordances. @default 'primary' */ + color?: ColorPickerColor +} + +export type ColorPickerTriggerProps = SomaTriggerProps & { + size?: ResponsiveProp + variant?: ColorPickerVariant + color?: ColorPickerColor + /** When true (default) the trigger shows a swatch dot before its content. */ + showSwatch?: boolean +} + +export type ColorPickerContentProps = Omit & { + size?: ResponsiveProp +} + +export type ColorPickerLabelProps = SomaLabelProps +export type ColorPickerControlProps = SomaControlProps +export type ColorPickerValueSwatchProps = SomaValueSwatchProps +export type ColorPickerValueTextProps = SomaValueTextProps +export type ColorPickerHiddenInputProps = SomaHiddenInputProps + +export type ColorPickerAreaProps = SomaAreaProps +export type ColorPickerAreaBackgroundProps = SomaAreaBackgroundProps +export type ColorPickerAreaThumbProps = SomaAreaThumbProps +export type ColorPickerTransparencyGridProps = SomaTransparencyGridProps + +export type ColorPickerChannelSliderProps = SomaChannelSliderProps +export type ColorPickerChannelSliderTrackProps = SomaChannelSliderTrackProps +export type ColorPickerChannelSliderThumbProps = SomaChannelSliderThumbProps + +export type ColorPickerEyeDropperProps = SomaEyeDropperProps + +export type ColorPickerSwatchGroupProps = SomaSwatchGroupProps +export type ColorPickerSwatchTriggerProps = SomaSwatchTriggerProps +export type ColorPickerSwatchProps = SomaSwatchProps +export type ColorPickerSwatchIndicatorProps = SomaSwatchIndicatorProps + +export type ColorPickerChannelInputProps = SomaChannelInputProps +export type ColorPickerChannelSegmentProps = SomaChannelSegmentProps +export type ColorPickerFormatSelectProps = SomaFormatSelectProps + +export type { + ColorPickerAreaSnippetProps, + ColorPickerChannelSliderSnippetProps, + ColorPickerChannelInputSnippetProps, + ColorChannel, + ColorPickerAreaSnippetProps as AreaSnippetProps, + ColorPickerChannelSliderSnippetProps as ChannelSliderSnippetProps, + ColorPickerChannelInputSnippetProps as ChannelInputSnippetProps +} diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 4cf8d8597..f08ab8ac2 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -981,6 +981,74 @@ --color-field-transition-duration: var(--duration-fast); --color-field-transition-ease: var(--ease-default); --color-field-disabled-opacity: 0.56; + --color-picker-content-gap: var(--space-3); + --color-picker-content-padding-sm: var(--space-3); + --color-picker-content-padding-md: var(--space-3-5); + --color-picker-content-padding-lg: var(--space-4); + --color-picker-content-width-sm: 16rem; + --color-picker-content-width-md: 18rem; + --color-picker-content-width-lg: 20rem; + --color-picker-content-radius: var(--radius-lg); + --color-picker-content-border-width: var(--border-width); + --color-picker-content-border: var(--color-border-subtle); + --color-picker-content-bg: var(--color-surface-raised); + --color-picker-content-shadow: var(--shadow-md); + --color-picker-trigger-height-sm: var(--control-height-sm); + --color-picker-trigger-height-md: var(--control-height-md); + --color-picker-trigger-height-lg: var(--control-height-lg); + --color-picker-trigger-px-sm: var(--space-2); + --color-picker-trigger-px-md: var(--space-2-5); + --color-picker-trigger-px-lg: var(--space-3); + --color-picker-trigger-gap: var(--space-2); + --color-picker-trigger-radius: var(--radius-md); + --color-picker-trigger-border-width: var(--border-width); + --color-picker-trigger-border: var(--color-border-default); + --color-picker-trigger-bg: var(--color-surface-default); + --color-picker-trigger-color: var(--color-content-primary); + --color-picker-trigger-hover-bg: var(--color-surface-overlay); + --color-picker-trigger-hover-border: var(--color-border-strong); + --color-picker-trigger-swatch-size-sm: calc(var(--control-height-sm) - var(--space-2-5)); + --color-picker-trigger-swatch-size-md: calc(var(--control-height-md) - var(--space-2-5)); + --color-picker-trigger-swatch-size-lg: calc(var(--control-height-lg) - var(--space-2-5)); + --color-picker-trigger-swatch-radius: var(--radius-full); + --color-picker-trigger-swatch-border: var(--color-border-subtle); + --color-picker-trigger-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color); + --color-picker-area-height: 10rem; + --color-picker-area-radius: var(--radius-md); + --color-picker-area-border-width: var(--border-width); + --color-picker-area-border: var(--color-border-subtle); + --color-picker-area-thumb-size: 14px; + --color-picker-area-thumb-radius: var(--radius-full); + --color-picker-area-thumb-border-width: 2px; + --color-picker-area-thumb-border: var(--color-surface-default); + --color-picker-area-thumb-shadow: 0 0 0 1px color-mix(in srgb, var(--color-neutral-12) 45%, transparent), 0 1px 2px color-mix(in srgb, var(--color-neutral-12) 20%, transparent); + --color-picker-channel-slider-track-size: 10px; + --color-picker-channel-slider-track-radius: var(--radius-pill); + --color-picker-channel-slider-track-border-width: var(--border-width); + --color-picker-channel-slider-track-border: var(--color-border-subtle); + --color-picker-channel-slider-thumb-size: 14px; + --color-picker-channel-slider-thumb-radius: var(--radius-full); + --color-picker-channel-slider-thumb-border-width: 2px; + --color-picker-channel-slider-thumb-border: var(--color-surface-default); + --color-picker-channel-slider-thumb-shadow: 0 0 0 1px color-mix(in srgb, var(--color-neutral-12) 45%, transparent), 0 1px 2px color-mix(in srgb, var(--color-neutral-12) 20%, transparent); + --color-picker-transparency-cell: var(--color-content-muted); + --color-picker-transparency-cell-alt: var(--color-surface-default); + --color-picker-swatch-size-sm: 1rem; + --color-picker-swatch-size-md: 1.25rem; + --color-picker-swatch-size-lg: 1.5rem; + --color-picker-swatch-radius: var(--radius-sm); + --color-picker-swatch-border: var(--color-border-subtle); + --color-picker-swatch-checked-ring: var(--color-primary-border); + --color-picker-swatch-gap: var(--space-1-5); + --color-picker-eye-dropper-size: var(--control-height-sm); + --color-picker-eye-dropper-radius: var(--radius-md); + --color-picker-eye-dropper-bg: transparent; + --color-picker-eye-dropper-color: var(--color-content-secondary); + --color-picker-eye-dropper-hover-bg: var(--color-surface-overlay); + --color-picker-eye-dropper-hover-color: var(--color-content-primary); + --color-picker-transition-duration: var(--duration-fast); + --color-picker-transition-ease: var(--ease-default); + --color-picker-disabled-opacity: 0.56; --calendar-gap: var(--space-3); --calendar-padding-sm: var(--space-3); --calendar-padding-md: var(--space-4); diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index 56d8320c2..ece532cc1 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -92,6 +92,7 @@ @import './components/time-field/time-field.css'; @import './components/color-field/color-field.css'; @import './components/time-picker/time-picker.css'; +@import './components/color-picker/color-picker.css'; @import './components/slider/slider.css'; @import './components/toolbar/toolbar.css'; @import './components/tag-group/tag-group.css'; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index a50d84a08..1cedfe0f4 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -754,6 +754,94 @@ export const THEME_BASE_RECIPE_TOKENS = { 'transition-ease': 'var(--ease-default)', 'disabled-opacity': '0.56' }, + 'color-picker': { + // Popover content shell + 'content-gap': 'var(--space-3)', + 'content-padding-sm': 'var(--space-3)', + 'content-padding-md': 'var(--space-3-5)', + 'content-padding-lg': 'var(--space-4)', + 'content-width-sm': '16rem', + 'content-width-md': '18rem', + 'content-width-lg': '20rem', + 'content-radius': 'var(--radius-lg)', + 'content-border-width': 'var(--border-width)', + 'content-border': 'var(--color-border-subtle)', + 'content-bg': 'var(--color-surface-raised)', + 'content-shadow': 'var(--shadow-md)', + + // Trigger button (compact field-shaped pill with a swatch dot) + 'trigger-height-sm': 'var(--control-height-sm)', + 'trigger-height-md': 'var(--control-height-md)', + 'trigger-height-lg': 'var(--control-height-lg)', + 'trigger-px-sm': 'var(--space-2)', + 'trigger-px-md': 'var(--space-2-5)', + 'trigger-px-lg': 'var(--space-3)', + 'trigger-gap': 'var(--space-2)', + 'trigger-radius': 'var(--radius-md)', + 'trigger-border-width': 'var(--border-width)', + 'trigger-border': 'var(--color-border-default)', + 'trigger-bg': 'var(--color-surface-default)', + 'trigger-color': 'var(--color-content-primary)', + 'trigger-hover-bg': 'var(--color-surface-overlay)', + 'trigger-hover-border': 'var(--color-border-strong)', + 'trigger-swatch-size-sm': 'calc(var(--control-height-sm) - var(--space-2-5))', + 'trigger-swatch-size-md': 'calc(var(--control-height-md) - var(--space-2-5))', + 'trigger-swatch-size-lg': 'calc(var(--control-height-lg) - var(--space-2-5))', + 'trigger-swatch-radius': 'var(--radius-full)', + 'trigger-swatch-border': 'var(--color-border-subtle)', + 'trigger-focus-shadow': + '0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)', + + // 2D area + 'area-height': '10rem', + 'area-radius': 'var(--radius-md)', + 'area-border-width': 'var(--border-width)', + 'area-border': 'var(--color-border-subtle)', + 'area-thumb-size': '14px', + 'area-thumb-radius': 'var(--radius-full)', + 'area-thumb-border-width': '2px', + 'area-thumb-border': 'var(--color-surface-default)', + 'area-thumb-shadow': + '0 0 0 1px color-mix(in srgb, var(--color-neutral-12) 45%, transparent), 0 1px 2px color-mix(in srgb, var(--color-neutral-12) 20%, transparent)', + + // Channel sliders (hue / alpha / etc) + 'channel-slider-track-size': '10px', + 'channel-slider-track-radius': 'var(--radius-pill)', + 'channel-slider-track-border-width': 'var(--border-width)', + 'channel-slider-track-border': 'var(--color-border-subtle)', + 'channel-slider-thumb-size': '14px', + 'channel-slider-thumb-radius': 'var(--radius-full)', + 'channel-slider-thumb-border-width': '2px', + 'channel-slider-thumb-border': 'var(--color-surface-default)', + 'channel-slider-thumb-shadow': + '0 0 0 1px color-mix(in srgb, var(--color-neutral-12) 45%, transparent), 0 1px 2px color-mix(in srgb, var(--color-neutral-12) 20%, transparent)', + + // Transparency checker (alpha background) + 'transparency-cell': 'var(--color-content-muted)', + 'transparency-cell-alt': 'var(--color-surface-default)', + + // Swatches + 'swatch-size-sm': '1rem', + 'swatch-size-md': '1.25rem', + 'swatch-size-lg': '1.5rem', + 'swatch-radius': 'var(--radius-sm)', + 'swatch-border': 'var(--color-border-subtle)', + 'swatch-checked-ring': 'var(--color-primary-border)', + 'swatch-gap': 'var(--space-1-5)', + + // Eye-dropper button + 'eye-dropper-size': 'var(--control-height-sm)', + 'eye-dropper-radius': 'var(--radius-md)', + 'eye-dropper-bg': 'transparent', + 'eye-dropper-color': 'var(--color-content-secondary)', + 'eye-dropper-hover-bg': 'var(--color-surface-overlay)', + 'eye-dropper-hover-color': 'var(--color-content-primary)', + + // Misc + 'transition-duration': 'var(--duration-fast)', + 'transition-ease': 'var(--ease-default)', + 'disabled-opacity': '0.56' + }, calendar: { gap: 'var(--space-3)', 'padding-sm': 'var(--space-3)', diff --git a/src/uix/morfo/components/color-picker.ts b/src/uix/morfo/components/color-picker.ts index 719b46ed3..6c9db3cc8 100644 --- a/src/uix/morfo/components/color-picker.ts +++ b/src/uix/morfo/components/color-picker.ts @@ -4,7 +4,20 @@ import { v } from '../types'; export const colorPickerMorfo = { name: 'ColorPicker', kebab: 'color-picker', - scope: ['soma'], + scope: ['soma', 'sema'], + apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/', + events: [ + { + name: 'commit-set', + semantic: { + family: 'commit', + verb: 'set', + target: v.partRef('provider'), + intent: 'neutral', + sequence: 'post' + } + } + ], texts: { label: '#?components.color-picker.label|Color Picker', trigger: '#?components.color-picker.trigger|Open color picker', diff --git a/src/uix/soma/components/color-picker/exports.ts b/src/uix/soma/components/color-picker/exports.ts index 73ca77b95..20b949fa4 100644 --- a/src/uix/soma/components/color-picker/exports.ts +++ b/src/uix/soma/components/color-picker/exports.ts @@ -1,3 +1,6 @@ +// Provider class — exposes value/format/open state + helpers for eidos. +export { ColorPickerProvider } from './color-picker-provider.svelte'; + // Unique parts export { default as Provider } from './components/color-picker.svelte'; export { default as Label } from './components/color-picker-label.svelte'; diff --git a/web/routes/uix/+layout@.svelte b/web/routes/uix/+layout@.svelte index 677b5712c..a9370f5be 100644 --- a/web/routes/uix/+layout@.svelte +++ b/web/routes/uix/+layout@.svelte @@ -150,6 +150,7 @@ { slug: '/uix/components/time-field', label: 'Time field' }, { slug: '/uix/components/time-picker', label: 'Time picker' }, { slug: '/uix/components/color-field', label: 'Color field' }, + { slug: '/uix/components/color-picker', label: 'Color picker' }, { slug: '/uix/components/stepper', label: 'Stepper' }, { slug: '/uix/components/tag-group', label: 'Tag group' }, { slug: '/uix/components/tags-input', label: 'Tags input' }, diff --git a/web/routes/uix/components/color-picker/+page.svelte b/web/routes/uix/components/color-picker/+page.svelte new file mode 100644 index 000000000..8ece30690 --- /dev/null +++ b/web/routes/uix/components/color-picker/+page.svelte @@ -0,0 +1,612 @@ + + +
+
+
Forms · ColorPicker
+

ColorPicker

+

+ Trigger + popover with 2D saturation/brightness area, hue + alpha channel sliders, + preset swatches, eye-dropper and a segmented channel input composed from ColorField. + Soma owns pointer/keyboard, gradients and color math; Eidos adds visual treatment. +

+
+ parts{compiled.parts.order.length} + events{eventRows.length} + variants3 + sizes3 +
+
+ +
+
+
+ + Brand color + + + + + + + + + + + + {#if enableAlpha} + + {/if} + + + {#snippet children({ segments }: ChannelInputSnippetProps)} + {#each segments as { part, value }, i (`${part}-${i}`)} + {value} + {/each} + + {/snippet} + + + + {#each presets as preset} + + + + + {/each} + + +
+ +
+
+
+
+
+
+
+ trace + {#if trace.length === 0} + edit, drag the area or play an event to see sema stamps + {:else} + {#each trace.slice(0, 3) as entry} + {entry.event} · {entry.family}{entry.intent + ? ' · ' + entry.intent + : ''} + {fmtTime(entry.at)} + {/each} + {/if} + + value + {valueLabel} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ soma owns value coordination, channel + gradients, pointer drag and color math. + eidos owns visual treatment. +

+ +
+ soma props · value + behaviour +
+
+ + + + + + +
+ +
+ eidos props · visual treatment +
+
+ + + +
+ +
+
+ soma + headless · provider + parts + svelte +
+
{somaSnippet}
+
+
+
+ eidos + visual · root + attached parts + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API + reference comparison

+
+ + + + + + + + + + + + + + +
FeatureAirArkChakraUIXDecision
2D areayesyesyesyesSoma owns drag + gradient calc.
Hue / Alpha slidersyesyesyesyesSame ChannelSlider component, channel prop drives gradient.
RGB / individual channel slidersnononoyesSoma exposes red/green/blue/saturation/brightness channels too.
Segmented channel inputnononoyesComposed from ColorField — unique to UIX.
Format switchyesyesyesyesFormatSelect locks when allowedFormats has one entry.
Preset swatchesyesyesyesyesSwatchGroup + SwatchTrigger pattern.
Eye dropperyesyesyesyesNative EyeDropper API (Chrome/Edge).
Picker contract (mode/Footer/commit/cancel)nononopartialHas closeOnSelect + inline. Modal mode + Footer parts pendiente alinear con date/time picker.
+
+
+ + + + + + + + + + + + +
PropOwnerDefaultNotes
value / placeholdersoma—Bindable ColorValue (hex + rgb + hsl + hsv en sync).
formatsomahexBindable. Drives ValueText, FormatSelect y ChannelInput segments.
allowedFormatssoma['hex','rgb','hsl']Whitelist en FormatSelect.
open / inlinesomafalse / falseModo popover vs inline.
closeOnSelectsomafalseCierra el popover al elegir swatch / eyedropper.
enableAlphasomatrueQuita el slider alpha cuando false.
areaChannelssoma{ x: 'saturation', y: 'brightness' }Eje X/Y de la 2D area.
size / variant / coloreidosmd / surface / primaryVisual-only data attrs.
+
+
+ {/if} + + {#if tab === 'morfo'} +
+

Morfo contract

+
+ + + + {#each partsList as part} + + + + + + + + {/each} + +
PartMarkerElementRoleOptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.role ?? '—'}{part.optional ? 'yes' : 'no'}
+
+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema · events + perceptual signature +

+

+ ColorPicker no declara eventos sema propios todavía — la apertura/cierre y commit + vienen de Popover y ColorField subyacentes. Se añadirán open / close-commit / + commit-clear cuando se alinee con el contrato canónico de pickers. +

+
+ + + + {#each eventRows as row} + + + + + + + + + + {/each} + +
OwnerNameFamilyVerbSequenceTargetPlay
{row.owner}{row.action.name}{row.action.semantic.family}{row.action.semantic.verb ?? '—'}{row.action.semantic.sequence ?? 'pre'}{row.action.target} + +
+
+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Selectors live in src/uix/eidos/components/color-picker/color-picker.css. + Reuses color-field tokens for the segmented input and adds picker-specific + tokens for the area, channel sliders, swatches and eye-dropper. +

+
+ + + + + + + + + + +
SelectorOwnerPurpose
[data-color-picker]morfoRoot scope + size/variant/color tokens.
[data-color-picker-trigger]morfoOpen button con swatch dot (CSS var --cp-trigger-color).
[data-color-picker-area]morfoContenedor 2D; soma calcula backgroundStyle y posición del thumb.
[data-color-picker-channel-slider][data-channel]morfoSlider 1D por canal; data-channel permite tintar el gradient en CSS.
[data-color-picker-swatch-trigger][data-checked]morfoPreset swatch; ring acento cuando matchea el valor actual.
[data-color-picker-eye-dropper][data-unsupported]morfoBotón eye-dropper; disabled cuando el browser no implementa la API.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+
+ + + + + + + + + + +
ConcernContract
Triggeraria-haspopup="dialog" + aria-expanded + aria-controls.
AreaAreaThumb es role="slider" con aria-valuenow reportando ambos canales.
Channel slidersChannelSliderThumb = role="slider" con keyboard arrow/home/end.
SwatchesSwatchGroup = role="group"; cada SwatchTrigger arrow-navegable; aria-checked sobre el matching.
Eye dropperdata-unsupported cuando window.EyeDropper no existe; el botón queda disabled.
FormHidden input submit como format formatted string cuando name está set.
+
+
+ {/if} +