feat(eidos): add ColorPicker wrapper + demo

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) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 2f147897d9
commit 2731ee567b

@ -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
<ColorPicker bind:value bind:format bind:open size="md">
<ColorPicker.Label>Brand color</ColorPicker.Label>
<ColorPicker.Control>
<ColorPicker.Trigger>
<ColorPicker.ValueText />
</ColorPicker.Trigger>
<ColorPicker.HiddenInput />
</ColorPicker.Control>
<ColorPicker.Portal>
<ColorPicker.Content>
<ColorPicker.Area />
<ColorPicker.ChannelSlider channel="hue" />
<ColorPicker.ChannelSlider channel="alpha" />
<ColorPicker.ChannelInput>
{#snippet children({ segments })}
{#each segments as { part, value } (part + value)}
<ColorPicker.ChannelSegment {part}>{value}</ColorPicker.ChannelSegment>
{/each}
<ColorPicker.FormatSelect />
{/snippet}
</ColorPicker.ChannelInput>
<ColorPicker.SwatchGroup>
{#each presets as color}
<ColorPicker.SwatchTrigger {color} />
{/each}
</ColorPicker.SwatchGroup>
<ColorPicker.EyeDropper />
</ColorPicker.Content>
</ColorPicker.Portal>
</ColorPicker>
```
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
`<ColorPicker>` 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
`<ColorPicker.Trigger showSwatch={false}>`.
- `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

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerAreaBackgroundProps } from './types';
let { children, ...rest }: ColorPickerAreaBackgroundProps = $props();
</script>
<ColorPicker.AreaBackground {...rest}>{@render children?.()}</ColorPicker.AreaBackground>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerAreaThumbProps } from './types';
let { children, ...rest }: ColorPickerAreaThumbProps = $props();
</script>
<ColorPicker.AreaThumb {...rest}>{@render children?.()}</ColorPicker.AreaThumb>

@ -0,0 +1,17 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerAreaProps, ColorPickerAreaSnippetProps } from './types';
let { children: bodyContent, ...rest }: ColorPickerAreaProps = $props();
</script>
<ColorPicker.Area {...rest}>
{#snippet children(snippetProps: ColorPickerAreaSnippetProps)}
{#if bodyContent}
{@render bodyContent(snippetProps)}
{:else}
<ColorPicker.AreaBackground />
<ColorPicker.AreaThumb />
{/if}
{/snippet}
</ColorPicker.Area>

@ -0,0 +1,15 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type {
ColorPickerChannelInputProps,
ColorPickerChannelInputSnippetProps
} from './types';
let { children: bodyContent, ...rest }: ColorPickerChannelInputProps = $props();
</script>
<ColorPicker.ChannelInput {...rest}>
{#snippet children(args: ColorPickerChannelInputSnippetProps)}
{@render bodyContent?.(args)}
{/snippet}
</ColorPicker.ChannelInput>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerChannelSegmentProps } from './types';
let { children, ...rest }: ColorPickerChannelSegmentProps = $props();
</script>
<ColorPicker.ChannelSegment {...rest}>{@render children?.()}</ColorPicker.ChannelSegment>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerChannelSliderThumbProps } from './types';
let { children, ...rest }: ColorPickerChannelSliderThumbProps = $props();
</script>
<ColorPicker.ChannelSliderThumb {...rest}>{@render children?.()}</ColorPicker.ChannelSliderThumb>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerChannelSliderTrackProps } from './types';
let { children, ...rest }: ColorPickerChannelSliderTrackProps = $props();
</script>
<ColorPicker.ChannelSliderTrack {...rest}>{@render children?.()}</ColorPicker.ChannelSliderTrack>

