feat(uix): ColorPicker Untitled-UI redesign (tabs + integrated ColorField)

Demo (web/routes/uix/components/color-picker): tabbed layout
(Seleccion | Presets) as a flat segmented band, a tools row (outline
eyedropper + two 12px sliders inset by the thumb diameter), and the real
<ColorField> as the value input (size-synced, value swatch, system
format-select). Icons sized canonically via --icon-size-{size}.

Framework:
- color-picker.css: popover viewport scrollbar-gutter stable->auto
  (tabbed content fits; the reserved gutter only skewed the panel right).
- ChannelInput/ChannelSegment gain visual parity with <ColorField>
  ([data-color-field] root context + data-field-segment marker + state CSS).

Presets 4-group split (Sistema/Intent/Paleta/Guardados) + eyedropper
button-size decision pending; see color-picker/CONTINUE.md.
active-uix
dev 3 months ago
parent 61ea97b5bb
commit d494729116

@ -0,0 +1,83 @@
# ColorPicker — continuación (handoff)
Rediseño del ColorPicker al estilo Untitled-UI. Estado al cierre de la sesión
**2026-06-28** (continuar 2026-06-29).
## Hecho
**ColorField** (commit `5974be26`, ya cerrado): separadores verticales por canal
+ glifos de prefijo/sufijo (`#`, `%`), y el format-select = `<Select>` del sistema.
**ColorPicker** (en este commit):
- **Layout en tabs** (demo `web/routes/uix/components/color-picker/+page.svelte`):
`<Tabs>` `Selección` | `Presets`, estilo **banda segmentada** = variant `pills`
con el activo APLANADO (`--tabs-pills-trigger-border-active: transparent`,
`--tabs-pills-trigger-shadow-active: none`, `--tabs-pills-trigger-radius:
var(--radius-sm)`). `fitted`, padding del panel a 0, fuente del trigger
`--font-size-{size}` y `size={size}` (escala con el picker).
- **Selección**: Area + fila de herramientas (eyedropper cuadrado a la izquierda
+ 2 sliders apilados a la derecha) + el ColorField.
- **Presets**: `SwatchGroup` con la lista plana de presets. **PENDIENTE: 4
grupos** (ver abajo).
- **Sliders**: track 12px (`--_slider-track-size: 12px`) + inset por el diámetro
del thumb (`inline-size: calc(100% - thumb-diameter); margin-inline: auto`) para
que el gutter no rebase los bordes izq/der.
- **Eyedropper**: cuadrado (`aspect-ratio: 1`, abarca los 2 sliders), estilo
outline (`border-color: var(--color-border-default); background: transparent`),
icono canónico `--icon-size-{size}` (vía `style="font-size: var(--icon-size-{size})"`,
el icono es `1em`).
- **Value field = el `<ColorField>` real** integrado (su propio provider,
`bind:value`/`bind:format`), con value swatch y `size={size}` — trae el
format-select del sistema + separadores + glifos. Sustituye la reimplementación
`ColorPicker.ChannelInput`.
- **Framework** (`color-picker.css`): `scrollbar-gutter: stable → auto` en el
viewport del popover. El contenido en tabs CABE (no scrollea); el gutter
reservado solo sesgaba el panel a la derecha. Panel ahora simétrico.
- **ChannelInput / ChannelSegment** (`color-picker-channel-input.svelte` /
`-segment.svelte`): paridad con `<ColorField>` — contexto `[data-color-field]`
(tokens de tamaño) + `data-field-segment` + el active-state CSS. (El demo ya usa
el `<ColorField>` real; esto queda para consumidores que compongan
`ColorPicker.ChannelInput` directo.)
## Pendiente
1. **Presets en 4 grupos** (lo grande). El tab Presets dividido en:
- **Sistema** — `HierarchyColorRole` = `primary` · `secondary` · `tertiary`.
- **Intent** — `Intent` = `risk` · `threat` · `loss` · `affirm` · `fulfill`…
- **Paleta** — `PaletteScale` (las ~32 escalas: sand, tomato, ruby, crimson,
plum, violet, iris…).
- **Guardados** — prop `savedColors` (array de hex que pasa el consumidor).
Tipos en `src/uix/eidos/lib/types.ts` (`HierarchyColorRole`, `Intent`,
`PaletteScale`). `SwatchTrigger` toma un color (hex) → resolver cada rol/escala
a su valor (`var(--color-…)` resuelto, p. ej. getComputedStyle, o las hex de la
paleta). Añadir un `savedColors` de ejemplo al demo.
2. **Eyedropper 72px vs icono 18px (md) ≈ 25%**. Decidir: ¿dejarlo abarcando los 2
sliders (icono canónico se ve pequeño) o dimensionarlo a una altura de control
canónica (icono ~50%)? Pendiente de respuesta del usuario.
3. **"90% de tamaño" de los sliders** — mencionado al principio; no aplicado (el
track ya es 12px). Aclarar a qué se refería.
## Decisiones con efecto duradero
- **Tabs = banda segmentada** (`pills` aplanado), NO `line` ni `pills` crudo. El
usuario rechazó `line` (quería segmentado) y el `pills` por defecto (redondeo +
borde + sombra). Siempre con tokens del sistema, nunca valores a ojo.
- **Iconos = `--icon-size-{size}`** (canónico, escala con el componente), NUNCA un
`rem` fijo. Igual que los iconos del field.
- **Tipografía 1:1 con el size** (`--font-size-{size}`) en tabs + field.
- **El value field ES el `<ColorField>` real** (integrado), no una
reimplementación — "compose existing components".
- **Picker popover sin gutter reservado** (`scrollbar-gutter: auto`): el layout en
tabs cabe; reservar gutter solo sesga el panel a la derecha.
## Verificación
- `npm run check`: **0 errores** en color-picker / color-field (61 pre-existentes
no relacionados).
- **Visual NO verificado** en la última parte de la sesión: había otro dev-server
en la carpeta y no se arrancó otro (para no orfanar la pestaña del usuario).
PRIMERA TAREA mañana: abrir `/uix/components/color-picker`, abrir el picker y
revisar — banda de tabs (radio/padding), eyedropper (tamaño botón vs icono),
sliders (track 12px + inset sin rebose), y el value field (ColorField integrado,
swatch, format-select).

