feat(eidos): color-field leading value swatch + full-width coherence

- <ColorField.Input swatch> renders an opt-in leading colour chip showing
  the field's current value, sized to the control icon (--icon-size-{size})
  via a concrete token — never 1em, so it cannot recompute/jitter.
- field-segment-state: drop the active-segment font-weight 600; bold widened
  the ch-based segment ~0.5px, resizing the highlight on every increment. The
  accent background already marks the active segment.
- color-field stays full-width like the whole field/select family — width is
  container-driven, not field-driven (no per-component exception).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent baa1604f5b
commit e16c169f37

@ -2,11 +2,14 @@
import * as ColorField from '$soma/components/color-field'; import * as ColorField from '$soma/components/color-field';
import type { ColorFieldInputProps, ColorFieldInputSnippetProps } from './types'; import type { ColorFieldInputProps, ColorFieldInputSnippetProps } from './types';
let { children: bodyContent, ...rest }: ColorFieldInputProps = $props(); let { children: bodyContent, swatch = false, ...rest }: ColorFieldInputProps = $props();
</script> </script>
<ColorField.Input {...rest}> <ColorField.Input {...rest}>
{#snippet children(args: ColorFieldInputSnippetProps)} {#snippet children(args: ColorFieldInputSnippetProps)}
{#if swatch}
<span data-color-field-swatch aria-hidden="true"></span>
{/if}
{@render bodyContent?.(args)} {@render bodyContent?.(args)}
{/snippet} {/snippet}
</ColorField.Input> </ColorField.Input>

@ -182,6 +182,23 @@
min-inline-size: calc(6ch + 2 * var(--_color-field-segment-px)); min-inline-size: calc(6ch + 2 * var(--_color-field-segment-px));
} }
/* Leading value swatch (opt-in via <ColorField.Input swatch>). Sized to the
control icon for this field size via the `--_color-field-swatch-size` token
(= `--icon-size-{size}`) — a CONCRETE length, not `1em`, so it can never
recompute/jitter on re-render. The colour comes from --cf-swatch-color, set
on the root from the field value. Inset border keeps light colours visible. */
[data-color-field-swatch] {
flex: 0 0 auto;
inline-size: var(--_color-field-swatch-size);
block-size: var(--_color-field-swatch-size);
/* Shares the field's own corner radius token (no hand-rolled calc) so it
tracks the field and reads as part of the same control. */
border-radius: var(--color-field-radius);
background: var(--cf-swatch-color, transparent);
box-shadow: inset 0 0 0 var(--border-width) var(--color-border-subtle);
margin-inline-end: var(--_color-field-segment-px);
}
[data-color-field-segment][data-placeholder] { [data-color-field-segment][data-placeholder] {
color: var(--color-field-placeholder-color); color: var(--color-field-placeholder-color);
} }

@ -11,12 +11,22 @@
value = $bindable(), value = $bindable(),
placeholder = $bindable(), placeholder = $bindable(),
format = $bindable(), format = $bindable(),
style,
children: bodyContent, children: bodyContent,
...rest ...rest
}: ColorFieldProps = $props(); }: ColorFieldProps = $props();
const eidos = ActiveEidos.require(); const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md')); const resolvedSize = $derived(eidos.resolve(size, 'md'));
// Publish the current colour as a CSS var on the root so a leading
// <ColorField.Input swatch> can preview it (inherits the var). Falls back to
// the placeholder colour, else unset (swatch renders transparent).
const swatchColor = $derived(value?.hex ?? placeholder?.hex);
const rootStyle = $derived(
[style, swatchColor && `--cf-swatch-color: ${swatchColor}`]
.filter(Boolean)
.join('; ') || undefined
);
</script> </script>
<ColorField.Provider <ColorField.Provider
@ -24,6 +34,7 @@
bind:value bind:value
bind:placeholder bind:placeholder
bind:format bind:format
style={rootStyle}
data-size={resolvedSize} data-size={resolvedSize}
data-variant={variant} data-variant={variant}
data-color={color} data-color={color}

@ -26,7 +26,11 @@ export type ColorFieldProps = ProviderProps & {
} }
export type ColorFieldLabelProps = LabelProps export type ColorFieldLabelProps = LabelProps
export type ColorFieldInputProps = InputProps export type ColorFieldInputProps = InputProps & {
/** Show a leading swatch of the field's current colour, sized like the
* control icon (`1em`). The colour tracks the field value. @default false */
swatch?: boolean
}
export type ColorFieldSegmentProps = SegmentProps export type ColorFieldSegmentProps = SegmentProps
export type ColorFieldFormatSelectProps = FormatSelectProps export type ColorFieldFormatSelectProps = FormatSelectProps
export type ColorFieldHiddenInputProps = HiddenInputProps export type ColorFieldHiddenInputProps = HiddenInputProps