@ -0,0 +1,20 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type {
ColorPickerChannelSliderProps,
ColorPickerChannelSliderSnippetProps
} from './types';
let { children: bodyContent, ...rest }: ColorPickerChannelSliderProps = $props();
</script>
<ColorPicker.ChannelSlider {...rest}>
{#snippet children(snippetProps: ColorPickerChannelSliderSnippetProps)}
{#if bodyContent}
{@render bodyContent(snippetProps)}
{:else}
<ColorPicker.ChannelSliderTrack />
<ColorPicker.ChannelSliderThumb />
{/if}
{/snippet}
</ColorPicker.ChannelSlider>

@ -0,0 +1,32 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import PopoverContent from '../popover/popover-content.svelte';
import { getColorPickerVisualContext } from './context.svelte';
import type { ColorPickerContentProps } from './types';
let {
size,
align = 'start',
sideOffset = 10,
collisionPadding = 12,
children: bodyContent,
...rest
}: ColorPickerContentProps = $props();
const eidos = ActiveEidos.require();
const visual = getColorPickerVisualContext();
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize() ?? 'md', 'md'));
</script>
<PopoverContent
{...rest}
{align}
{sideOffset}
{collisionPadding}
size={resolvedSize}
data-color-picker-content=""
>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
{/snippet}
</PopoverContent>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerControlProps } from './types';
let { children, ...rest }: ColorPickerControlProps = $props();
</script>
<ColorPicker.Control {...rest}>{@render children?.()}</ColorPicker.Control>

@ -0,0 +1,15 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import PipetteIcon from '../icon/lib/pipette.svelte';
import type { ColorPickerEyeDropperProps } from './types';
let { children, ...rest }: ColorPickerEyeDropperProps = $props();
</script>
<ColorPicker.EyeDropper {...rest}>
{#if children}
{@render children()}
{:else}
<PipetteIcon size="1em" decorative />
{/if}
</ColorPicker.EyeDropper>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerFormatSelectProps } from './types';
let { children, ...rest }: ColorPickerFormatSelectProps = $props();
</script>
<ColorPicker.FormatSelect {...rest}>{@render children?.()}</ColorPicker.FormatSelect>

@ -0,0 +1,5 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
</script>
<ColorPicker.HiddenInput />

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerLabelProps } from './types';
let { children, ...rest }: ColorPickerLabelProps = $props();
</script>
<ColorPicker.Label {...rest}>{@render children?.()}</ColorPicker.Label>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerSwatchGroupProps } from './types';
let { children, ...rest }: ColorPickerSwatchGroupProps = $props();
</script>
<ColorPicker.SwatchGroup {...rest}>{@render children?.()}</ColorPicker.SwatchGroup>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerSwatchIndicatorProps } from './types';
let { children, ...rest }: ColorPickerSwatchIndicatorProps = $props();
</script>
<ColorPicker.SwatchIndicator {...rest}>{@render children?.()}</ColorPicker.SwatchIndicator>

@ -0,0 +1,15 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerSwatchTriggerProps } from './types';
let { children, ...rest }: ColorPickerSwatchTriggerProps = $props();
</script>
<ColorPicker.SwatchTrigger {...rest}>
{#if children}
{@render children()}
{:else}
<ColorPicker.Swatch />
<ColorPicker.SwatchIndicator />
{/if}
</ColorPicker.SwatchTrigger>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerSwatchProps } from './types';
let { children, ...rest }: ColorPickerSwatchProps = $props();
</script>
<ColorPicker.Swatch {...rest}>{@render children?.()}</ColorPicker.Swatch>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerTransparencyGridProps } from './types';
let { children, ...rest }: ColorPickerTransparencyGridProps = $props();
</script>
<ColorPicker.TransparencyGrid {...rest}>{@render children?.()}</ColorPicker.TransparencyGrid>

