uix(color): abre `color` en skeleton · spinner · textarea

Tres standalones al forward de paleta compartido (`resolveComponentColor` +
recipe `_palette-*`):

- skeleton: tinta el wash (slot track, 50% alpha) + el highlight del shimmer.
  Colored-only: el default `'neutral'` NO estampa data-color y conserva su
  mezcla base cross-mode (oklab + 16% solid). Delta declarado: affirm/fulfill/
  risk/threat/loss ganan highlight tintado en el shimmer (antes solo primary/
  secondary lo definian; la regla generica uniformiza la cobertura).
- spinner: patron C puro — la base consume `--_spinner-palette-{text,track}`
  (host = primary, behavior-preserving); fuera los 7 bloques per-rol.
  `'inherit'` se conserva como union aditiva con su regla literal
  (currentColor); no puede viajar por el seed custom (trampa `--x: inherit`).
- textarea: tinta el borde de focus (slot solid). Colored-only: el default
  `'primary'` no estampa y conserva `--focus-ring-color` (que NO es
  primary-solid: es un mix al 52% — estampar habria cambiado el default).

Verificado en Chrome (probe getComputedStyle): default intacto en los tres;
risk/teal/amber/jade = token compartido exacto; custom hex → color-mix del
seed. check: 0 errores en los componentes tocados; contract test 26/28 (2
pre-existentes de proof-of-human).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-sec-dom
dev 3 months ago
parent 0886bfe538
commit 60a30c2e60