@ -107,6 +107,30 @@ Partes públicas: `Label`, `Control`, `Trigger`, `ValueSwatch`,
parsea para emparejarlo con el valor actual y pintar
`data-checked`.
## Layout canónico (demo) + cambios 2026-06-28
El demo (`web/routes/uix/components/color-picker/+page.svelte`) monta el layout
canónico estilo Untitled-UI: **Tabs** `Selección` | `Presets` como banda
segmentada (variant `pills` aplanado — activo sin borde/sombra, `--radius-sm`),
`fitted`, fuente `--font-size-{size}`, `size={size}`. La pestaña `Selección`
apila `Area` + una fila de herramientas (eyedropper outline cuadrado + 2
`ChannelSlider` con track 12px e inset por el thumb) + el `<ColorField>` real
como value input. Los iconos se dimensionan con `--icon-size-{size}` (el
eyedropper fija `font-size` al token; el icono es `1em`).
Cambios de framework en esta pasada:
- `color-picker.css`: el viewport del popover usa `scrollbar-gutter: auto` (antes
`stable`) — el contenido en tabs cabe sin scroll y el gutter reservado solo
sesgaba el panel a la derecha → panel simétrico.
- `ChannelInput` / `ChannelSegment` ganan paridad visual con `<ColorField>`: el
ChannelInput envuelve sus partes en un contexto raíz `[data-color-field]`
(tokens de tamaño de segmento) y el ChannelSegment estampa `data-field-segment`
+ carga el active-state CSS del field.
Estado completo + pendientes (presets en 4 grupos, tamaño del eyedropper):
[`CONTINUE.md`](./CONTINUE.md).
## Gaps
| Gap | Disposición | Detalle |