@ -0,0 +1,52 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import * as ColorPicker from '$soma/components/color-picker';
import { ColorPickerProvider } from '$soma/components/color-picker';
import { getColorPickerVisualContext } from './context.svelte';
import type { ColorPickerTriggerProps } from './types';
let {
size,
variant,
color,
showSwatch = true,
children,
style,
...rest
}: ColorPickerTriggerProps = $props();
const eidos = ActiveEidos.require();
const visual = getColorPickerVisualContext();
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize() ?? 'md', 'md'));
const resolvedVariant = $derived(variant ?? visual?.getVariant() ?? 'surface');
const resolvedColor = $derived(color ?? visual?.getColor() ?? 'primary');
const provider = ColorPickerProvider.require();
// Expose the current value to CSS so the trigger swatch dot tints itself.
const swatchStyle = $derived(
showSwatch
? `--cp-trigger-color: ${provider.opts.value.current?.hex ?? 'transparent'}`
: ''
);
const composedStyle = $derived(
[swatchStyle, style as string | undefined].filter(Boolean).join('; ')
);
</script>
<ColorPicker.Trigger
{...rest}
data-size={resolvedSize}
data-variant={resolvedVariant}
data-color={resolvedColor}
data-show-swatch={showSwatch ? '' : undefined}
style={composedStyle}
>
{#if showSwatch}
<span class="color-picker-trigger-swatch" aria-hidden="true"></span>
{/if}
{#if children}
{@render children()}
{:else if !showSwatch}
Pick color
{/if}
</ColorPicker.Trigger>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerValueSwatchProps } from './types';
let { children, ...rest }: ColorPickerValueSwatchProps = $props();
</script>
<ColorPicker.ValueSwatch {...rest}>{@render children?.()}</ColorPicker.ValueSwatch>

@ -0,0 +1,8 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import type { ColorPickerValueTextProps } from './types';
let { children, ...rest }: ColorPickerValueTextProps = $props();
</script>
<ColorPicker.ValueText {...rest}>{@render children?.()}</ColorPicker.ValueText>

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

@ -0,0 +1,40 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import * as ColorPicker from '$soma/components/color-picker';
import { setColorPickerVisualContext } from './context.svelte';
import type { ColorPickerProps } from './types';
let {
size = 'md',
variant = 'surface',
color = 'primary',
value = $bindable(),
placeholder = $bindable(),
format = $bindable(),
open = $bindable(false),
children,
...rest
}: ColorPickerProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
setColorPickerVisualContext({
getSize: () => resolvedSize,
getVariant: () => variant,
getColor: () => color
});
</script>
<ColorPicker.Provider
{...rest}
bind:value
bind:placeholder
bind:format
bind:open
data-size={resolvedSize}
data-variant={variant}
data-color={color}
>
{@render children?.()}
</ColorPicker.Provider>

@ -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<ColorPickerVisualContext | undefined>(COLOR_PICKER_VISUAL_CONTEXT);
}

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

@ -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<Size, 'sm' | 'md' | 'lg'>
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<ColorPickerSize>
/** 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<ColorPickerSize>
variant?: ColorPickerVariant
color?: ColorPickerColor
/** When true (default) the trigger shows a swatch dot before its content. */
showSwatch?: boolean
}
export type ColorPickerContentProps = Omit<PopoverContentProps, 'size'> & {
size?: ResponsiveProp<ColorPickerSize>
}
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
}

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

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

@ -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)',

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

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

@ -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' },