@ -76,28 +76,14 @@
/* ── Color accents ─────────────────────────────────────────────────── */
[data-skeleton][data-color='primary'] {
--_skeleton-bg: color-mix(in srgb, var(--color-primary-track) 50%, transparent);
--_skeleton-highlight: color-mix(in srgb, var(--color-primary-track) 90%, white 8%);
}
[data-skeleton][data-color='secondary'] {
--_skeleton-bg: color-mix(in srgb, var(--color-secondary-track) 50%, transparent);
--_skeleton-highlight: color-mix(in srgb, var(--color-secondary-track) 90%, white 8%);
}
[data-skeleton][data-color='affirm'] {
--_skeleton-bg: color-mix(in srgb, var(--color-affirm-track) 50%, transparent);
}
[data-skeleton][data-color='fulfill'] {
--_skeleton-bg: color-mix(in srgb, var(--color-fulfill-track) 50%, transparent);
}
[data-skeleton][data-color='risk'] {
--_skeleton-bg: color-mix(in srgb, var(--color-risk-track) 50%, transparent);
}
[data-skeleton][data-color='threat'] {
--_skeleton-bg: color-mix(in srgb, var(--color-threat-track) 50%, transparent);
}
[data-skeleton][data-color='loss'] {
--_skeleton-bg: color-mix(in srgb, var(--color-loss-track) 50%, transparent);
/* Colour comes from the shared palette forward (`--_skeleton-palette-track`):
any role / 33 scale / custom value tints the wash + shimmer highlight.
`'neutral'` (the default) never stamps `data-color`, so the base cross-mode
grey mix above stays untouched. */
[data-skeleton][data-color],
[data-skeleton][data-color-custom] {
--_skeleton-bg: color-mix(in srgb, var(--_skeleton-palette-track) 50%, transparent);
--_skeleton-highlight: color-mix(in srgb, var(--_skeleton-palette-track) 90%, white 8%);
}
/* ── Animation (shimmer wave) ──────────────────────────────────────── */

@ -13,6 +13,8 @@
*/
import './skeleton.css';
import { ActiveEidos } from '$uix/eidos';
import { composeInlineStyle } from '$uix/eidos/lib/style';
import { resolveComponentColor } from '$uix/eidos/lib/component-color';
import { SKELETON_LANGS } from './langs';
import type { SkeletonProps } from './types';
@ -23,6 +25,7 @@
lines = 1,
animated = true,
'aria-label': ariaLabel,
style,
children,
...rest
}: SkeletonProps = $props();
@ -33,6 +36,12 @@
const resolvedSize = $derived(eidos.resolve(size, 'md'));
const resolvedLabel = $derived(ariaLabel ?? eidos.langs.ts(SKELETON_LANGS.LABEL));
// `color` accepts the full system: canonical → `data-color`, raw value →
// `data-color-custom` + the `--color-custom` seed. `'neutral'` (the default)
// stamps nothing so the base grey wash keeps its cross-mode mix.
const colorAttrs = $derived(resolveComponentColor(color === 'neutral' ? undefined : color));
const mergedStyle = $derived(composeInlineStyle(style, colorAttrs.customStyle));
// Stagger widths for multi-line text skeletons so the block reads as
// a paragraph rather than a solid rectangle. Last line always shorter.
const lineWidths = $derived.by(() => {
@ -51,11 +60,13 @@
data-skeleton=""
data-shape="text"
data-size={resolvedSize}
data-color={color}
data-color={colorAttrs.dataColor}
data-color-custom={colorAttrs.isCustom ? '' : undefined}
data-animated={animated ? '' : undefined}
role="status"
aria-busy="true"
aria-label={resolvedLabel}
style={mergedStyle}
{...rest}
>
{#each lineWidths as w, i (i)}
@ -67,11 +78,13 @@
data-skeleton=""
data-shape={resolvedShape}
data-size={resolvedSize}
data-color={color}
data-color={colorAttrs.dataColor}
data-color-custom={colorAttrs.isCustom ? '' : undefined}
data-animated={animated ? '' : undefined}
role="status"
aria-busy="true"
aria-label={resolvedLabel}
style={mergedStyle}
{...rest}
>
{@render children?.()}

@ -1,10 +1,10 @@
import type { Snippet } from 'svelte';
import type { ColorRole, ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { ComponentColorProp, ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { BoxProps } from '../box/types';
export type SkeletonShape = 'rect' | 'circle' | 'text';
export type SkeletonSize = Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
export type SkeletonColor = ColorRole;
export type SkeletonColor = ComponentColorProp;
/**
* Props for `<Skeleton>`.
@ -19,7 +19,10 @@ export type SkeletonProps = Omit<BoxProps, 'display'> & {
shape?: ResponsiveProp<SkeletonShape>;
/** Reservation size. @default 'md' (only meaningful when no width/height set) */
size?: ResponsiveProp<SkeletonSize>;
/** Accent palette (placeholder background lightness). @default 'neutral' */
/**
* Placeholder tint — any role / intent / 33 donor scales / raw CSS color.
* `'neutral'` (the default) keeps the base grey wash. @default 'neutral'
*/
color?: SkeletonColor;
/**
* Number of stacked bars when `shape='text'`. Each bar's width

@ -13,8 +13,8 @@
--_spinner-size: 1.25rem;
--_spinner-thickness: 2px;
--_spinner-duration: 0.9s;
--_spinner-color: var(--color-primary-text);
--_spinner-track: color-mix(in srgb, var(--color-primary-track) 60%, transparent);
--_spinner-color: var(--_spinner-palette-text);
--_spinner-track: color-mix(in srgb, var(--_spinner-palette-track) 60%, transparent);
--_spinner-gap: var(--space-2);
display: inline-flex;
@ -44,35 +44,9 @@
--_spinner-thickness: 4px;
}
/* Color tokens — accent + track. */
[data-spinner][data-color='secondary'] {
--_spinner-color: var(--color-secondary-text);
--_spinner-track: color-mix(in srgb, var(--color-secondary-track) 60%, transparent);
}
[data-spinner][data-color='neutral'] {
--_spinner-color: var(--color-neutral-text);
--_spinner-track: color-mix(in srgb, var(--color-neutral-track) 60%, transparent);
}
[data-spinner][data-color='affirm'] {
--_spinner-color: var(--color-affirm-text);
--_spinner-track: color-mix(in srgb, var(--color-affirm-track) 60%, transparent);
}
[data-spinner][data-color='fulfill'] {
--_spinner-color: var(--color-fulfill-text);
--_spinner-track: color-mix(in srgb, var(--color-fulfill-track) 60%, transparent);
}
[data-spinner][data-color='risk'] {
--_spinner-color: var(--color-risk-text);
--_spinner-track: color-mix(in srgb, var(--color-risk-track) 60%, transparent);
}
[data-spinner][data-color='threat'] {
--_spinner-color: var(--color-threat-text);
--_spinner-track: color-mix(in srgb, var(--color-threat-track) 60%, transparent);
}
[data-spinner][data-color='loss'] {
--_spinner-color: var(--color-loss-text);
--_spinner-track: color-mix(in srgb, var(--color-loss-track) 60%, transparent);
}
/* Colour comes from the shared palette forward (`--_spinner-palette-*`): the
base tokens above consume it directly (host default = primary), so any role /
33 scale / custom value routes through with no per-role cascade. */
/* `inherit` — take the surrounding `currentColor` (for embedding in a control
that owns the colour, e.g. a solid Button whose text is the contrast colour). */

@ -12,6 +12,8 @@
*/
import './spinner.css';
import { ActiveEidos } from '$uix/eidos';
import { composeInlineStyle } from '$uix/eidos/lib/style';
import { resolveComponentColor } from '$uix/eidos/lib/component-color';
import { SPINNER_LANGS } from './langs';
import type { SpinnerProps } from './types';
@ -21,6 +23,7 @@
color = 'primary',
presentation = false,
'aria-label': ariaLabel,
style,
children,
...rest
}: SpinnerProps = $props();
@ -29,17 +32,31 @@
const resolvedSize = $derived(eidos.resolve(size, 'md'));
const resolvedLabel = $derived(ariaLabel ?? eidos.langs.ts(SPINNER_LANGS.LABEL));
// `color` accepts the full system: canonical → `data-color`, raw value →
// `data-color-custom` + the `--color-custom` seed. `'inherit'` keeps its own
// literal stamp — routing it through the custom seed would make the seed
// itself inherit from the parent, not the ink (the `--x: inherit` trap).
const colorAttrs = $derived.by(() => {
if (color === 'inherit') {
return { dataColor: 'inherit', isCustom: false, customStyle: undefined };
}
return resolveComponentColor(color);
});
const mergedStyle = $derived(composeInlineStyle(style, colorAttrs.customStyle));
</script>
<div
data-spinner=""
data-variant={variant}
data-size={resolvedSize}
data-color={color}
data-color={colorAttrs.dataColor}
data-color-custom={colorAttrs.isCustom ? '' : undefined}
role={presentation ? undefined : 'status'}
aria-live={presentation ? undefined : 'polite'}
aria-label={presentation ? undefined : resolvedLabel}
aria-hidden={presentation ? 'true' : undefined}
style={mergedStyle}
{...rest}
>
{#if variant === 'dots'}

@ -1,9 +1,9 @@
import type { Snippet } from 'svelte';
import type { ColorRole, ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { ComponentColorProp, ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { BoxProps } from '../box/types';
export type SpinnerSize = Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
export type SpinnerColor = ColorRole | 'inherit';
export type SpinnerColor = ComponentColorProp | 'inherit';
export type SpinnerVariant = 'ring' | 'dots' | 'bars';
/**
@ -20,9 +20,9 @@ export type SpinnerProps = Omit<BoxProps, 'display'> & {
/** Spinner size. @default 'md' */
size?: ResponsiveProp<SpinnerSize>;
/**
* Accent palette, or `'inherit'` to take the surrounding `currentColor`
* (for embedding inside a control that owns the colour, e.g. Button).
* @default 'primary'
* Accent — any role / intent / 33 donor scales / raw CSS color, or
* `'inherit'` to take the surrounding `currentColor` (for embedding inside
* a control that owns the colour, e.g. Button). @default 'primary'
*/
color?: SpinnerColor;
/**

@ -52,29 +52,13 @@
--_textarea-font-size: var(--font-size-xl);
}
/* Color accents — applied on focus + when invalid. Each role's `-solid` (the
saturated accent, step 9) is the existing token closest to the intended strong
focus border. */
[data-textarea][data-color='secondary'] {
--_textarea-border-focus: var(--color-secondary-solid);
}
[data-textarea][data-color='neutral'] {
--_textarea-border-focus: var(--color-neutral-solid);
}
[data-textarea][data-color='affirm'] {
--_textarea-border-focus: var(--color-affirm-solid);
}
[data-textarea][data-color='fulfill'] {
--_textarea-border-focus: var(--color-fulfill-solid);
}
[data-textarea][data-color='risk'] {
--_textarea-border-focus: var(--color-risk-solid);
}
[data-textarea][data-color='threat'] {
--_textarea-border-focus: var(--color-threat-solid);
}
[data-textarea][data-color='loss'] {
--_textarea-border-focus: var(--color-loss-solid);
/* Color accent — applied on focus. The solid slot (the saturated accent,
step 9) comes from the shared palette forward (`--_textarea-palette-solid`):
any role / 33 scale / custom value routes through. `'primary'` (the default)
never stamps `data-color`, so the base `--focus-ring-color` stays. */
[data-textarea][data-color],
[data-textarea][data-color-custom] {
--_textarea-border-focus: var(--_textarea-palette-solid);
}
/* `resize` is owned by eidos (presentation), driven by the morfo's

@ -5,6 +5,8 @@
* Eidos `<TextArea>` — visual wrapper over Soma's multi-line text primitive.
* Soma owns value, autosize math, submit shortcut and Field integration.
*/
import { composeInlineStyle } from '$uix/eidos/lib/style';
import { resolveComponentColor } from '$uix/eidos/lib/component-color';
import * as TextArea from '$soma/components/textarea';
import type { TextAreaProps } from './types';
@ -13,12 +15,19 @@
variant = 'surface',
color = 'primary',
value = $bindable(''),
style,
children: bodyContent,
...rest
}: TextAreaProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
// `color` accepts the full system: canonical → `data-color`, raw value →
// `data-color-custom` + the `--color-custom` seed. `'primary'` (the default)
// stamps nothing so the focus border keeps the shared `--focus-ring-color`.
const colorAttrs = $derived(resolveComponentColor(color === 'primary' ? undefined : color));
const mergedStyle = $derived(composeInlineStyle(style, colorAttrs.customStyle));
</script>
<TextArea.Provider
@ -26,7 +35,9 @@
bind:value
data-size={resolvedSize}
data-variant={variant}
data-color={color}
data-color={colorAttrs.dataColor}
data-color-custom={colorAttrs.isCustom ? '' : undefined}
style={mergedStyle}
>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}

@ -5,11 +5,11 @@ import type {
ProviderSnippetProps as TextAreaProviderSnippetProps,
CountSnippetProps as TextAreaCountSnippetProps
} from '$soma/components/textarea';
import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { ComponentColorProp, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types';
export type TextAreaSize = Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
export type TextAreaVariant = ControlVariant;
export type TextAreaColor = ColorRole;
export type TextAreaColor = ComponentColorProp;
/**
* Props for the eidos `<TextArea>`.
@ -23,7 +23,11 @@ export type TextAreaProps = ProviderProps & {
size?: ResponsiveProp<TextAreaSize>;
/** Surface treatment for the textarea control. @default 'surface' */
variant?: TextAreaVariant;
/** Accent palette for focus and invalid states. @default 'primary' */
/**
* Focus accent — any role / intent / 33 donor scales / raw CSS color.
* `'primary'` (the default) keeps the shared `--focus-ring-color` border.
* @default 'primary'
*/
color?: TextAreaColor;
};

@ -4547,6 +4547,32 @@
--_mark-palette-text: var(--palette-text, var(--color-primary-text));
}
[data-skeleton] {
--_skeleton-palette-track: var(--color-primary-track);
}
[data-skeleton][data-color], [data-skeleton][data-color-custom] {
--_skeleton-palette-track: var(--palette-track, var(--color-primary-track));
}
[data-spinner] {
--_spinner-palette-text: var(--color-primary-text);
--_spinner-palette-track: var(--color-primary-track);
}
[data-spinner][data-color], [data-spinner][data-color-custom] {
--_spinner-palette-track: var(--palette-track, var(--color-primary-track));
--_spinner-palette-text: var(--palette-text, var(--color-primary-text));
}
[data-textarea] {
--_textarea-palette-solid: var(--color-primary-solid);
}
[data-textarea][data-color], [data-textarea][data-color-custom] {
--_textarea-palette-solid: var(--palette-solid, var(--color-primary-solid));
}
[data-calendar] {
--_calendar-palette-solid: var(--color-primary-solid);
--_calendar-palette-solid-hover: var(--color-primary-solid-hover);

@ -1749,6 +1749,25 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'_palette-element': { value: 'var(--color-fulfill-element)', scope: 'host' },
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
},
// Skeleton — `color` tints the placeholder wash (track slot). Colored-only:
// the `'neutral'` default stamps no `data-color` and keeps the base grey mix,
// so the host value here is the forward fallback only.
skeleton: {
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' }
},
// Spinner — `color` tints the ink (text slot) + the ring track. The base
// tokens consume the palette directly (host default = primary); `'inherit'`
// keeps its own literal rule (currentColor takeover) outside the forward.
spinner: {
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' },
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' }
},
// TextArea — `color` tints the focus border (solid slot). Colored-only: the
// `'primary'` default stamps no `data-color` and keeps `--focus-ring-color`,
// so the host value here is the forward fallback only.
textarea: {
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' }
},
calendar: {
gap: 'var(--space-3)',
'padding-xs': 'var(--space-2)',

Loading…
Cancel
Save

Powered by TurnKey Linux.