@ -35,8 +35,10 @@
[data-field-segment]:focus, [data-field-segment]:focus,
[data-field-segment]:focus-visible { [data-field-segment]:focus-visible {
/* NO font-weight change: bold widens the ch-based segment width (~0.5px),
so the active segment — a coloured highlight rectangle — would resize on
every increment. The accent background already marks the active segment. */
background: var(--field-segment-active-bg); background: var(--field-segment-active-bg);
color: var(--field-segment-active-text); color: var(--field-segment-active-text);
font-weight: 600;
outline: none; outline: none;
} }

@ -1525,11 +1525,11 @@
--color-field-segment-readonly-color: var(--color-content-secondary); --color-field-segment-readonly-color: var(--color-content-secondary);
--color-field-segment-readonly-decoration: underline dotted var(--color-content-muted); --color-field-segment-readonly-decoration: underline dotted var(--color-content-muted);
--color-field-segment-focus-shadow: inset 0 0 0 var(--border-width) var(--_color-field-accent-border); --color-field-segment-focus-shadow: inset 0 0 0 var(--border-width) var(--_color-field-accent-border);
--color-field-swatch-size-xs: calc(var(--control-height-xs) - var(--space-2)); --color-field-swatch-size-xs: var(--icon-size-xs);
--color-field-swatch-size-sm: calc(var(--control-height-sm) - var(--space-2)); --color-field-swatch-size-sm: var(--icon-size-sm);
--color-field-swatch-size-md: calc(var(--control-height-md) - var(--space-2)); --color-field-swatch-size-md: var(--icon-size-md);
--color-field-swatch-size-lg: calc(var(--control-height-lg) - var(--space-2)); --color-field-swatch-size-lg: var(--icon-size-lg);
--color-field-swatch-size-xl: calc(var(--control-height-xl) - var(--space-2)); --color-field-swatch-size-xl: var(--icon-size-xl);
--color-field-format-select-color: var(--color-content-secondary); --color-field-format-select-color: var(--color-content-secondary);
--color-field-format-select-bg: transparent; --color-field-format-select-bg: transparent;
--color-field-format-select-radius: var(--radius-sm); --color-field-format-select-radius: var(--radius-sm);

@ -1353,11 +1353,14 @@ export const THEME_BASE_RECIPE_TOKENS = {
'segment-readonly-color': 'var(--color-content-secondary)', 'segment-readonly-color': 'var(--color-content-secondary)',
'segment-readonly-decoration': 'underline dotted var(--color-content-muted)', 'segment-readonly-decoration': 'underline dotted var(--color-content-muted)',
'segment-focus-shadow': 'inset 0 0 0 var(--border-width) var(--_color-field-accent-border)', 'segment-focus-shadow': 'inset 0 0 0 var(--border-width) var(--_color-field-accent-border)',
'swatch-size-xs': 'calc(var(--control-height-xs) - var(--space-2))', // Leading value swatch: exactly the control icon size for this field size
'swatch-size-sm': 'calc(var(--control-height-sm) - var(--space-2))', // (user spec: "del mismo tamaño que los iconos del control"). A concrete
'swatch-size-md': 'calc(var(--control-height-md) - var(--space-2))', // length per size — never `1em` — so it can't recompute on re-render.
'swatch-size-lg': 'calc(var(--control-height-lg) - var(--space-2))', 'swatch-size-xs': 'var(--icon-size-xs)',
'swatch-size-xl': 'calc(var(--control-height-xl) - var(--space-2))', 'swatch-size-sm': 'var(--icon-size-sm)',
'swatch-size-md': 'var(--icon-size-md)',
'swatch-size-lg': 'var(--icon-size-lg)',
'swatch-size-xl': 'var(--icon-size-xl)',
'format-select-color': 'var(--color-content-secondary)', 'format-select-color': 'var(--color-content-secondary)',
'format-select-bg': 'transparent', 'format-select-bg': 'transparent',
'format-select-radius': 'var(--radius-sm)', 'format-select-radius': 'var(--radius-sm)',

@ -264,7 +264,7 @@
{color} {color}
> >
{#if showLabel}<ColorField.Label>{labelText}</ColorField.Label>{/if} {#if showLabel}<ColorField.Label>{labelText}</ColorField.Label>{/if}
<ColorField.Input {name} aria-label={ariaLabel}> <ColorField.Input {name} aria-label={ariaLabel} swatch>
{#snippet children({ segments }: InputSnippetProps)} {#snippet children({ segments }: InputSnippetProps)}
{#each segments as { part, value }, index (`${part}-${index}`)} {#each segments as { part, value }, index (`${part}-${index}`)}
<ColorField.Segment {part}>{value}</ColorField.Segment> <ColorField.Segment {part}>{value}</ColorField.Segment>

Loading…
Cancel
Save

Powered by TurnKey Linux.