@ -0,0 +1,612 @@
<script lang="ts">
import {
ColorPicker,
type ColorPickerColor,
type ColorPickerSize,
type ColorPickerVariant,
type ChannelInputSnippetProps
} from '$uix/eidos/components/color-picker';
import { compileMorfo } from '$uix/morfo';
import { colorPickerMorfo } from '@/uix/morfo/components/color-picker';
import { colorFieldMorfo } from '@/uix/morfo/components/color-field';
import { popoverMorfo } from '@/uix/morfo/components/popover';
import { getActiveUix } from '$active-uix';
import { colorValueFromRgb, type ColorValue, type ColorFormat } from '$libs/color';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
const brand: ColorValue = colorValueFromRgb({ r: 30, g: 144, b: 255, a: 1 });
const accent: ColorValue = colorValueFromRgb({ r: 234, g: 88, b: 12, a: 0.85 });
const presets = [
'#ef4444',
'#f97316',
'#eab308',
'#22c55e',
'#06b6d4',
'#3b82f6',
'#a855f7',
'#ec4899',
'#64748b',
'#0f172a',
'#ffffff'
];
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
let value = $state<ColorValue | undefined>(brand);
let placeholder = $state<ColorValue>(brand);
let format = $state<ColorFormat>('hex');
let open = $state(false);
let inline = $state(false);
let closeOnSelect = $state(false);
let disabled = $state(false);
let readonly = $state(false);
let enableAlpha = $state(true);
let allowedFormats = $state<ColorFormat[]>(['hex', 'rgb', 'hsl']);
let size = $state<ColorPickerSize>('md');
let variant = $state<ColorPickerVariant>('surface');
let color = $state<ColorPickerColor>('primary');
const compiled = compileMorfo(colorPickerMorfo);
const colorFieldCompiled = compileMorfo(colorFieldMorfo);
const popoverCompiled = compileMorfo(popoverMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
const fieldEvents = $derived([...colorFieldCompiled.actions.byName.values()]);
const popoverEvents = $derived([...popoverCompiled.actions.byName.values()]);
const eventRows = $derived([
...events.map((action) => ({ owner: 'ColorPicker', action })),
...fieldEvents.map((action) => ({ owner: 'ColorField', action })),
...popoverEvents.map((action) => ({ owner: 'Popover', action }))
]);
function sameValue(a: ColorValue | undefined, b: ColorValue): boolean {
return a?.hex === b.hex;
}
function setDemoValue(kind: 'brand' | 'accent' | 'empty'): void {
if (kind === 'empty') {
value = undefined;
return;
}
value = kind === 'brand' ? brand : accent;
}
function toggleAllowedFormat(f: ColorFormat): void {
const set = new Set(allowedFormats);
if (set.has(f)) set.delete(f);
else set.add(f);
if (set.size === 0) set.add(f);
allowedFormats = ['hex', 'rgb', 'hsl'].filter((x) => set.has(x as ColorFormat)) as ColorFormat[];
if (!set.has(format)) format = allowedFormats[0];
}
function fmtTime(at: number): string {
return new Intl.DateTimeFormat(undefined, {
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 2
}).format(at);
}
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '—',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
return () => obs.disconnect();
});
const valueLabel = $derived(value?.hex ?? 'empty');
const somaSnippet = $derived(
[
"<script lang='ts'>",
" import * as ColorPicker from '$soma/components/color-picker';",
" import { colorValueFromRgb, type ColorValue } from '$libs/color';",
'',
' let value = $state<ColorValue | undefined>(colorValueFromRgb({ r: 30, g: 144, b: 255, a: 1 }));',
" let format = $state<'hex' | 'rgb' | 'hsl'>('hex');",
' let open = $state(false);',
'</' + 'script>',
'',
'<ColorPicker.Provider bind:value bind:format bind:open',
inline && ' inline',
closeOnSelect && ' closeOnSelect',
!enableAlpha && ' enableAlpha={false}',
'>',
' <ColorPicker.Trigger><ColorPicker.ValueText /></ColorPicker.Trigger>',
' <ColorPicker.Content>',
' <ColorPicker.Area><ColorPicker.AreaBackground /><ColorPicker.AreaThumb /></ColorPicker.Area>',
' <ColorPicker.ChannelSlider channel="hue">',
' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
' </ColorPicker.ChannelSlider>',
enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha">',
enableAlpha && ' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
enableAlpha && ' </ColorPicker.ChannelSlider>',
' <ColorPicker.SwatchGroup>{#each presets as c}<ColorPicker.SwatchTrigger color={c} />{/each}</ColorPicker.SwatchGroup>',
' </ColorPicker.Content>',
'</ColorPicker.Provider>'
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { ColorPicker } from '$uix/eidos/components/color-picker';",
" import { colorValueFromRgb, type ColorValue } from '$libs/color';",
'',
' let value = $state<ColorValue | undefined>(colorValueFromRgb({ r: 30, g: 144, b: 255, a: 1 }));',
" let format = $state<'hex' | 'rgb' | 'hsl'>('hex');",
' let open = $state(false);',
'</' + 'script>',
'',
'<ColorPicker bind:value bind:format bind:open',
size !== 'md' && ` size="${size}"`,
variant !== 'surface' && ` variant="${variant}"`,
color !== 'primary' && ` color="${color}"`,
inline && ' inline',
closeOnSelect && ' closeOnSelect',
!enableAlpha && ' enableAlpha={false}',
'>',
' <ColorPicker.Trigger><ColorPicker.ValueText /></ColorPicker.Trigger>',
' <ColorPicker.Portal>',
' <ColorPicker.Content>',
' <ColorPicker.Area />',
' <ColorPicker.ChannelSlider channel="hue" />',
enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha" />',
' <ColorPicker.SwatchGroup>{#each presets as c}<ColorPicker.SwatchTrigger color={c} />{/each}</ColorPicker.SwatchGroup>',
' <ColorPicker.EyeDropper />',
' </ColorPicker.Content>',
' </ColorPicker.Portal>',
'</ColorPicker>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Forms · ColorPicker</div>
<h1 data-uix-page-title>ColorPicker</h1>
<p data-uix-page-lede>
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.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill
><span data-uix-meta-key>parts</span>{compiled.parts.order.length}</span
>
<span data-uix-meta-pill><span data-uix-meta-key>events</span>{eventRows.length}</span>
<span data-uix-meta-pill><span data-uix-meta-key>variants</span>3</span>
<span data-uix-meta-pill><span data-uix-meta-key>sizes</span>3</span>
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<div style="inline-size: min(32rem, 100%); display: grid; gap: var(--uix-space-3);">
<ColorPicker
bind:value
bind:placeholder
bind:format
bind:open
{inline}
{closeOnSelect}
{disabled}
{readonly}
{enableAlpha}
{allowedFormats}
{size}
{variant}
{color}
>
<ColorPicker.Label>Brand color</ColorPicker.Label>
<ColorPicker.Control>
<ColorPicker.Trigger>
<ColorPicker.ValueText />
</ColorPicker.Trigger>
<ColorPicker.HiddenInput />
</ColorPicker.Control>
<ColorPicker.Portal>
<ColorPicker.Content>
<ColorPicker.Area />
<ColorPicker.ChannelSlider channel="hue" />
{#if enableAlpha}
<ColorPicker.ChannelSlider channel="alpha" />
{/if}
<ColorPicker.ChannelInput>
{#snippet children({ segments }: ChannelInputSnippetProps)}
{#each segments as { part, value }, i (`${part}-${i}`)}
<ColorPicker.ChannelSegment {part}>{value}</ColorPicker.ChannelSegment>
{/each}
<ColorPicker.FormatSelect />
{/snippet}
</ColorPicker.ChannelInput>
<ColorPicker.SwatchGroup aria-label="Preset colors">
{#each presets as preset}
<ColorPicker.SwatchTrigger color={preset}>
<ColorPicker.Swatch color={preset} />
<ColorPicker.SwatchIndicator />
</ColorPicker.SwatchTrigger>
{/each}
</ColorPicker.SwatchGroup>
<div style="display: flex; gap: var(--uix-space-2); align-items: center; justify-content: flex-end;">
<ColorPicker.EyeDropper />
</div>
</ColorPicker.Content>
</ColorPicker.Portal>
</ColorPicker>
</div>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>edit, drag the area or play an event to see sema stamps</span>
{:else}
{#each trace.slice(0, 3) as entry}
<span
><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent
? ' · ' + entry.intent
: ''}</span
>
<span style="color: var(--uix-text-faint)">{fmtTime(entry.at)}</span>
{/each}
{/if}
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>value</span>
{valueLabel}
</span>
</div>
</div>
<div data-uix-tabs role="tablist">
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>API</button>
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
<span data-uix-layer-badge="morfo">morfo</span>
<span data-uix-tab-count>{partsList.length}p · {events.length}e</span>
</button>
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
<span data-uix-layer-badge="sema">sema</span>
<span data-uix-tab-count>{eventRows.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
{#if tab === 'live'}
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>
<p data-uix-section-desc>
<span data-uix-layer-badge="soma">soma</span> owns value coordination, channel
gradients, pointer drag and color math.
<span data-uix-layer-badge="eidos">eidos</span> owns visual treatment.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> props · value + behaviour
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>value</span>
<span data-uix-chips role="group">
<button
data-uix-chip
data-active={sameValue(value, brand)}
onclick={() => setDemoValue('brand')}>brand</button
>
<button
data-uix-chip
data-active={sameValue(value, accent)}
onclick={() => setDemoValue('accent')}>accent</button
>
<button
data-uix-chip
data-active={value === undefined}
onclick={() => setDemoValue('empty')}>empty</button
>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>format</span>
<span data-uix-chips role="radiogroup">
{#each ['hex', 'rgb', 'hsl'] as v}
<button
data-uix-chip
data-active={format === v}
disabled={!allowedFormats.includes(v as ColorFormat)}
onclick={() => (format = v as ColorFormat)}>{v}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>allowed formats</span>
<span data-uix-chips role="group">
{#each ['hex', 'rgb', 'hsl'] as v}
<button
data-uix-chip
data-active={allowedFormats.includes(v as ColorFormat)}
onclick={() => toggleAllowedFormat(v as ColorFormat)}>{v}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>popover</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={inline} />
<span data-uix-switch-label>inline</span>
</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={closeOnSelect} disabled={inline} />
<span data-uix-switch-label>close on swatch select</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>channels</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={enableAlpha} />
<span data-uix-switch-label>enable alpha</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>states</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={disabled} />
<span data-uix-switch-label>disabled</span>
</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={readonly} />
<span data-uix-switch-label>readonly</span>
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each ['sm', 'md', 'lg'] as v}
<button
data-uix-chip
data-active={size === v}
onclick={() => (size = v as ColorPickerSize)}>{v}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each ['surface', 'outline', 'ghost'] as v}
<button
data-uix-chip
data-active={variant === v}
onclick={() => (variant = v as ColorPickerVariant)}>{v}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>color</span>
<span data-uix-chips role="radiogroup">
{#each ['primary', 'secondary', 'neutral', 'affirm', 'fulfill', 'risk', 'threat', 'loss'] as v}
<button
data-uix-chip
data-active={color === v}
onclick={() => (color = v as ColorPickerColor)}>{v}</button
>
{/each}
</span>
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless · provider + parts</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{somaSnippet}</code></pre>
</div>
<div data-uix-code style="margin-top: var(--uix-space-3);">
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · root + attached parts</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
</section>
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API + reference comparison</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead>
<tr><th>Feature</th><th>Air</th><th>Ark</th><th>Chakra</th><th>UIX</th><th>Decision</th></tr>
</thead>
<tbody>
<tr><td class="name">2D area</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>Soma owns drag + gradient calc.</td></tr>
<tr><td class="name">Hue / Alpha sliders</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>Same ChannelSlider component, channel prop drives gradient.</td></tr>
<tr><td class="name">RGB / individual channel sliders</td><td>no</td><td>no</td><td>no</td><td>yes</td><td>Soma exposes red/green/blue/saturation/brightness channels too.</td></tr>
<tr><td class="name">Segmented channel input</td><td>no</td><td>no</td><td>no</td><td>yes</td><td>Composed from ColorField — unique to UIX.</td></tr>
<tr><td class="name">Format switch</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>FormatSelect locks when allowedFormats has one entry.</td></tr>
<tr><td class="name">Preset swatches</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>SwatchGroup + SwatchTrigger pattern.</td></tr>
<tr><td class="name">Eye dropper</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>Native EyeDropper API (Chrome/Edge).</td></tr>
<tr><td class="name">Picker contract (mode/Footer/commit/cancel)</td><td>no</td><td>no</td><td>no</td><td>partial</td><td>Has closeOnSelect + inline. Modal mode + Footer parts pendiente alinear con date/time picker.</td></tr>
</tbody>
</table>
</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Owner</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">value / placeholder</td><td>soma</td><td class="default">—</td><td>Bindable ColorValue (hex + rgb + hsl + hsv en sync).</td></tr>
<tr><td class="name">format</td><td>soma</td><td class="default">hex</td><td>Bindable. Drives ValueText, FormatSelect y ChannelInput segments.</td></tr>
<tr><td class="name">allowedFormats</td><td>soma</td><td class="default">['hex','rgb','hsl']</td><td>Whitelist en FormatSelect.</td></tr>
<tr><td class="name">open / inline</td><td>soma</td><td class="default">false / false</td><td>Modo popover vs inline.</td></tr>
<tr><td class="name">closeOnSelect</td><td>soma</td><td class="default">false</td><td>Cierra el popover al elegir swatch / eyedropper.</td></tr>
<tr><td class="name">enableAlpha</td><td>soma</td><td class="default">true</td><td>Quita el slider alpha cuando false.</td></tr>
<tr><td class="name">areaChannels</td><td>soma</td><td class="default"><code>&#123; x: 'saturation', y: 'brightness' &#125;</code></td><td>Eje X/Y de la 2D area.</td></tr>
<tr><td class="name">size / variant / color</td><td>eidos</td><td class="default">md / surface / primary</td><td>Visual-only data attrs.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>Morfo contract</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Optional</th></tr></thead>
<tbody>
{#each partsList as part}
<tr>
<td class="name">{part.kebab}</td>
<td><code data-uix-part-marker>[{part.marker}]</code></td>
<td class="type">&lt;{part.defaultElement}&gt;</td>
<td class="type">{part.role ?? '—'}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> · events + perceptual signature
</h2>
<p data-uix-section-desc>
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.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Owner</th><th>Name</th><th>Family</th><th>Verb</th><th>Sequence</th><th>Target</th><th>Play</th></tr></thead>
<tbody>
{#each eventRows as row}
<tr>
<td class="default">{row.owner}</td>
<td class="name">{row.action.name}</td>
<td class="type">{row.action.semantic.family}</td>
<td>{row.action.semantic.verb ?? '—'}</td>
<td>{row.action.semantic.sequence ?? 'pre'}</td>
<td>{row.action.target}</td>
<td>
<button
data-uix-play
onclick={() => {
const selector =
row.owner === 'ColorPicker'
? '[data-color-picker]'
: row.owner === 'ColorField'
? '[data-color-field]'
: '[data-popover-content]';
const target = stageRef?.querySelector(
selector
) as HTMLElement | null;
if (!target) return;
void uix.events?.emit({
name: row.action.name,
family: row.action.semantic.family,
target
});
}}>▶ play</button
>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
Selectors live in <code>src/uix/eidos/components/color-picker/color-picker.css</code>.
Reuses <code>color-field</code> tokens for the segmented input and adds picker-specific
tokens for the area, channel sliders, swatches and eye-dropper.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Owner</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td class="name"><code>[data-color-picker]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Root scope + size/variant/color tokens.</td></tr>
<tr><td class="name"><code>[data-color-picker-trigger]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Open button con swatch dot (CSS var <code>--cp-trigger-color</code>).</td></tr>
<tr><td class="name"><code>[data-color-picker-area]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Contenedor 2D; soma calcula <code>backgroundStyle</code> y posición del thumb.</td></tr>
<tr><td class="name"><code>[data-color-picker-channel-slider][data-channel]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Slider 1D por canal; <code>data-channel</code> permite tintar el gradient en CSS.</td></tr>
<tr><td class="name"><code>[data-color-picker-swatch-trigger][data-checked]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Preset swatch; ring acento cuando matchea el valor actual.</td></tr>
<tr><td class="name"><code>[data-color-picker-eye-dropper][data-unsupported]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Botón eye-dropper; disabled cuando el browser no implementa la API.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
<tbody>
<tr><td class="name">Trigger</td><td><code>aria-haspopup="dialog"</code> + <code>aria-expanded</code> + <code>aria-controls</code>.</td></tr>
<tr><td class="name">Area</td><td><code>AreaThumb</code> es <code>role="slider"</code> con <code>aria-valuenow</code> reportando ambos canales.</td></tr>
<tr><td class="name">Channel sliders</td><td><code>ChannelSliderThumb</code> = <code>role="slider"</code> con keyboard arrow/home/end.</td></tr>
<tr><td class="name">Swatches</td><td><code>SwatchGroup</code> = <code>role="group"</code>; cada <code>SwatchTrigger</code> arrow-navegable; <code>aria-checked</code> sobre el matching.</td></tr>
<tr><td class="name">Eye dropper</td><td><code>data-unsupported</code> cuando <code>window.EyeDropper</code> no existe; el botón queda disabled.</td></tr>
<tr><td class="name">Form</td><td>Hidden input submit como <code>format</code> formatted string cuando <code>name</code> está set.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>
Loading…
Cancel
Save

Powered by TurnKey Linux.