@ -1,15 +1,29 @@
<script lang="ts">
import * as ColorPicker from '$soma/components/color-picker';
import { getColorPickerVisualContext } from './context.svelte';
import type {
ColorPickerChannelInputProps,
ColorPickerChannelInputSnippetProps
} from './types';
let { children: bodyContent, ...rest }: ColorPickerChannelInputProps = $props();
// The ChannelInput renders bare ColorField parts (input + segments). Without a
// `[data-color-field]` ROOT, the `--_color-field-*` size tokens (segment width
// 3ch/6ch, gap, padding, accent) are undefined → segments fall back to `auto`
// and the picker's value input diverges from a standalone `<ColorField>`.
// Provide the canonical root context here. `display: contents` carries the
// token cascade to the children WITHOUT generating a layout box (the value-row
// flex stays exactly as before).
const visual = getColorPickerVisualContext();
const size = $derived(visual?.getSize() ?? 'md');
const color = $derived(visual?.getColor() ?? 'primary');
</script>
<ColorPicker.ChannelInput {...rest}>
{#snippet children(args: ColorPickerChannelInputSnippetProps)}
{@render bodyContent?.(args)}
{/snippet}
</ColorPicker.ChannelInput>
<div data-color-field data-size={size} data-color={color} style="display: contents;">
<ColorPicker.ChannelInput {...rest}>
{#snippet children(args: ColorPickerChannelInputSnippetProps)}
{@render bodyContent?.(args)}
{/snippet}
</ColorPicker.ChannelInput>
</div>

@ -1,8 +1,14 @@
<script lang="ts">
// The picker's channel segment composes the SOMA ColorFieldSegment directly,
// bypassing the eidos <ColorField.Segment> wrapper that stamps the shared
// `data-field-segment` marker + loads the active-state layer. Re-apply both
// here so the active/focused segment gets the SAME highlight (background) +
// editable affordance as a standalone <ColorField>.
import '../field/field-segment-state.css';
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>
<ColorPicker.ChannelSegment {...rest} data-field-segment="">{@render children?.()}</ColorPicker.ChannelSegment>

@ -99,8 +99,11 @@
--_color-picker-accent: var(--color-primary-border);
--_color-picker-swatch-size: var(--color-picker-swatch-size-md);
--_popover-content-width-override: var(--color-picker-content-width-md);
--_popover-content-min-width-override: var(--color-picker-content-width-md);
/* +2rem over the base so the CANONICAL ColorField value row (6ch hex + 3ch
channels + format select, no longer recompressed to 1ch) fits on one line
without horizontal overflow. */
--_popover-content-width-override: calc(var(--color-picker-content-width-md) + 2rem);
--_popover-content-min-width-override: calc(var(--color-picker-content-width-md) + 2rem);
--_popover-content-max-width-override: calc(100vw - var(--space-4));
/* Padding · scroll · row layout all belong to the popover's inner
@ -123,12 +126,14 @@
min-inline-size: 0;
}
/* The viewport is the scroll container. Reserve the scrollbar gutter so a
vertical scrollbar appearing/disappearing mid-drag (state updates + focus
rings toggle its visibility once the picker is tall enough to scroll) does
NOT shift the area laterally. */
/* The viewport is the scroll container. The canonical (tabbed) picker layout
keeps the content within the popover height, so it doesn't scroll — leaving a
reserved `stable` gutter would show on the inline-END only, skewing the panel
(more space on the right than the left). `auto` keeps it symmetric; the
scrollbar only claims space if a consumer composes a picker tall enough to
actually scroll. */
[data-popover-content][data-color-picker-content] [data-popover-viewport] {
scrollbar-gutter: stable;
scrollbar-gutter: auto;
}
/* ── Trigger ─────────────────────────────────────────────────────────────── */
@ -589,7 +594,13 @@
box-shadow: 0 0 0 2px var(--_color-picker-accent);
}
[data-color-picker-eye-dropper][data-unsupported],
/* The EyeDropper API isn't available in every browser (e.g. Firefox). When the
browser lacks it there's no action to offer, so hide the trigger entirely
rather than showing a dead disabled button. */
[data-color-picker-eye-dropper][data-unsupported] {
display: none;
}
[data-color-picker-eye-dropper][data-disabled] {
cursor: not-allowed;
opacity: var(--color-picker-disabled-opacity);
@ -597,28 +608,20 @@
/* ── ChannelInput integration with ColorField inside the popover ────────── */
/* The channel input IS a ColorField — keep it FULLY CANONICAL (the same segment
widths 3ch/6ch, focus ring and padding as a standalone <ColorField>). The
popover is wide enough to hold the canonical row, so we only constrain the
input to the panel width; we do NOT recompress the segments or shrink the
format select (the previous `min-inline-size: 1ch` + `font-size: xs`
overrides made the picker's value input visibly diverge from the canon). */
[data-popover-content][data-color-picker-content] [data-color-field-input] {
/* Background + border stay the field's own (surface-default + border-default)
so the value field reads as an editable input that CONTRASTS with the popover
overlay surface. The previous `surface-overlay` + transparent border matched
the popover exactly, making the field look transparent (it vanished into the
panel). Only layout (fit + wrap) is popover-specific here. */
inline-size: 100%;
max-inline-size: 100%;
/* Flex child (not a hard 100% width) so it SHARES the value row with the
eyedropper — `inline-size: 100%` forced the input to the full row width and
overlapped the fixed-size eyedropper. `flex: 1 1 auto` grows to fill the
space LEFT BY the eyedropper and shrinks if needed. */
flex: 1 1 auto;
min-inline-size: 0;
overflow-x: hidden;
box-sizing: border-box;
flex-wrap: wrap;
}
[data-popover-content][data-color-picker-content] [data-color-field-segment] {
flex: 0 1 auto;
min-inline-size: 1ch;
}
[data-popover-content][data-color-picker-content] [data-color-field-format-select] {
font-size: var(--font-size-xs);
margin-inline-start: auto;
}
@media (prefers-reduced-motion: reduce) {

@ -3,10 +3,11 @@
ColorPicker,
type ColorPickerColor,
type ColorPickerSize,
type ColorPickerVariant,
type ChannelInputSnippetProps
type ColorPickerVariant
} from '$uix/eidos/components/color-picker';
import { PickerShell } from '$uix/eidos/components/picker-shell';
import { Tabs } from '$uix/eidos/components/tabs';
import { ColorField } from '$uix/eidos/components/color-field';
import { compileMorfo } from '$uix/morfo';
import { colorPickerMorfo } from '@/uix/morfo/components/color-picker';
import { colorFieldMorfo } from '@/uix/morfo/components/color-field';
@ -64,6 +65,8 @@
let showEyeDropper = $state(true);
let showSwatches = $state(true);
let showLabel = $state(true);
// Untitled-UI layout: the popover body is split into two tabs.
let pickerTab = $state('selection');
let open = $state(false);
let inline = $state(false);
let closeOnSelect = $state(false);
@ -111,7 +114,9 @@
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[];
allowedFormats = ['hex', 'rgb', 'hsl'].filter((x) =>
set.has(x as ColorFormat)
) as ColorFormat[];
if (!set.has(format)) format = allowedFormats[0];
}
@ -183,7 +188,9 @@
showHue && ' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
showHue && ' </ColorPicker.ChannelSlider>',
showAlpha && enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha">',
showAlpha && enableAlpha && ' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
showAlpha &&
enableAlpha &&
' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
showAlpha && enableAlpha && ' </ColorPicker.ChannelSlider>',
' <ColorPicker.ValueSwatch />',
showChannelInput && ' <ColorPicker.ChannelInput />',
@ -247,7 +254,8 @@
showChannelInput && ' <ColorPicker.ChannelInput />',
showEyeDropper && ' <ColorPicker.EyeDropper />',
showSwatches && ' <ColorPicker.SwatchGroup>',
showSwatches && ' {#each presets as c}<ColorPicker.SwatchTrigger color={c} />{/each}',
showSwatches &&
' {#each presets as c}<ColorPicker.SwatchTrigger color={c} />{/each}',
showSwatches && ' </ColorPicker.SwatchGroup>',
' </PickerShell.Body>',
showFooter && (showClear || showCancel || showClose) && ' <ColorPicker.Footer>',
@ -269,18 +277,17 @@
<div data-uix-eyebrow>Form · ColorPicker</div>
<h1 data-uix-page-title>ColorPicker</h1>
<p data-uix-page-lede>
Trigger + popover with a 2D saturation/brightness Area, hue + alpha
channel sliders, preset swatches, native EyeDropper API integration
and a segmented channel input composed from ColorField.
<span data-uix-layer-badge="soma">soma</span> owns pointer +
keyboard, gradient math, the HSV ⇄ RGB ⇄ HSL conversions, the
range state machine and the 9 own events
(<code>open</code>/<code>close-commit</code>/<code>close-cancel</code>/<code>close-dismiss</code>/<code>commit-clear</code>
Trigger + popover with a 2D saturation/brightness Area, hue + alpha channel sliders, preset
swatches, native EyeDropper API integration and a segmented channel input composed from
ColorField.
<span data-uix-layer-badge="soma">soma</span> owns pointer + keyboard, gradient math, the HSV
⇄ RGB ⇄ HSL conversions, the range state machine and the 9 own events (<code>open</code>/<code
>close-commit</code
>/<code>close-cancel</code>/<code>close-dismiss</code>/<code>commit-clear</code>
plus <code>handle-pick</code>/<code>handle-drag</code>/<code>commit-set</code>
fired by the Area + Sliders);
<span data-uix-layer-badge="eidos">eidos</span> adds size /
variant / color recipe, the popover shell, transparency grid
under alpha, swatch chrome and the canonical Footer composition.
<span data-uix-layer-badge="eidos">eidos</span> adds size / variant / color recipe, the popover
shell, transparency grid under alpha, swatch chrome and the canonical Footer composition.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>
@ -290,7 +297,8 @@
<span data-uix-meta-key>events</span>{events.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>inherited</span>{fieldEvents.length} ColorField · {popoverEvents.length} Popover
<span data-uix-meta-key>inherited</span>{fieldEvents.length} ColorField · {popoverEvents.length}
Popover
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>sizes</span>3
@ -333,55 +341,83 @@
<ColorPicker.Portal>
<ColorPicker.Content>
<PickerShell.Body>
<ColorPicker.Area />
<Tabs
variant="pills"
fitted
size={size}
bind:value={pickerTab}
style="--tabs-trigger-font-size: var(--font-size-{size})"
>
<Tabs.List>
<Tabs.Trigger value="selection">Selección</Tabs.Trigger>
<Tabs.Trigger value="presets">Presets</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
<!-- Channel sliders with explicit row labels so users
know what each gradient drives. -->
{#if showHue}
<div class="cp-slider-row">
<span class="cp-slider-label">Hue</span>
<ColorPicker.ChannelSlider channel="hue" />
</div>
{/if}
{#if showAlpha && enableAlpha}
<div class="cp-slider-row">
<span class="cp-slider-label">Alpha</span>
<ColorPicker.ChannelSlider channel="alpha" />
</div>
{/if}
<Tabs.Content value="selection">
<div class="cp-selection">
<ColorPicker.Area />
<!-- Compact value row: live preview swatch + segmented channel
input + format select + eyedropper, all on one line. -->
{#if showChannelInput || showEyeDropper}
<div class="cp-value-row">
<ColorPicker.ValueSwatch />
{#if showChannelInput}
<ColorPicker.ChannelInput>
{#snippet children({ segments }: ChannelInputSnippetProps)}
{#each segments as { part, value }, i (`${part}-${i}`)}
<ColorPicker.ChannelSegment {part}>{value}</ColorPicker.ChannelSegment>
{/each}
{#if showFormatSelect}<ColorPicker.FormatSelect />{/if}
{/snippet}
</ColorPicker.ChannelInput>
{/if}
{#if showEyeDropper}
<ColorPicker.EyeDropper aria-label="Pick color from screen" />
{/if}
</div>
{/if}
<!-- Tools row: eyedropper (square, left) + the two
channel sliders stacked to its right. No labels. -->
<div class="cp-tools-row">
{#if showEyeDropper}
<ColorPicker.EyeDropper
aria-label="Pick color from screen"
style="font-size: var(--icon-size-{size})"
/>
{/if}
<div class="cp-sliders">
{#if showHue}
<ColorPicker.ChannelSlider channel="hue" />
{/if}
{#if showAlpha && enableAlpha}
<ColorPicker.ChannelSlider channel="alpha" />
{/if}
</div>
</div>
{#if showChannelInput}
<!-- Value input = the REAL ColorField (its own provider, bound to the
picker's value + format), no swatch. Brings the system-Select format
switcher + separator/glyph styling instead of re-implementing them. -->
<ColorField
bind:value
bind:format
{allowedFormats}
{enableAlpha}
{disabled}
{readonly}
{size}
>
<ColorField.Input aria-label="Color value" swatch>
{#snippet children({ segments })}
{#each segments as { part, value }, i (`${part}-${i}`)}
<ColorField.Segment {part}>{value}</ColorField.Segment>
{/each}
{#if showFormatSelect}<ColorField.FormatSelect />{/if}
{/snippet}
</ColorField.Input>
</ColorField>
{/if}
</div>
</Tabs.Content>
{#if showSwatches}
<div class="cp-section-label">Preset colors</div>
<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>
{/if}
<Tabs.Content value="presets">
{#if showSwatches}
<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>
{:else}
<p class="cp-empty">No presets configured.</p>
{/if}
</Tabs.Content>
</Tabs>
</PickerShell.Body>
{#if showFooter && (showClear || showCancel || showClose)}
@ -428,7 +464,9 @@
<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 === 'recipe'} onclick={() => (tab = 'recipe')}
>Recipe</button
>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -436,8 +474,8 @@
<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="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>
@ -491,14 +529,22 @@
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>autoFormat <span data-uix-control-hint>popover FormatSelect drives the external Trigger's ValueText</span></span>
<span data-uix-control-label
>autoFormat <span data-uix-control-hint
>popover FormatSelect drives the external Trigger's ValueText</span
></span
>
<span data-uix-switch>
<input type="checkbox" bind:checked={autoFormat} />
<span data-uix-switch-label>{autoFormat ? 'sync' : 'isolated'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>Trigger ValueText <span data-uix-control-hint>composition · render the formatted text inside the Trigger</span></span>
<span data-uix-control-label
>Trigger ValueText <span data-uix-control-hint
>composition · render the formatted text inside the Trigger</span
></span
>
<span data-uix-switch>
<input type="checkbox" bind:checked={showValueText} />
<span data-uix-switch-label>{showValueText ? 'shown' : 'swatch only'}</span>
@ -569,7 +615,8 @@
<div data-uix-subsection-head>
Composition
<span style="font-weight: normal; color: var(--uix-text-muted);">
· toggle parts in / out of the popover · pure composition (the parts themselves don't read these flags — the demo's
· toggle parts in / out of the popover · pure composition (the parts themselves don't read
these flags — the demo's
{`{#if}`} branches decide whether to include them)
</span>
</div>
@ -603,14 +650,20 @@
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>FormatSelect <span data-uix-control-hint>inside ChannelInput</span></span>
<span data-uix-control-label
>FormatSelect <span data-uix-control-hint>inside ChannelInput</span></span
>
<span data-uix-switch>
<input type="checkbox" bind:checked={showFormatSelect} disabled={!showChannelInput} />
<span data-uix-switch-label>{showFormatSelect && showChannelInput ? 'shown' : 'hidden'}</span>
<span data-uix-switch-label
>{showFormatSelect && showChannelInput ? 'shown' : 'hidden'}</span
>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>EyeDropper <span data-uix-control-hint>"el gotero"</span></span>
<span data-uix-control-label
>EyeDropper <span data-uix-control-hint>"el gotero"</span></span
>
<span data-uix-switch>
<input type="checkbox" bind:checked={showEyeDropper} />
<span data-uix-switch-label>{showEyeDropper ? 'shown' : 'hidden'}</span>
@ -692,17 +745,54 @@
<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>
<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>
<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>
@ -710,14 +800,45 @@
<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>
<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>
@ -731,12 +852,10 @@
· declarative contract
</h2>
<p data-uix-section-desc>
The largest picker contract: 20+ parts (Provider, Label,
Control, Trigger, ValueSwatch / ValueText, HiddenInput,
Content, Area + AreaBackground + AreaThumb, ChannelSlider +
ChannelInput + ChannelSegment, FormatSelect, TransparencyGrid,
EyeDropper, SwatchGroup + SwatchTrigger + Swatch +
SwatchIndicator) + 9 own events. Source:
The largest picker contract: 20+ parts (Provider, Label, Control, Trigger, ValueSwatch /
ValueText, HiddenInput, Content, Area + AreaBackground + AreaThumb, ChannelSlider +
ChannelInput + ChannelSegment, FormatSelect, TransparencyGrid, EyeDropper, SwatchGroup +
SwatchTrigger + Swatch + SwatchIndicator) + 9 own events. Source:
<code>src/uix/morfo/components/color-picker.ts</code>.
</p>
@ -746,9 +865,24 @@
<tbody>
<tr><td class="name">name</td><td class="type">"{colorPickerMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{colorPickerMorfo.kebab}"</td></tr>
<tr><td class="name">scope</td><td class="type">[{colorPickerMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
<tr><td class="name">parts.length</td><td class="default">{colorPickerMorfo.parts.length}</td></tr>
<tr><td class="name">events.length</td><td class="default">{colorPickerMorfo.events?.length ?? 0} <span style="color: var(--uix-text-faint)">(+ {fieldEvents.length} ColorField + {popoverEvents.length} Popover inherited)</span></td></tr>
<tr
><td class="name">scope</td><td class="type"
>[{colorPickerMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td
></tr
>
<tr
><td class="name">parts.length</td><td class="default"
>{colorPickerMorfo.parts.length}</td
></tr
>
<tr
><td class="name">events.length</td><td class="default"
>{colorPickerMorfo.events?.length ?? 0}
<span style="color: var(--uix-text-faint)"
>(+ {fieldEvents.length} ColorField + {popoverEvents.length} Popover inherited)</span
></td
></tr
>
</tbody>
</table>
</div>
@ -756,7 +890,13 @@
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Archetype</th><th>States</th><th>Optional</th></tr></thead>
<thead
><tr
><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Archetype</th><th
>States</th
><th>Optional</th></tr
></thead
>
<tbody>
{#each partsList as part (part.kebab)}
<tr>
@ -774,7 +914,16 @@
</div>
{#each colorPickerMorfo.parts as rawPart (rawPart.kebab)}
{@const partAny = rawPart as unknown as { kebab: string; aria?: ReadonlyArray<{ attr: string; value: { kind: string }; condition?: { when: string; prop?: string; part?: string }; severity?: string }>; keyboard?: ReadonlyArray<{ key: string; action: string }> }}
{@const partAny = rawPart as unknown as {
kebab: string;
aria?: ReadonlyArray<{
attr: string;
value: { kind: string };
condition?: { when: string; prop?: string; part?: string };
severity?: string;
}>;
keyboard?: ReadonlyArray<{ key: string; action: string }>;
}}
{@const dataAttrs = compiled.contracts.dataAttrsByPart.get(partAny.kebab) ?? []}
{@const ariaAttrs = partAny.aria ?? []}
{@const keyboard = partAny.keyboard ?? []}
@ -790,7 +939,11 @@
<tr>
<td class="name">{attr.attr}</td>
<td class="type">{attr.values ? attr.values.join(' | ') : '—'}</td>
<td class="default">{'value' in attr && attr.value ? (attr as { value: { kind: string } }).value.kind : '—'}</td>
<td class="default"
>{'value' in attr && attr.value
? (attr as { value: { kind: string } }).value.kind
: '—'}</td
>
</tr>
{/each}
</tbody>
@ -801,13 +954,20 @@
{#if ariaAttrs.length}
<div data-uix-table-wrap style="margin-top: var(--uix-space-2);">
<table data-uix-table>
<thead><tr><th>aria-attr</th><th>Source kind</th><th>Condition</th><th>Severity</th></tr></thead>
<thead
><tr><th>aria-attr</th><th>Source kind</th><th>Condition</th><th>Severity</th></tr
></thead
>
<tbody>
{#each ariaAttrs as a (a.attr)}
<tr>
<td class="name">{a.attr}</td>
<td class="type">{a.value.kind}</td>
<td class="default">{a.condition ? `when ${a.condition.when}${a.condition.prop ? ` (${a.condition.prop})` : ''}${a.condition.part ? ` (${a.condition.part})` : ''}` : 'always'}</td>
<td class="default"
>{a.condition
? `when ${a.condition.when}${a.condition.prop ? ` (${a.condition.prop})` : ''}${a.condition.part ? ` (${a.condition.part})` : ''}`
: 'always'}</td
>
<td class="default">{a.severity ?? 'required'}</td>
</tr>
{/each}
@ -841,20 +1001,30 @@
<code>close-dismiss</code> / <code>close-dismiss-outside</code>
follow the picker open-state machine with prewrites stamping
<code>data-last-action</code> causa. <code>commit-clear</code>
fires on the Clear button. The 2D Area + each ChannelSlider
fire <code>handle-pick</code> (pointerdown landing on track),
fires on the Clear button. The 2D Area + each ChannelSlider fire <code>handle-pick</code>
(pointerdown landing on track),
<code>handle-drag</code> (continuous during drag) and
<code>commit-set</code> (release / Enter); these are wired
transversally from the slider primitive.
<code>commit-set</code> (release / Enter); these are wired transversally from the slider primitive.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th>target</th><th>prewrite</th><th>commit</th></tr></thead>
<thead
><tr
><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th
>target</th
><th>prewrite</th><th>commit</th></tr
></thead
>
<tbody>
{#each events as action (action.name)}
{@const sem = action.semantic}
{@const intentDecl = 'intent' in sem ? sem.intent : undefined}
{@const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}` : '—'}
{@const intentStr =
typeof intentDecl === 'string'
? intentDecl
: intentDecl
? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}`
: '—'}
<tr>
<td class="name">{action.name}</td>
<td class="type">{sem.family}</td>
@ -862,8 +1032,14 @@
<td>{sem.sequence ?? 'pre'}</td>
<td class="default">{intentStr}</td>
<td>{action.target}</td>
<td class="default">{action.prewrite.length ? action.prewrite.map((p) => `${p.attr}=${p.value}`).join(', ') : '—'}</td>
<td class="default">{action.commit ? `${action.commit.attr}=${action.commit.value}` : '—'}</td>
<td class="default"
>{action.prewrite.length
? action.prewrite.map((p) => `${p.attr}=${p.value}`).join(', ')
: '—'}</td
>
<td class="default"
>{action.commit ? `${action.commit.attr}=${action.commit.value}` : '—'}</td
>
</tr>
{/each}
</tbody>
@ -882,16 +1058,20 @@
<code>close-commit</code>, <code>close-cancel</code>,
<code>close-dismiss</code>, <code>close-dismiss-outside</code>,
<code>commit-clear</code>, <code>handle-pick</code>,
<code>handle-drag</code>, <code>commit-set</code>. The Area
and per-channel Sliders share the slider-family perceptual
signature (handle-pick on press, handle-drag continuous,
commit-set on release). Open / close events follow the picker
open-state machine; close-commit fires intent='fulfill' so
Save lands with the affirmative chime.
<code>handle-drag</code>, <code>commit-set</code>. The Area and per-channel Sliders share
the slider-family perceptual signature (handle-pick on press, handle-drag continuous,
commit-set on release). Open / close events follow the picker open-state machine;
close-commit fires intent='fulfill' so Save lands with the affirmative chime.
</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>
<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>
@ -911,9 +1091,7 @@
: row.owner === 'ColorField'
? '[data-color-field]'
: '[data-popover-content]';
const target = stageRef?.querySelector(
selector
) as HTMLElement | null;
const target = stageRef?.querySelector(selector) as HTMLElement | null;
if (!target) return;
void uix.events?.emit({
name: row.action.name,
@ -936,19 +1114,48 @@
<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.
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>
<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>
@ -959,9 +1166,8 @@
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<p data-uix-section-desc>
Composes a <strong>Popover</strong> (the WAI-ARIA dialog)
for the popover + slider per channel + radiogroup for the
preset swatches. The 2D Area exposes its thumb as a
Composes a <strong>Popover</strong> (the WAI-ARIA dialog) for the popover + slider per
channel + radiogroup for the preset swatches. The 2D Area exposes its thumb as a
<code>role='slider'</code> reporting both axes via
<code>aria-valuetext</code>. EyeDropper uses the native
<code>window.EyeDropper</code> API when available and stamps
@ -974,11 +1180,30 @@
<table data-uix-table>
<thead><tr><th>Key</th><th>Action</th></tr></thead>
<tbody>
<tr><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span> on Trigger</td><td>Open the popover; focus first interactive element (typically Area).</td></tr>
<tr><td><span data-uix-kbd>ArrowLeft</span> / <span data-uix-kbd>ArrowRight</span> on AreaThumb</td><td>Step the X-axis channel (saturation by default) by 1.</td></tr>
<tr><td><span data-uix-kbd>ArrowUp</span> / <span data-uix-kbd>ArrowDown</span> on AreaThumb</td><td>Step the Y-axis channel (brightness by default) by 1.</td></tr>
<tr><td><span data-uix-kbd>PageUp</span> / <span data-uix-kbd>PageDown</span></td><td>Step by 10 on the active axis.</td></tr>
<tr><td><span data-uix-kbd>Home</span> / <span data-uix-kbd>End</span></td><td>Jump to min / max on the X-axis channel.</td></tr>
<tr
><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span> on Trigger</td
><td>Open the popover; focus first interactive element (typically Area).</td></tr
>
<tr
><td
><span data-uix-kbd>ArrowLeft</span> / <span data-uix-kbd>ArrowRight</span> on AreaThumb</td
><td>Step the X-axis channel (saturation by default) by 1.</td></tr
>
<tr
><td
><span data-uix-kbd>ArrowUp</span> / <span data-uix-kbd>ArrowDown</span> on AreaThumb</td
><td>Step the Y-axis channel (brightness by default) by 1.</td></tr
>
<tr
><td><span data-uix-kbd>PageUp</span> / <span data-uix-kbd>PageDown</span></td><td
>Step by 10 on the active axis.</td
></tr
>
<tr
><td><span data-uix-kbd>Home</span> / <span data-uix-kbd>End</span></td><td
>Jump to min / max on the X-axis channel.</td
></tr
>
</tbody>
</table>
</div>
@ -988,15 +1213,50 @@
<table data-uix-table>
<thead><tr><th>Key</th><th>Action</th></tr></thead>
<tbody>
<tr><td><span data-uix-kbd>Tab</span> / <span data-uix-kbd>Shift+Tab</span></td><td>Cycle focus: Area → Hue slider → Alpha slider → ChannelInput segments → FormatSelect → EyeDropper → Swatches → Footer buttons.</td></tr>
<tr><td><span data-uix-kbd>ArrowLeft</span> / <span data-uix-kbd>ArrowRight</span> on ChannelSlider</td><td>Step by 1 (Shift+Arrow = 10).</td></tr>
<tr><td><span data-uix-kbd>Home</span> / <span data-uix-kbd>End</span></td><td>Jump to min / max for the channel.</td></tr>
<tr><td><span data-uix-kbd>ArrowUp</span> / <span data-uix-kbd>ArrowDown</span> on ChannelSegment</td><td>Increment / decrement the segment value.</td></tr>
<tr><td>Digit keys on ChannelSegment</td><td>Type the value directly; auto-advance.</td></tr>
<tr><td><span data-uix-kbd>ArrowKeys</span> on SwatchGroup</td><td>Roving focus across preset swatches.</td></tr>
<tr><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span> on Swatch</td><td>Apply the swatch color (fires <code>commit-set</code> on root).</td></tr>
<tr><td><span data-uix-kbd>Enter</span> on Close (Footer)</td><td>Save + close (fires <code>close-commit</code>).</td></tr>
<tr><td><span data-uix-kbd>Escape</span></td><td>inline mode: dismiss. modal mode: ignored unless Cancel is wired.</td></tr>
<tr
><td><span data-uix-kbd>Tab</span> / <span data-uix-kbd>Shift+Tab</span></td><td
>Cycle focus: Area → Hue slider → Alpha slider → ChannelInput segments →
FormatSelect → EyeDropper → Swatches → Footer buttons.</td
></tr
>
<tr
><td
><span data-uix-kbd>ArrowLeft</span> / <span data-uix-kbd>ArrowRight</span> on ChannelSlider</td
><td>Step by 1 (Shift+Arrow = 10).</td></tr
>
<tr
><td><span data-uix-kbd>Home</span> / <span data-uix-kbd>End</span></td><td
>Jump to min / max for the channel.</td
></tr
>
<tr
><td
><span data-uix-kbd>ArrowUp</span> / <span data-uix-kbd>ArrowDown</span> on ChannelSegment</td
><td>Increment / decrement the segment value.</td></tr
>
<tr
><td>Digit keys on ChannelSegment</td><td>Type the value directly; auto-advance.</td
></tr
>
<tr
><td><span data-uix-kbd>ArrowKeys</span> on SwatchGroup</td><td
>Roving focus across preset swatches.</td
></tr
>
<tr
><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span> on Swatch</td
><td>Apply the swatch color (fires <code>commit-set</code> on root).</td></tr
>
<tr
><td><span data-uix-kbd>Enter</span> on Close (Footer)</td><td
>Save + close (fires <code>close-commit</code>).</td
></tr
>
<tr
><td><span data-uix-kbd>Escape</span></td><td
>inline mode: dismiss. modal mode: ignored unless Cancel is wired.</td
></tr
>
</tbody>
</table>
</div>
@ -1006,19 +1266,67 @@
<table data-uix-table>
<thead><tr><th>Part</th><th>Attribute</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">trigger</td><td>aria-haspopup / aria-expanded / aria-controls</td><td class="type">"dialog" / {`{open}`} / {`<content id>`}</td></tr>
<tr><td class="name">trigger</td><td>aria-label</td><td class="type">"Pick color" or {`{aria-label}`}</td></tr>
<tr><td class="name">content</td><td>role / aria-modal / aria-labelledby</td><td class="type">"dialog" / "true" when mode='modal' / {`<label id>`}</td></tr>
<tr
><td class="name">trigger</td><td>aria-haspopup / aria-expanded / aria-controls</td
><td class="type">"dialog" / {`{open}`} / {`<content id>`}</td></tr
>
<tr
><td class="name">trigger</td><td>aria-label</td><td class="type"
>"Pick color" or {`{aria-label}`}</td
></tr
>
<tr
><td class="name">content</td><td>role / aria-modal / aria-labelledby</td><td
class="type">"dialog" / "true" when mode='modal' / {`<label id>`}</td
></tr
>
<tr><td class="name">area</td><td>role</td><td class="type">"group"</td></tr>
<tr><td class="name">area-thumb</td><td>role / aria-valuenow / aria-valuetext</td><td class="type">"slider" with 2-axis report</td></tr>
<tr><td class="name">channel-slider[data-channel]</td><td>role / aria-valuenow / aria-valuetext</td><td class="type">"slider" per channel</td></tr>
<tr><td class="name">channel-input (group of segments)</td><td>role</td><td class="type">"group"</td></tr>
<tr><td class="name">channel-segment</td><td>role / aria-valuemin/max/now/text</td><td class="type">"spinbutton"</td></tr>
<tr><td class="name">format-select</td><td>role / aria-label</td><td class="type">"combobox" or native &lt;select&gt;</td></tr>
<tr><td class="name">eye-dropper</td><td>type / aria-label / disabled / data-unsupported</td><td class="type">"button" / "Pick color from screen" / "true" when API absent</td></tr>
<tr><td class="name">swatch-group</td><td>role / aria-label</td><td class="type">"radiogroup" / "Preset colors"</td></tr>
<tr><td class="name">swatch-trigger</td><td>role / aria-checked / data-checked</td><td class="type">"radio" / matches current value</td></tr>
<tr><td class="name">hidden-input</td><td>name / required</td><td class="type">{`{name}`} / mirrors required state · emits formatted color string</td></tr>
<tr
><td class="name">area-thumb</td><td>role / aria-valuenow / aria-valuetext</td><td
class="type">"slider" with 2-axis report</td
></tr
>
<tr
><td class="name">channel-slider[data-channel]</td><td
>role / aria-valuenow / aria-valuetext</td
><td class="type">"slider" per channel</td></tr
>
<tr
><td class="name">channel-input (group of segments)</td><td>role</td><td class="type"
>"group"</td
></tr
>
<tr
><td class="name">channel-segment</td><td>role / aria-valuemin/max/now/text</td><td
class="type">"spinbutton"</td
></tr
>
<tr
><td class="name">format-select</td><td>role / aria-label</td><td class="type"
>"combobox" or native &lt;select&gt;</td
></tr
>
<tr
><td class="name">eye-dropper</td><td
>type / aria-label / disabled / data-unsupported</td
><td class="type">"button" / "Pick color from screen" / "true" when API absent</td
></tr
>
<tr
><td class="name">swatch-group</td><td>role / aria-label</td><td class="type"
>"radiogroup" / "Preset colors"</td
></tr
>
<tr
><td class="name">swatch-trigger</td><td>role / aria-checked / data-checked</td><td
class="type">"radio" / matches current value</td
></tr
>
<tr
><td class="name">hidden-input</td><td>name / required</td><td class="type"
>{`{name}`} / mirrors required state · emits formatted color string</td
></tr
>
</tbody>
</table>
</div>
@ -1027,47 +1335,66 @@
</div>
<style>
.cp-slider-row {
display: flex;
align-items: center;
gap: var(--uix-space-2, 0.5rem);
/* Picker tabs flush: kill the recipe's content-panel padding so the area /
sliders / field align with the popover edges. The footer buttons stay
OUTSIDE the tabs (rendered after <Tabs>, shared across both panels). */
:global([data-color-picker-content] [data-tabs-content]) {
padding: 0;
}
.cp-slider-label {
flex: 0 0 3.25rem;
font-size: 11px;
font-weight: 500;
color: var(--uix-text-faint);
text-transform: uppercase;
letter-spacing: 0.04em;
/* Rail = flat segmented band: the active segment is a solid fill, NOT the
default raised pill — drop its border + shadow and use a minimal radius.
All via the tabs' own tokens (no ad-hoc values). */
:global([data-color-picker-content] [data-tabs]) {
--tabs-pills-trigger-border-active: transparent;
--tabs-pills-trigger-shadow-active: none;
--tabs-pills-trigger-radius: var(--radius-sm);
}
/* Compact value row: swatch + channel input + eyedropper inline. */
.cp-value-row {
.cp-selection {
display: grid;
gap: var(--uix-space-3, 0.75rem);
}
/* Tools row: eyedropper square (left) spanning the two stacked sliders. */
.cp-tools-row {
display: flex;
align-items: center;
align-items: stretch;
gap: var(--uix-space-2, 0.5rem);
min-inline-size: 0;
}
.cp-value-row :global([data-color-picker-value-swatch]) {
.cp-tools-row :global([data-color-picker-eye-dropper]) {
flex: 0 0 auto;
inline-size: 1.75rem;
block-size: 1.75rem;
align-self: stretch;
aspect-ratio: 1;
block-size: auto;
inline-size: auto;
/* Outline style: visible border, transparent fill. The pipette icon is
`size="1em"`, so scale it via font-size to stay proportional to the
(taller) square button rather than the default ~1em. */
border-color: var(--color-border-default);
background: transparent;
}
.cp-value-row :global([data-color-field]) {
.cp-sliders {
flex: 1 1 auto;
min-inline-size: 0;
display: grid;
gap: var(--uix-space-2, 0.5rem);
align-content: center;
}
.cp-value-row :global([data-color-field-input]) {
flex-wrap: nowrap;
/* Keep the gutter (thumb) within the rail: inset each slider by the thumb's
diameter, centered, so the thumb never overruns the left / right edges at
0 % / 100 % (the soma positions it edge-to-edge with `left: {pct}%`). */
.cp-sliders :global([data-color-picker-channel-slider][data-slider]) {
inline-size: calc(100% - var(--_slider-thumb-size, 20px) - 2 * var(--border-width-medium, 2px));
margin-inline: auto;
--_slider-track-size: 12px;
}
.cp-value-row :global([data-color-picker-eye-dropper]) {
flex: 0 0 auto;
/* Value input = the ColorField (no swatch); fill the row. */
.cp-selection :global([data-color-field-input]) {
inline-size: 100%;
flex-wrap: nowrap;
}
.cp-section-label {
font-size: 11px;
font-weight: 500;
.cp-empty {
font-size: 12px;
color: var(--uix-text-faint);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-block-start: var(--uix-space-1, 0.25rem);
text-align: center;
padding-block: var(--uix-space-4, 1rem);
}
</style>

Loading…
Cancel
Save

Powered by TurnKey Linux.