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
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
|
||||
}
|
||||
@ -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>{ x: 'saturation', y: 'brightness' }</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"><{part.defaultElement}></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…
Reference in new issue