|
|
import type {
|
|
|
RecipeTokenSet,
|
|
|
RecipeTokenMap,
|
|
|
RecipeTokenMultiDeclaration,
|
|
|
RecipeTokenDeclaration
|
|
|
} from '../config-types';
|
|
|
import { defineRecipes } from './define';
|
|
|
|
|
|
// ── Per-instance palette cascade (THEMING §25) ────────────────────────────
|
|
|
//
|
|
|
// `palette-{slot}` is RESERVED recipe vocabulary: the GENERATOR appends the
|
|
|
// full per-scale `color:{scale}` cascade to any token with that name
|
|
|
// (`universalPaletteDecls`, render-css.ts) — recipes only author the
|
|
|
// role/intent declarations. Button below is the pilot consumer.
|
|
|
|
|
|
// Wrapped in `defineRecipes` — the TYPE WALL of the dimensional vocabulary:
|
|
|
// a physical-axis key (`px` / `py` / `padding-x`…) is a COMPILE error
|
|
|
// (recipe-contract §1; normalized 2026-07-06).
|
|
|
export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
|
|
|
accordion: {
|
|
|
'provider-border-width': 'var(--border-width)',
|
|
|
'provider-radius': 'var(--radius-lg)',
|
|
|
'outline-provider-bg': 'var(--color-surface-default)',
|
|
|
'outline-provider-border': 'var(--color-border-default)',
|
|
|
'outline-item-border': 'var(--color-border-subtle)',
|
|
|
'outline-content-bg': 'var(--color-surface-default)',
|
|
|
'surface-provider-bg': 'var(--color-surface-raised)',
|
|
|
'surface-provider-border': 'var(--color-border-subtle)',
|
|
|
'surface-provider-shadow': 'var(--shadow-subtle)',
|
|
|
'surface-item-border': 'var(--color-border-subtle)',
|
|
|
'surface-content-bg': 'var(--color-surface-default)',
|
|
|
'ghost-provider-bg': 'transparent',
|
|
|
'ghost-provider-border': 'transparent',
|
|
|
'ghost-item-border': 'var(--color-border-subtle)',
|
|
|
'ghost-content-bg': 'transparent',
|
|
|
'item-border-width': 'var(--border-width)',
|
|
|
'trigger-min-height-xs': 'var(--size-xs-control-height)',
|
|
|
'trigger-min-height-sm': 'var(--size-sm-control-height)',
|
|
|
'trigger-min-height-md': 'var(--size-md-control-height)',
|
|
|
'trigger-min-height-lg': 'var(--size-lg-control-height)',
|
|
|
'trigger-min-height-full': 'var(--size-xl-control-height)',
|
|
|
'trigger-padding-inline-xs': 'var(--space-2-5)',
|
|
|
'trigger-padding-inline-sm': 'var(--space-3)',
|
|
|
'trigger-padding-inline-md': 'var(--space-4)',
|
|
|
'trigger-padding-inline-lg': 'var(--space-5)',
|
|
|
'trigger-padding-inline-full': 'var(--space-6)',
|
|
|
'trigger-padding-block-xs': 'var(--space-2)',
|
|
|
'trigger-padding-block-sm': 'var(--space-2-5)',
|
|
|
'trigger-padding-block-md': 'var(--space-3)',
|
|
|
'trigger-padding-block-lg': 'var(--space-3-5)',
|
|
|
'trigger-padding-block-full': 'var(--space-4)',
|
|
|
'trigger-gap': 'var(--space-3)',
|
|
|
'trigger-font-family': 'var(--style-label-font-family)',
|
|
|
'trigger-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'trigger-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'trigger-font-size-md': 'var(--size-md-font-size)',
|
|
|
'trigger-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'trigger-font-size-full': 'var(--size-xl-font-size)',
|
|
|
'trigger-font-weight': 'var(--font-weight-medium)',
|
|
|
'trigger-line-height': 'var(--leading-ui)',
|
|
|
'trigger-fg': 'var(--color-content-primary)',
|
|
|
// Open state: keep the trigger text in the primary content tone.
|
|
|
// The chevron rotation + revealed content already signal openness;
|
|
|
// recoloring the heading adds noise without information (matches
|
|
|
// shadcn/Radix/Ark).
|
|
|
'open-trigger-fg': 'var(--color-content-primary)',
|
|
|
'disabled-trigger-fg': 'var(--color-content-disabled)',
|
|
|
'trigger-hover-fg': 'var(--color-content-primary)',
|
|
|
'trigger-transition-duration': 'var(--duration-fast)',
|
|
|
'trigger-transition-ease': 'var(--ease-default)',
|
|
|
'trigger-focus-ring-offset': 'var(--focus-ring-offset)',
|
|
|
'trigger-focus-ring-width': 'var(--focus-ring-width)',
|
|
|
'trigger-focus-ring-color': 'var(--focus-ring-color)',
|
|
|
'content-padding-inline-xs': 'var(--space-2-5)',
|
|
|
'content-padding-inline-sm': 'var(--space-3)',
|
|
|
'content-padding-inline-md': 'var(--space-4)',
|
|
|
'content-padding-inline-lg': 'var(--space-5)',
|
|
|
'content-padding-inline-full': 'var(--space-6)',
|
|
|
'content-padding-block-xs': 'var(--space-2)',
|
|
|
'content-padding-block-sm': 'var(--space-2-5)',
|
|
|
'content-padding-block-md': 'var(--space-3)',
|
|
|
'content-padding-block-lg': 'var(--space-4)',
|
|
|
'content-padding-block-full': 'var(--space-5)',
|
|
|
'content-font-family': 'var(--font-family-secondary)',
|
|
|
'content-font-size': 'var(--size-md-font-size)',
|
|
|
'content-line-height': 'var(--leading-prose)',
|
|
|
'content-fg': 'var(--color-content-secondary)',
|
|
|
'content-open-duration': 'var(--duration-normal)',
|
|
|
'content-close-duration': 'var(--duration-fast)',
|
|
|
'content-open-ease': 'var(--ease-out)',
|
|
|
'content-close-ease': 'var(--ease-in)',
|
|
|
'indicator-size-xs': 'var(--size-xs-icon-size)',
|
|
|
'indicator-size-sm': 'var(--size-sm-icon-size)',
|
|
|
'indicator-size-md': 'var(--size-sm-icon-size)',
|
|
|
'indicator-size-lg': 'var(--size-md-icon-size)',
|
|
|
'indicator-fg': 'var(--color-content-secondary)',
|
|
|
// Indicator turns to the primary content tone when open — a subtle
|
|
|
// emphasis without the loud accent recolor.
|
|
|
'open-indicator-fg': 'var(--color-content-primary)',
|
|
|
'indicator-rotation': '180deg',
|
|
|
'indicator-transition-duration': 'var(--duration-fast)',
|
|
|
'indicator-transition-ease': 'var(--ease-default)'
|
|
|
},
|
|
|
avatar: ((): RecipeTokenMap => {
|
|
|
// TSC v2.2 — 2D matrix (variant × color) cascade.
|
|
|
//
|
|
|
// Avatar's per-instance palette varies across 8 colors × 3
|
|
|
// variants. Each variant flavor (solid/soft/outline) reads
|
|
|
// different slots from the same color block. Encoding 24
|
|
|
// composite scopes by hand for 6 tokens (3 on the root, 3 on the
|
|
|
// badge part) is 144 lines of boilerplate; this small helper
|
|
|
// generates them with the same intent.
|
|
|
//
|
|
|
// Helper consumed only here — explicit declarations would obscure
|
|
|
// the variant logic. Keep the helper local to the entry to avoid
|
|
|
// polluting the recipes module surface.
|
|
|
const COLORS = [
|
|
|
'primary',
|
|
|
'secondary',
|
|
|
'neutral',
|
|
|
'affirm',
|
|
|
'fulfill',
|
|
|
'risk',
|
|
|
'threat',
|
|
|
'loss'
|
|
|
] as const;
|
|
|
type AvatarColor = (typeof COLORS)[number];
|
|
|
type AvatarVariant = 'solid' | 'soft' | 'outline';
|
|
|
|
|
|
/** Build (variant × color) declarations for a slot getter. */
|
|
|
function matrix(
|
|
|
pick: (color: AvatarColor, variant: AvatarVariant) => string | undefined,
|
|
|
parts?: readonly string[]
|
|
|
): RecipeTokenMultiDeclaration {
|
|
|
const declarations: RecipeTokenDeclaration[] = [];
|
|
|
for (const variant of ['solid', 'soft', 'outline'] as const) {
|
|
|
for (const color of COLORS) {
|
|
|
const value = pick(color, variant);
|
|
|
if (value === undefined) continue;
|
|
|
declarations.push({
|
|
|
value,
|
|
|
scope: [`variant:${variant}`, `color:${color}`]
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
return parts ? { parts, declarations } : { declarations };
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
'size-xs': '24px',
|
|
|
'size-sm': '32px',
|
|
|
'size-md': '40px',
|
|
|
'size-lg': '48px',
|
|
|
'size-xl': '64px',
|
|
|
'size-xxl': '96px',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xxs-font-size)',
|
|
|
'font-size-sm': 'var(--size-xs-font-size)',
|
|
|
'font-size-md': 'var(--size-sm-font-size)',
|
|
|
'font-size-lg': 'var(--size-md-font-size)',
|
|
|
'font-size-xl': 'var(--size-lg-font-size)',
|
|
|
'font-size-xxl': 'var(--size-xl-font-size)',
|
|
|
'font-weight': 'var(--font-weight-medium)',
|
|
|
'radius-full': 'var(--radius-full)',
|
|
|
'radius-md': 'var(--radius-md)',
|
|
|
'radius-sm': 'var(--radius-sm)',
|
|
|
'radius-none': '0',
|
|
|
'border-width': 'var(--border-width-medium)',
|
|
|
'primary-solid-bg': 'var(--color-primary-solid)',
|
|
|
'primary-solid-fg': 'var(--color-primary-contrast)',
|
|
|
'primary-soft-bg': 'var(--color-primary-surface)',
|
|
|
'primary-soft-fg': 'var(--color-primary-text)',
|
|
|
'primary-outline-border': 'var(--color-primary-border)',
|
|
|
'primary-outline-fg': 'var(--color-primary-text)',
|
|
|
'secondary-solid-bg': 'var(--color-secondary-solid)',
|
|
|
'secondary-solid-fg': 'var(--color-secondary-contrast)',
|
|
|
'secondary-soft-bg': 'var(--color-secondary-surface)',
|
|
|
'secondary-soft-fg': 'var(--color-secondary-text)',
|
|
|
'secondary-outline-border': 'var(--color-secondary-border)',
|
|
|
'secondary-outline-fg': 'var(--color-secondary-text)',
|
|
|
'neutral-solid-bg': 'var(--color-neutral-solid)',
|
|
|
'neutral-solid-fg': 'var(--color-neutral-contrast)',
|
|
|
'neutral-soft-bg': 'var(--color-neutral-surface)',
|
|
|
'neutral-soft-fg': 'var(--color-neutral-text)',
|
|
|
'neutral-outline-border': 'var(--color-neutral-border)',
|
|
|
'neutral-outline-fg': 'var(--color-neutral-text)',
|
|
|
'affirm-solid-bg': 'var(--color-affirm-solid)',
|
|
|
'affirm-solid-fg': 'var(--color-affirm-contrast)',
|
|
|
'affirm-soft-bg': 'var(--color-affirm-surface)',
|
|
|
'affirm-soft-fg': 'var(--color-affirm-text)',
|
|
|
'affirm-outline-border': 'var(--color-affirm-border)',
|
|
|
'affirm-outline-fg': 'var(--color-affirm-text)',
|
|
|
'fulfill-solid-bg': 'var(--color-fulfill-solid)',
|
|
|
'fulfill-solid-fg': 'var(--color-fulfill-contrast)',
|
|
|
'fulfill-soft-bg': 'var(--color-fulfill-surface)',
|
|
|
'fulfill-soft-fg': 'var(--color-fulfill-text)',
|
|
|
'fulfill-outline-border': 'var(--color-fulfill-border)',
|
|
|
'fulfill-outline-fg': 'var(--color-fulfill-text)',
|
|
|
'risk-solid-bg': 'var(--color-risk-solid)',
|
|
|
'risk-solid-fg': 'var(--color-risk-contrast)',
|
|
|
'risk-soft-bg': 'var(--color-risk-surface)',
|
|
|
'risk-soft-fg': 'var(--color-risk-text)',
|
|
|
'risk-outline-border': 'var(--color-risk-border)',
|
|
|
'risk-outline-fg': 'var(--color-risk-text)',
|
|
|
'threat-solid-bg': 'var(--color-threat-solid)',
|
|
|
'threat-solid-fg': 'var(--color-threat-contrast)',
|
|
|
'threat-soft-bg': 'var(--color-threat-surface)',
|
|
|
'threat-soft-fg': 'var(--color-threat-text)',
|
|
|
'threat-outline-border': 'var(--color-threat-border)',
|
|
|
'threat-outline-fg': 'var(--color-threat-text)',
|
|
|
'loss-solid-bg': 'var(--color-loss-solid)',
|
|
|
'loss-solid-fg': 'var(--color-loss-contrast)',
|
|
|
'loss-soft-bg': 'var(--color-loss-surface)',
|
|
|
'loss-soft-fg': 'var(--color-loss-text)',
|
|
|
'loss-outline-border': 'var(--color-loss-border)',
|
|
|
'loss-outline-fg': 'var(--color-loss-text)',
|
|
|
// ── Ring (outline halo around the avatar) ────────────────────────
|
|
|
'ring-width-sm': '1.5px',
|
|
|
'ring-width-md': '2px',
|
|
|
'ring-width-lg': '3px',
|
|
|
// ── Custom colour ─────────────────────────────────────────────────
|
|
|
// The main fill routes through the shared `--color-custom` seed
|
|
|
// (`resolveComponentColor` + the palette forward), so only the ring
|
|
|
// keeps its own custom channel, set inline by the Svelte file.
|
|
|
'ring-color-custom': 'currentColor',
|
|
|
// ── Badge ─────────────────────────────────────────────────────────
|
|
|
'badge-border-width': 'var(--border-width-medium)',
|
|
|
'badge-color-custom': 'currentColor',
|
|
|
'badge-color-custom-contrast': 'white',
|
|
|
// ── AvatarGroup ──────────────────────────────────────────────────
|
|
|
'group-gap-xs': 'var(--space-1)',
|
|
|
'group-gap-sm': 'var(--space-2)',
|
|
|
'group-gap-md': 'var(--space-3)',
|
|
|
'group-gap-lg': 'var(--space-4)',
|
|
|
'group-overlap': '-0.6em',
|
|
|
'group-carve-width': 'var(--border-width-medium)',
|
|
|
'group-carve-color': 'var(--color-surface-default)',
|
|
|
'group-reverse-z-max': '20',
|
|
|
'group-max': '99',
|
|
|
'overflow-font-weight': 'var(--font-weight-medium)',
|
|
|
// ── TSC v2.2 — composite (variant × color) cascade ────────────────
|
|
|
// 8 colors × 3 variants = 24 composite scopes per slot, applied
|
|
|
// to the root for {_bg, _fg, _border} and to the badge part for
|
|
|
// {_badge-bg, _badge-fg, _badge-border} (matrix helper above
|
|
|
// generates the declarations[] from the slot pickers).
|
|
|
//
|
|
|
// Defaults at `host` match the legacy CSS base rule (avatar:
|
|
|
// neutral-soft; badge: affirm-solid). Outline is the only variant
|
|
|
// that touches `_border` / `_badge-border`; solid+soft keep the
|
|
|
// transparent / surface-default defaults.
|
|
|
// THM-2 (2026-07-12): the ROOT palette resolves via the shared
|
|
|
// per-instance layer. `_palette-{slot}` carries only a host fallback —
|
|
|
// the avatar always emits a canonical `data-color`, so the presence-
|
|
|
// guarded forward routes every role AND the 33 scales; the host is the
|
|
|
// out-of-vocab fallback. `_bg/_fg/_border` then pick the slot by
|
|
|
// VARIANT (was a variant×color matrix; the color dimension moved to the
|
|
|
// shared layer). The badge keeps its own matrix (independent color).
|
|
|
'_palette-solid': { value: 'var(--avatar-neutral-solid-bg)', scope: 'host' },
|
|
|
'_palette-surface': { value: 'var(--avatar-neutral-soft-bg)', scope: 'host' },
|
|
|
'_palette-contrast': { value: 'var(--avatar-neutral-solid-fg)', scope: 'host' },
|
|
|
'_palette-text': { value: 'var(--avatar-neutral-soft-fg)', scope: 'host' },
|
|
|
'_palette-border': { value: 'var(--avatar-neutral-outline-border)', scope: 'host' },
|
|
|
_bg: {
|
|
|
declarations: [
|
|
|
{ value: 'var(--_avatar-palette-surface)', scope: 'host' },
|
|
|
{ value: 'var(--_avatar-palette-solid)', scope: 'variant:solid' },
|
|
|
{ value: 'var(--_avatar-palette-surface)', scope: 'variant:soft' },
|
|
|
{ value: 'transparent', scope: 'variant:outline' }
|
|
|
]
|
|
|
},
|
|
|
_fg: {
|
|
|
declarations: [
|
|
|
{ value: 'var(--_avatar-palette-text)', scope: 'host' },
|
|
|
{ value: 'var(--_avatar-palette-contrast)', scope: 'variant:solid' },
|
|
|
{ value: 'var(--_avatar-palette-text)', scope: 'variant:soft' },
|
|
|
{ value: 'var(--_avatar-palette-text)', scope: 'variant:outline' }
|
|
|
]
|
|
|
},
|
|
|
_border: {
|
|
|
declarations: [
|
|
|
{ value: 'transparent', scope: 'host' },
|
|
|
{ value: 'var(--_avatar-palette-border)', scope: 'variant:outline' }
|
|
|
]
|
|
|
},
|
|
|
'_badge-bg': {
|
|
|
parts: ['badge'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--avatar-affirm-solid-bg)', scope: 'host' },
|
|
|
...matrix((color, variant) =>
|
|
|
variant === 'outline'
|
|
|
? 'var(--color-surface-default)'
|
|
|
: `var(--avatar-${color}-${variant}-bg)`
|
|
|
).declarations
|
|
|
]
|
|
|
},
|
|
|
'_badge-fg': {
|
|
|
parts: ['badge'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--avatar-affirm-solid-fg)', scope: 'host' },
|
|
|
...matrix((color, variant) => `var(--avatar-${color}-${variant}-fg)`).declarations
|
|
|
]
|
|
|
},
|
|
|
'_badge-border': {
|
|
|
parts: ['badge'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--color-surface-default)', scope: 'host' },
|
|
|
...matrix((color, variant) =>
|
|
|
variant === 'outline' ? `var(--avatar-${color}-outline-border)` : undefined
|
|
|
).declarations
|
|
|
]
|
|
|
}
|
|
|
};
|
|
|
})(),
|
|
|
aura: {
|
|
|
// ── La costura (2026-08-22) ──────────────────────────────────────────
|
|
|
// El texto de estado y su detalle bajan UN paso del bundle por talla, y las
|
|
|
// tres coordenadas viajan juntas (tamaño, interlineado, tracking): doce
|
|
|
// declaraciones, doce valores distintos, doce claves. El valor sigue siendo
|
|
|
// del bundle; el knob pasa a ser del componente.
|
|
|
'status-font-size-md': 'var(--size-sm-font-size)',
|
|
|
'status-line-height-md': 'var(--size-sm-font-line-height)',
|
|
|
'status-letter-spacing-md': 'var(--size-sm-font-letter-spacing)',
|
|
|
'status-font-size-lg': 'var(--size-md-font-size)',
|
|
|
'status-line-height-lg': 'var(--size-md-font-line-height)',
|
|
|
'status-letter-spacing-lg': 'var(--size-md-font-letter-spacing)',
|
|
|
'status-detail-font-size-md': 'var(--size-xs-font-size)',
|
|
|
'status-detail-line-height-md': 'var(--size-xs-font-line-height)',
|
|
|
'status-detail-letter-spacing-md': 'var(--size-xs-font-letter-spacing)',
|
|
|
'status-detail-font-size-lg': 'var(--size-sm-font-size)',
|
|
|
'status-detail-line-height-lg': 'var(--size-sm-font-line-height)',
|
|
|
'status-detail-letter-spacing-lg': 'var(--size-sm-font-letter-spacing)',
|
|
|
// El reposo no desaparece: se retira a media opacidad.
|
|
|
'idle-opacity': 'var(--opacity-50)',
|
|
|
// ── Orb geometry per size ────────────────────────────────────────
|
|
|
// Derived from the AVATAR primitive (the canonical round
|
|
|
// presence/identity disc — Aura is the agent's presence): one
|
|
|
// source, themes that retune avatars retune the orb with them.
|
|
|
'orb-size-sm': 'var(--avatar-size-sm)',
|
|
|
'orb-size-md': 'var(--avatar-size-md)',
|
|
|
'orb-size-lg': 'var(--avatar-size-lg)',
|
|
|
'orb-radius': 'var(--radius-full)',
|
|
|
gap: 'var(--space-2)',
|
|
|
// ── State tint (role slots — themes re-tint here). The mapping is
|
|
|
// canon-aligned: reviewing=risk («revisa esto»), escalated=threat
|
|
|
// («atiende ahora»), acting/offered/planned=primary, idle=neutral.
|
|
|
'orb-color-idle': 'var(--color-neutral-solid)',
|
|
|
'orb-color-offered': 'var(--color-primary-solid)',
|
|
|
'orb-color-planned': 'var(--color-primary-solid)',
|
|
|
'orb-color-reviewing': 'var(--color-risk-solid)',
|
|
|
'orb-color-acting': 'var(--color-primary-solid)',
|
|
|
'orb-color-escalated': 'var(--color-threat-solid)',
|
|
|
'orb-halo-alpha': 'var(--primitive-neutral-a5)',
|
|
|
// ── Custody ring ─────────────────────────────────────────────────
|
|
|
// The ring is the orb's FRAME (it wraps the Indicator), so its box is
|
|
|
// a multiple of the orb size — one axis scales both. The stroke is
|
|
|
// where the state becomes SHAPE; the tint comes from the orb's role
|
|
|
// slot above (one source, never a second colour to keep in sync).
|
|
|
'ring-scale': '1.7',
|
|
|
'ring-track-fg': 'var(--color-border-default)',
|
|
|
'ring-track-width': 'var(--border-width-thin)',
|
|
|
'ring-arc-width': 'var(--border-width-medium)',
|
|
|
'ring-halo-width': 'var(--border-width-thin)',
|
|
|
'ring-halo-opacity': 'var(--opacity-50)',
|
|
|
// The Status is two lines: the state carries the weight (it is the
|
|
|
// answer to «¿quién actúa?»), the detail recedes.
|
|
|
'status-fg': 'var(--color-content-primary)',
|
|
|
'status-weight': 'var(--font-weight-medium)',
|
|
|
'status-gap': 'var(--space-0-5)',
|
|
|
'status-detail-fg': 'var(--color-content-secondary)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)'
|
|
|
},
|
|
|
badge: {
|
|
|
// ── Padding scale ────────────────────────────────────────────────
|
|
|
'padding-inline-xs': 'var(--space-1-5)',
|
|
|
'padding-inline-sm': 'var(--space-2)',
|
|
|
'padding-inline-md': 'var(--space-2-5)',
|
|
|
'padding-inline-lg': 'var(--space-3)',
|
|
|
// NO block padding: the chip's height is a CONTROL height
|
|
|
// (`--control-height-{size}` via `min-block-size`), not a padded line of
|
|
|
// text. Keeping these as tokens nobody reads is what the orphan guard
|
|
|
// exists to catch — and having two ways to set the same height is how `md`
|
|
|
// ended up meaning 30px in a chip and 36px in every control.
|
|
|
// ── Gap between dot / icon / label / remove ───────────────────────
|
|
|
'gap-xs': 'var(--space-1)',
|
|
|
'gap-sm': 'var(--space-1)',
|
|
|
'gap-md': 'var(--space-1-5)',
|
|
|
'gap-lg': 'var(--space-2)',
|
|
|
// ── Typography ────────────────────────────────────────────────────
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'font-weight': 'var(--font-weight-medium)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'letter-spacing': '0.01em',
|
|
|
// ── Borders + radii ───────────────────────────────────────────────
|
|
|
'border-width': 'var(--border-width)',
|
|
|
'radius-sm': 'var(--radius-sm)',
|
|
|
'radius-md': 'var(--radius-md)',
|
|
|
'radius-lg': 'var(--radius-lg)',
|
|
|
'radius-full': 'var(--radius-full)',
|
|
|
// ── Inner slots ───────────────────────────────────────────────────
|
|
|
// Use em so internal parts scale with the badge's resolved font-size.
|
|
|
// The chip's HEIGHT rides the control scale — the badge sizes from the
|
|
|
// control token so `md` means one thing whether or not the chip composes a
|
|
|
// real control (the removable ✕ is an IconButton; measured, the same `md`
|
|
|
// badge came out 30px without it and 46px with it). The scale was a
|
|
|
// private reading `--control-height-*` straight, so no theme could move
|
|
|
// the chip without moving every control in the app. Through the contract
|
|
|
// the value goes to the canonical BUNDLE coordinate, not the raw
|
|
|
// primitive: `recipe-css-contract` caught the verbatim copy the moment it
|
|
|
// entered (theming/reference §5) — the same trap that bit `code-block`.
|
|
|
//
|
|
|
// The NAME is `min-height`, not `min-block-size`: the property is logical
|
|
|
// but the vocabulary's slot for it is `height` (recipe-contract §1, and
|
|
|
// the census maps `min-block-size → height` itself). Twenty-four
|
|
|
// components size by `…-height-{k}`; the root-level twin with this exact
|
|
|
// shape — per size, over `--size-{k}-control-height` — is
|
|
|
// `proof-of-human.min-height-{k}`. Shipped as `min-block-size-*` for a
|
|
|
// few hours on 2026-08-23 and renamed the same day.
|
|
|
'min-height-xs': 'var(--size-xs-control-height)',
|
|
|
'min-height-sm': 'var(--size-sm-control-height)',
|
|
|
'min-height-md': 'var(--size-md-control-height)',
|
|
|
'min-height-lg': 'var(--size-lg-control-height)',
|
|
|
'min-height': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--badge-min-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--badge-min-height-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--badge-min-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--badge-min-height-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
// The dot's shape, the pair `dot-size` was missing. Round is the default,
|
|
|
// not the definition: `knob.indicator-radius` is `--radius-sm`, so the
|
|
|
// catalogue already treats a marker's radius as a knob.
|
|
|
'dot-radius': 'var(--radius-full)',
|
|
|
'dot-size': '0.5em',
|
|
|
'icon-size': '1em',
|
|
|
'remove-size': '1.25em',
|
|
|
'remove-margin-start': '0.125em',
|
|
|
// ── Color palette (mirrors the canonical 8-color matrix) ──────────
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-text': 'var(--color-primary-text)',
|
|
|
'primary-contrast': 'var(--color-primary-contrast)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-text': 'var(--color-secondary-text)',
|
|
|
'secondary-contrast': 'var(--color-secondary-contrast)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-text': 'var(--color-neutral-text)',
|
|
|
'neutral-contrast': 'var(--color-neutral-contrast)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-text': 'var(--color-affirm-text)',
|
|
|
'affirm-contrast': 'var(--color-affirm-contrast)',
|
|
|
'fulfill-track': 'var(--color-fulfill-track)',
|
|
|
'fulfill-border': 'var(--color-fulfill-border)',
|
|
|
'fulfill-solid': 'var(--color-fulfill-solid)',
|
|
|
'fulfill-text': 'var(--color-fulfill-text)',
|
|
|
'fulfill-contrast': 'var(--color-fulfill-contrast)',
|
|
|
'risk-solid': 'var(--color-risk-solid)',
|
|
|
'risk-text': 'var(--color-risk-text)',
|
|
|
'risk-contrast': 'var(--color-risk-contrast)',
|
|
|
'threat-solid': 'var(--color-threat-solid)',
|
|
|
'threat-text': 'var(--color-threat-text)',
|
|
|
'threat-contrast': 'var(--color-threat-contrast)',
|
|
|
'loss-track': 'var(--color-loss-track)',
|
|
|
'loss-border': 'var(--color-loss-border)',
|
|
|
'loss-solid': 'var(--color-loss-solid)',
|
|
|
'loss-text': 'var(--color-loss-text)',
|
|
|
'loss-contrast': 'var(--color-loss-contrast)',
|
|
|
// Restored after accidental sed-removal during switch migration.
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-border': 'var(--color-primary-border)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-border': 'var(--color-secondary-border)',
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-border': 'var(--color-neutral-border)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-border': 'var(--color-affirm-border)',
|
|
|
'risk-track': 'var(--color-risk-track)',
|
|
|
'risk-border': 'var(--color-risk-border)',
|
|
|
'threat-track': 'var(--color-threat-track)',
|
|
|
'threat-border': 'var(--color-threat-border)',
|
|
|
// Private palette (TSC v2.1 · canonical `_palette-*` slots so the THM-2
|
|
|
// shared-layer forward routes color="teal" + the 33 scales; renamed from
|
|
|
// the bare `_{slot}` prefix 2026-07-13). 8 colors, 5 slots.
|
|
|
'_palette-track': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--badge-primary-track)', scope: 'host' },
|
|
|
{ value: 'var(--badge-secondary-track)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--badge-neutral-track)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--badge-affirm-track)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--badge-fulfill-track)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--badge-risk-track)', scope: 'color:risk' },
|
|
|
{ value: 'var(--badge-threat-track)', scope: 'color:threat' },
|
|
|
{ value: 'var(--badge-loss-track)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--badge-primary-border)', scope: 'host' },
|
|
|
{ value: 'var(--badge-secondary-border)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--badge-neutral-border)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--badge-affirm-border)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--badge-fulfill-border)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--badge-risk-border)', scope: 'color:risk' },
|
|
|
{ value: 'var(--badge-threat-border)', scope: 'color:threat' },
|
|
|
{ value: 'var(--badge-loss-border)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--badge-primary-solid)', scope: 'host' },
|
|
|
{ value: 'var(--badge-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--badge-neutral-solid)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--badge-affirm-solid)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--badge-fulfill-solid)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--badge-risk-solid)', scope: 'color:risk' },
|
|
|
{ value: 'var(--badge-threat-solid)', scope: 'color:threat' },
|
|
|
{ value: 'var(--badge-loss-solid)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-text': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--badge-primary-text)', scope: 'host' },
|
|
|
{ value: 'var(--badge-secondary-text)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--badge-neutral-text)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--badge-affirm-text)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--badge-fulfill-text)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--badge-risk-text)', scope: 'color:risk' },
|
|
|
{ value: 'var(--badge-threat-text)', scope: 'color:threat' },
|
|
|
{ value: 'var(--badge-loss-text)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-contrast': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--badge-primary-contrast)', scope: 'host' },
|
|
|
{ value: 'var(--badge-secondary-contrast)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--badge-neutral-contrast)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--badge-affirm-contrast)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--badge-fulfill-contrast)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--badge-risk-contrast)', scope: 'color:risk' },
|
|
|
{ value: 'var(--badge-threat-contrast)', scope: 'color:threat' },
|
|
|
{ value: 'var(--badge-loss-contrast)', scope: 'color:loss' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
barcode: {
|
|
|
// FIXED-TONE by construction: a barcode cannot invert in dark mode and
|
|
|
// stay scannable, so the default ink/quiet-zone are theme-independent.
|
|
|
// Theming is opt-in through the `color` / `background` props.
|
|
|
fg: '#18181b' /* literal: barcode ink must stay dark for any theme */,
|
|
|
bg: '#ffffff' /* literal: the quiet zone must stay true white */,
|
|
|
radius: 'var(--radius-md)',
|
|
|
// Human-readable interpretation. The standard's HRI face is OCR-B, a
|
|
|
// fixed-width design: the digits sit digit-under-digit over their module
|
|
|
// cells, so a proportional face misaligns them. The system's mono anchor
|
|
|
// is the closest honest equivalent. The size is geometry (an SVG
|
|
|
// attribute proportional to the bar height); family and ink live here.
|
|
|
'text-font-family': 'var(--style-code-font-family)',
|
|
|
'text-letter-spacing': '0',
|
|
|
// Scoped to the host so a `color` override on the instance reaches it —
|
|
|
// at `:root` the reference would freeze at the default (TSC).
|
|
|
'text-fg': {
|
|
|
value: 'var(--barcode-fg)',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'invalid-border': 'var(--color-border-default)'
|
|
|
},
|
|
|
breadcrumb: {
|
|
|
'gap-xs': 'var(--space-0-5)',
|
|
|
'gap-sm': 'var(--space-1)',
|
|
|
'gap-md': 'var(--space-1-5)',
|
|
|
'gap-lg': 'var(--space-2)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-secondary)',
|
|
|
'link-radius': 'var(--radius-sm)',
|
|
|
'link-fg': 'var(--color-content-secondary)',
|
|
|
'hover-link-fg': 'var(--color-content-primary)',
|
|
|
'current-link-fg': 'var(--color-content-primary)',
|
|
|
'link-font-weight': 'var(--font-weight-medium)',
|
|
|
'current-link-font-weight': 'var(--font-weight-semibold)',
|
|
|
'separator-fg': 'var(--color-content-muted)',
|
|
|
'ellipsis-size': 'var(--size-xs-control-height)',
|
|
|
'ellipsis-radius': 'var(--radius-md)',
|
|
|
'ellipsis-border-width': 'var(--border-width)',
|
|
|
'ellipsis-border': 'transparent',
|
|
|
'hover-ellipsis-border': 'var(--color-border-subtle)',
|
|
|
'ellipsis-bg': 'transparent',
|
|
|
'ellipsis-fg': 'var(--color-content-muted)',
|
|
|
'hover-ellipsis-fg': 'var(--color-content-primary)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)'
|
|
|
},
|
|
|
card: {
|
|
|
// ── Padding scale ────────────────────────────────────────────────
|
|
|
'padding-inline-xs': 'var(--space-3)',
|
|
|
'padding-inline-sm': 'var(--space-4)',
|
|
|
'padding-inline-md': 'var(--space-5)',
|
|
|
'padding-inline-lg': 'var(--space-6)',
|
|
|
'padding-inline-xl': 'var(--space-8)',
|
|
|
'padding-block-xs': 'var(--space-3)',
|
|
|
'padding-block-sm': 'var(--space-4)',
|
|
|
'padding-block-md': 'var(--space-5)',
|
|
|
'padding-block-lg': 'var(--space-6)',
|
|
|
'padding-block-xl': 'var(--space-8)',
|
|
|
// ── Gap between header / body / footer slots ─────────────────────
|
|
|
'gap-xs': 'var(--space-2)',
|
|
|
'gap-sm': 'var(--space-3)',
|
|
|
'gap-md': 'var(--space-4)',
|
|
|
'gap-lg': 'var(--space-5)',
|
|
|
'gap-xl': 'var(--space-6)',
|
|
|
// ── Typography ────────────────────────────────────────────────────
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'title-font-family': 'var(--font-family-display)',
|
|
|
'title-font-size-xs': 'var(--size-sm-font-size)',
|
|
|
'title-font-size-sm': 'var(--size-md-font-size)',
|
|
|
'title-font-size-md': 'var(--size-lg-font-size)',
|
|
|
'title-font-size-lg': 'var(--size-xl-font-size)',
|
|
|
'title-font-size-xl': 'var(--size-xxl-font-size)',
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'title-line-height': 'var(--leading-heading)',
|
|
|
'title-letter-spacing': 'var(--tracking-tight)',
|
|
|
'description-font-family': 'var(--style-label-font-family)',
|
|
|
'description-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'description-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'description-font-size-md': 'var(--size-sm-font-size)',
|
|
|
'description-font-size-lg': 'var(--size-md-font-size)',
|
|
|
'description-font-size-xl': 'var(--size-md-font-size)',
|
|
|
'description-line-height': 'var(--leading-prose)',
|
|
|
'description-fg': 'var(--color-content-secondary)',
|
|
|
'body-font-size-xs': 'var(--size-sm-font-size)',
|
|
|
'body-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'body-font-size-md': 'var(--size-md-font-size)',
|
|
|
'body-font-size-lg': 'var(--size-md-font-size)',
|
|
|
'body-font-size-xl': 'var(--size-lg-font-size)',
|
|
|
'body-line-height': 'var(--leading-prose)',
|
|
|
// ── Borders + radii ───────────────────────────────────────────────
|
|
|
'border-width': 'var(--border-width)',
|
|
|
'radius-sm': 'var(--radius-md)',
|
|
|
'radius-md': 'var(--radius-lg)',
|
|
|
'radius-lg': 'var(--radius-xl)',
|
|
|
// ── Inner slot spacing ────────────────────────────────────────────
|
|
|
'header-gap': 'var(--space-1-5)',
|
|
|
'footer-gap': 'var(--space-3)',
|
|
|
'footer-padding-start': 'var(--space-3)',
|
|
|
'footer-border-fg': 'var(--color-border-subtle)',
|
|
|
// ── Animations ────────────────────────────────────────────────────
|
|
|
'emerge-duration': 'var(--duration-slow)',
|
|
|
'emerge-ease': 'var(--ease-out)',
|
|
|
'emerge-distance': '8px',
|
|
|
'hover-lift': '-2px',
|
|
|
'hover-shadow': 'var(--depth-overlay-shadow), var(--depth-overlay-halo)',
|
|
|
'press-scale': 'var(--press-scale)',
|
|
|
'press-duration': 'var(--press-duration)',
|
|
|
'selected-ring-width': 'var(--border-width-medium)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
// Custom colour routes through the shared `--color-custom` seed
|
|
|
// (`resolveComponentColor` + the palette forward) — no bespoke token.
|
|
|
// ── Color palette (canonical 8-color × 5 slots) ──────────────────
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-text': 'var(--color-primary-text)',
|
|
|
'primary-contrast': 'var(--color-primary-contrast)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-text': 'var(--color-secondary-text)',
|
|
|
'secondary-contrast': 'var(--color-secondary-contrast)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-text': 'var(--color-neutral-text)',
|
|
|
'neutral-contrast': 'var(--color-neutral-contrast)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-text': 'var(--color-affirm-text)',
|
|
|
'affirm-contrast': 'var(--color-affirm-contrast)',
|
|
|
'fulfill-track': 'var(--color-fulfill-track)',
|
|
|
'fulfill-border': 'var(--color-fulfill-border)',
|
|
|
'fulfill-solid': 'var(--color-fulfill-solid)',
|
|
|
'fulfill-text': 'var(--color-fulfill-text)',
|
|
|
'fulfill-contrast': 'var(--color-fulfill-contrast)',
|
|
|
'risk-solid': 'var(--color-risk-solid)',
|
|
|
'risk-text': 'var(--color-risk-text)',
|
|
|
'risk-contrast': 'var(--color-risk-contrast)',
|
|
|
'threat-solid': 'var(--color-threat-solid)',
|
|
|
'threat-text': 'var(--color-threat-text)',
|
|
|
'threat-contrast': 'var(--color-threat-contrast)',
|
|
|
'loss-track': 'var(--color-loss-track)',
|
|
|
'loss-border': 'var(--color-loss-border)',
|
|
|
'loss-solid': 'var(--color-loss-solid)',
|
|
|
'loss-text': 'var(--color-loss-text)',
|
|
|
'loss-contrast': 'var(--color-loss-contrast)',
|
|
|
// Restored after accidental sed-removal during switch migration.
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-border': 'var(--color-primary-border)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-border': 'var(--color-secondary-border)',
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-border': 'var(--color-neutral-border)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-border': 'var(--color-affirm-border)',
|
|
|
'risk-track': 'var(--color-risk-track)',
|
|
|
'risk-border': 'var(--color-risk-border)',
|
|
|
'threat-track': 'var(--color-threat-track)',
|
|
|
'threat-border': 'var(--color-threat-border)',
|
|
|
// Private palette (TSC v2.1 · canonical `_palette-*` slots so the THM-2
|
|
|
// shared-layer forward routes color="teal" + the 33 scales; renamed from
|
|
|
// the bare `_{slot}` prefix 2026-07-13). 8 colors, 5 slots. Default = neutral.
|
|
|
'_palette-track': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--card-neutral-track)', scope: 'host' },
|
|
|
{ value: 'var(--card-primary-track)', scope: 'color:primary' },
|
|
|
{ value: 'var(--card-secondary-track)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--card-affirm-track)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--card-fulfill-track)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--card-risk-track)', scope: 'color:risk' },
|
|
|
{ value: 'var(--card-threat-track)', scope: 'color:threat' },
|
|
|
{ value: 'var(--card-loss-track)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--card-neutral-border)', scope: 'host' },
|
|
|
{ value: 'var(--card-primary-border)', scope: 'color:primary' },
|
|
|
{ value: 'var(--card-secondary-border)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--card-affirm-border)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--card-fulfill-border)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--card-risk-border)', scope: 'color:risk' },
|
|
|
{ value: 'var(--card-threat-border)', scope: 'color:threat' },
|
|
|
{ value: 'var(--card-loss-border)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--card-neutral-solid)', scope: 'host' },
|
|
|
{ value: 'var(--card-primary-solid)', scope: 'color:primary' },
|
|
|
{ value: 'var(--card-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--card-affirm-solid)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--card-fulfill-solid)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--card-risk-solid)', scope: 'color:risk' },
|
|
|
{ value: 'var(--card-threat-solid)', scope: 'color:threat' },
|
|
|
{ value: 'var(--card-loss-solid)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-text': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--card-neutral-text)', scope: 'host' },
|
|
|
{ value: 'var(--card-primary-text)', scope: 'color:primary' },
|
|
|
{ value: 'var(--card-secondary-text)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--card-affirm-text)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--card-fulfill-text)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--card-risk-text)', scope: 'color:risk' },
|
|
|
{ value: 'var(--card-threat-text)', scope: 'color:threat' },
|
|
|
{ value: 'var(--card-loss-text)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-contrast': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--card-neutral-contrast)', scope: 'host' },
|
|
|
{ value: 'var(--card-primary-contrast)', scope: 'color:primary' },
|
|
|
{ value: 'var(--card-secondary-contrast)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--card-affirm-contrast)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--card-fulfill-contrast)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--card-risk-contrast)', scope: 'color:risk' },
|
|
|
{ value: 'var(--card-threat-contrast)', scope: 'color:threat' },
|
|
|
{ value: 'var(--card-loss-contrast)', scope: 'color:loss' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
'chat-composer': {
|
|
|
// ── Shell — ONE field-like capsule: attach · input · actions · send
|
|
|
// (WhatsApp/Telegram input pill; stays capsule-sided when autosize
|
|
|
// grows it into a rounded rectangle) ──────────────────────────────
|
|
|
gap: 'var(--space-1-5)',
|
|
|
bg: 'var(--color-surface-default)',
|
|
|
border: 'var(--color-border-default)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
'focus-border': 'var(--focus-ring-color)',
|
|
|
radius: 'var(--radius-xl)',
|
|
|
'padding-inline': 'var(--space-2)',
|
|
|
'padding-block': 'var(--space-1)',
|
|
|
// ── Input (borderless textarea inside the shell) ──────────────────
|
|
|
// md (16px): matches the message body baseline AND stays at/above the
|
|
|
// 16px floor that stops iOS Safari from auto-zooming the page on focus.
|
|
|
'input-font-size': 'var(--size-md-font-size)',
|
|
|
'input-line-height': 'var(--leading-normal)',
|
|
|
'input-fg': 'var(--color-content-primary)',
|
|
|
'input-placeholder-fg': 'var(--color-content-muted)',
|
|
|
'input-padding-inline': 'var(--space-1)',
|
|
|
'input-padding-block': 'var(--space-1-5)',
|
|
|
// ── Context bar (reply/edit, above the input row) ─────────────────
|
|
|
'context-bg': 'var(--color-neutral-track)',
|
|
|
'context-accent': 'var(--color-primary-solid)',
|
|
|
'context-accent-size': 'var(--border-width-thick)',
|
|
|
'context-font-size': 'var(--size-sm-font-size)',
|
|
|
'context-fg': 'var(--color-content-secondary)',
|
|
|
'context-radius': 'var(--radius-sm)',
|
|
|
'context-padding-inline': 'var(--space-2)',
|
|
|
'context-padding-block': 'var(--space-1)',
|
|
|
'context-gap': 'var(--space-2)',
|
|
|
// ── Attachment tray ───────────────────────────────────────────────
|
|
|
'attachments-gap': 'var(--space-2)',
|
|
|
// ── Send — circular primary action ────────────────────────────────
|
|
|
'send-size': 'var(--size-md-control-height)',
|
|
|
'send-bg': 'var(--color-primary-solid)',
|
|
|
'send-fg': 'var(--color-primary-contrast)',
|
|
|
'send-radius': 'var(--radius-full)',
|
|
|
// ── Motion ────────────────────────────────────────────────────────
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
'chat-list': {
|
|
|
// ── Shell (a vertical list of conversation rows) ──────────────────
|
|
|
bg: 'var(--color-surface-default)',
|
|
|
'padding-inline': 'var(--space-1)',
|
|
|
'padding-block': 'var(--space-1)',
|
|
|
// ── Item (a conversation row; hover/selected come from archetype
|
|
|
// 'item' via the composed Listbox — not re-declared here) ──────────
|
|
|
'item-gap': 'var(--space-2-5)',
|
|
|
'item-padding-inline': 'var(--space-2)',
|
|
|
'item-padding-block': 'var(--space-2)',
|
|
|
'item-radius': 'var(--radius-md)',
|
|
|
'item-row-gap': 'var(--space-0-5)',
|
|
|
// ── Title (conversation name) ─────────────────────────────────────
|
|
|
'title-font-size': 'var(--size-sm-font-size)',
|
|
|
'title-font-weight': 'var(--font-weight-medium)',
|
|
|
'title-fg': 'var(--color-content-primary)',
|
|
|
'title-unread-weight': 'var(--font-weight-bold)',
|
|
|
// ── Preview (last message, truncated) ─────────────────────────────
|
|
|
'preview-font-size': 'var(--size-sm-font-size)',
|
|
|
'preview-fg': 'var(--color-content-muted)',
|
|
|
'preview-unread-fg': 'var(--color-content-secondary)',
|
|
|
// ── Meta (time + unread badge column) ─────────────────────────────
|
|
|
'meta-gap': 'var(--space-1)',
|
|
|
'meta-font-size': 'var(--size-xs-font-size)',
|
|
|
'meta-fg': 'var(--color-content-muted)'
|
|
|
},
|
|
|
'chat-log': {
|
|
|
// ── Shell — a muted canvas so surface bubbles float (reference:
|
|
|
// WhatsApp's tinted wallpaper / Telegram's soft backdrop) ─────────
|
|
|
bg: 'var(--color-surface-muted)',
|
|
|
'padding-inline': 'var(--space-3)',
|
|
|
'padding-block': 'var(--space-2)',
|
|
|
// ── Day separator (sticky pill) ──────────────────────────────────
|
|
|
'separator-gap': 'var(--space-3)',
|
|
|
'separator-font-size': 'var(--size-xs-font-size)',
|
|
|
'separator-fg': 'var(--color-content-muted)',
|
|
|
'separator-bg': 'var(--color-neutral-track)',
|
|
|
'separator-padding-inline': 'var(--space-2-5)',
|
|
|
'separator-padding-block': 'var(--space-0-5)',
|
|
|
'separator-radius': 'var(--radius-full)',
|
|
|
// ── Unread divider ───────────────────────────────────────────────
|
|
|
'divider-fg': 'var(--color-primary-solid)',
|
|
|
'divider-line': 'var(--color-primary-border)',
|
|
|
'divider-font-size': 'var(--size-xs-font-size)',
|
|
|
'divider-gap': 'var(--space-2)',
|
|
|
// ── System event row ─────────────────────────────────────────────
|
|
|
'event-font-size': 'var(--size-sm-font-size)',
|
|
|
'event-fg': 'var(--color-content-muted)',
|
|
|
'event-padding-block': 'var(--space-1)',
|
|
|
// ── Floating pill ("N new messages") ─────────────────────────────
|
|
|
'pill-bg': 'var(--color-primary-solid)',
|
|
|
'pill-fg': 'var(--color-primary-contrast)',
|
|
|
'pill-font-size': 'var(--size-sm-font-size)',
|
|
|
'pill-padding-inline': 'var(--space-3)',
|
|
|
'pill-height': 'var(--size-sm-control-height)',
|
|
|
'pill-radius': 'var(--radius-full)',
|
|
|
'pill-shadow': 'var(--shadow-3)',
|
|
|
'pill-offset': 'var(--space-3)',
|
|
|
// ── To-latest button ─────────────────────────────────────────────
|
|
|
'to-latest-size': 'var(--size-md-control-height)',
|
|
|
'to-latest-bg': 'var(--color-neutral-element)',
|
|
|
'to-latest-fg': 'var(--color-content-primary)',
|
|
|
'to-latest-border': 'var(--color-neutral-border)',
|
|
|
'to-latest-shadow': 'var(--shadow-2)',
|
|
|
'to-latest-offset': 'var(--space-3)'
|
|
|
},
|
|
|
'chat-message': {
|
|
|
// ── Row layout ────────────────────────────────────────────────────
|
|
|
gap: 'var(--space-2)',
|
|
|
'row-gap-run': 'var(--space-0-5)',
|
|
|
'row-gap-group': 'var(--space-3)',
|
|
|
// Avatar column reserve — follows the composed Avatar's md diameter.
|
|
|
'gutter-inline-size': 'var(--avatar-size-md, var(--space-8))',
|
|
|
// ── Bubble ────────────────────────────────────────────────────────
|
|
|
'bubble-padding-inline-sm': 'var(--space-2-5)',
|
|
|
'bubble-padding-inline-md': 'var(--space-3)',
|
|
|
'bubble-padding-inline-lg': 'var(--space-4)',
|
|
|
'bubble-padding-block-sm': 'var(--space-1-5)',
|
|
|
'bubble-padding-block-md': 'var(--space-2)',
|
|
|
'bubble-padding-block-lg': 'var(--space-2-5)',
|
|
|
'bubble-radius': 'var(--radius-lg)',
|
|
|
// Flattened corners between messages of one run.
|
|
|
'bubble-radius-run': 'var(--radius-sm)',
|
|
|
// The "tail" corner — the bottom corner facing the margin on the
|
|
|
// LAST bubble of a run (the WhatsApp/Telegram anchor cue).
|
|
|
'bubble-tail-radius': 'var(--radius-sm)',
|
|
|
'bubble-max-inline-size': 'min(var(--measure-narrow), 72%)',
|
|
|
// Canonical size contract: `md` IS the system md (16px). Density in
|
|
|
// chat comes from line-height + padding, NOT from down-shifting the
|
|
|
// scale (that silently makes size="md" render sm, a contract break).
|
|
|
'bubble-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'bubble-font-size-md': 'var(--size-md-font-size)',
|
|
|
'bubble-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'bubble-line-height': 'var(--leading-normal)',
|
|
|
// Reference palette (WhatsApp/Telegram): OWN messages are a SOFT
|
|
|
// role tint with normal text — never the saturated solid — and PEER
|
|
|
// messages are plain surface cards with a hairline shadow, floating
|
|
|
// over the log's muted canvas.
|
|
|
'bubble-bg-in': 'var(--color-surface-default)',
|
|
|
'bubble-fg-in': 'var(--color-content-primary)',
|
|
|
'bubble-bg-out': 'var(--color-primary-element)',
|
|
|
'bubble-fg-out': 'var(--color-content-primary)',
|
|
|
'bubble-shadow': 'var(--shadow-1)',
|
|
|
// ── Header (author name, first of a run — accent-colored) ────────
|
|
|
'header-gap': 'var(--space-2)',
|
|
|
'header-font-size': 'var(--size-sm-font-size)',
|
|
|
'header-font-weight': 'var(--font-weight-medium)',
|
|
|
'header-fg': 'var(--color-primary-text)',
|
|
|
// ── Meta (time + status) ─────────────────────────────────────────
|
|
|
'meta-gap': 'var(--space-1)',
|
|
|
'meta-font-size': 'var(--size-xs-font-size)',
|
|
|
'meta-fg': 'var(--color-content-muted)',
|
|
|
// ── Delivery status indicator ────────────────────────────────────
|
|
|
'status-size': '1em',
|
|
|
'status-fg': 'var(--color-content-muted)',
|
|
|
'read-status-fg': 'var(--color-primary-solid)',
|
|
|
'failed-status-fg': 'var(--color-threat-solid)',
|
|
|
// ── Read receipts (avatar row — decorative) ──────────────────────
|
|
|
'read-by-gap': 'var(--space-1)',
|
|
|
'read-by-font-size': 'var(--size-xs-font-size)',
|
|
|
'read-by-fg': 'var(--color-content-muted)',
|
|
|
'read-by-margin': 'var(--space-0-5)',
|
|
|
// ── Jump-to-message highlight (row flash, fades via transition) ───
|
|
|
// A clearly perceptible soft tint (a2 surface is near-invisible over
|
|
|
// the muted log canvas) — same color-mix idiom as field segment-active.
|
|
|
'emphasize-bg': 'color-mix(in srgb, var(--color-primary-solid) 14%, transparent)',
|
|
|
'emphasize-radius': 'var(--radius-md)',
|
|
|
'emphasize-duration': 'var(--duration-moderate)',
|
|
|
// ── Reply (quoted preview) ───────────────────────────────────────
|
|
|
'reply-padding-inline': 'var(--space-2)',
|
|
|
'reply-padding-block': 'var(--space-1)',
|
|
|
'reply-accent-size': 'var(--space-0-5)',
|
|
|
'reply-radius': 'var(--radius-sm)',
|
|
|
'reply-bg': 'var(--color-neutral-track)',
|
|
|
'reply-accent': 'var(--color-primary-solid)',
|
|
|
'reply-font-size': 'var(--size-xs-font-size)',
|
|
|
// ── Reaction chips — floating pills lapping the bubble edge ──────
|
|
|
'reaction-gap': 'var(--space-1)',
|
|
|
'reaction-height': 'var(--size-xs-control-height)',
|
|
|
'reaction-padding-inline': 'var(--space-2)',
|
|
|
'reaction-radius': 'var(--radius-full)',
|
|
|
'reaction-font-size': 'var(--size-xs-font-size)',
|
|
|
'reaction-bg': 'var(--color-surface-default)',
|
|
|
'reaction-border': 'var(--color-neutral-separator)',
|
|
|
'reaction-shadow': 'var(--shadow-1)',
|
|
|
'reaction-overlap': 'var(--space-1)',
|
|
|
'reaction-pressed-bg': 'var(--color-primary-surface)',
|
|
|
'reaction-pressed-border': 'var(--color-primary-border)',
|
|
|
'reaction-pressed-fg': 'var(--color-primary-text)',
|
|
|
// ── Quick-reaction panel (the tapback bar: big emoji, round cells,
|
|
|
// scale-on-hover — WhatsApp/Telegram/Slack) ──────────────────────
|
|
|
'quick-react-gap': 'var(--space-0-5)',
|
|
|
'quick-react-pad': 'var(--space-1)',
|
|
|
'quick-react-size': 'var(--size-lg-control-height)',
|
|
|
'quick-react-emoji-size': 'var(--size-xl-font-size)',
|
|
|
'quick-react-hover-bg': 'var(--color-neutral-hover)',
|
|
|
'quick-react-hover-scale': '1.18',
|
|
|
// ── Hover actions — one floating pill cluster ────────────────────
|
|
|
'actions-bg': 'var(--color-surface-default)',
|
|
|
'actions-border': 'var(--color-neutral-separator)',
|
|
|
'actions-shadow': 'var(--shadow-2)',
|
|
|
'actions-radius': 'var(--radius-full)',
|
|
|
'actions-padding': 'var(--space-0-5)',
|
|
|
// ── Mention highlight ────────────────────────────────────────────
|
|
|
'mentioned-bg': 'var(--color-primary-surface)',
|
|
|
'mentioned-accent': 'var(--color-primary-solid)',
|
|
|
// ── Actions cluster ──────────────────────────────────────────────
|
|
|
'actions-gap': 'var(--space-0-5)',
|
|
|
'actions-offset': 'var(--space-1)'
|
|
|
},
|
|
|
'chat-typing': {
|
|
|
// ── Row (dots + aggregated label) ─────────────────────────────────
|
|
|
gap: 'var(--space-2)',
|
|
|
'font-size': 'var(--size-sm-font-size)',
|
|
|
fg: 'var(--color-content-muted)',
|
|
|
// Reserved line height — the row keeps its slot while idle so the
|
|
|
// conversation above never shifts when typing starts/stops.
|
|
|
'min-block-size': 'var(--size-sm-control-height)',
|
|
|
// ── Dots ──────────────────────────────────────────────────────────
|
|
|
'dot-size': 'var(--space-1)',
|
|
|
'dot-gap': 'var(--space-0-5)',
|
|
|
'dot-fg': 'var(--color-content-muted)'
|
|
|
},
|
|
|
'emoji-picker': {
|
|
|
// ── Trigger (icon button that opens the picker) ───────────────────
|
|
|
'trigger-size': 'var(--size-md-control-height)',
|
|
|
'trigger-radius': 'var(--radius-md)',
|
|
|
'trigger-font-size': 'var(--size-md-font-size)',
|
|
|
// ── Panel (portaled; Popover provides surface + depth). The picker owns
|
|
|
// its width as a component token (color-picker precedent: a concrete rem
|
|
|
// sized to ~9 emoji columns), passed to the Popover's `width`. ─────────
|
|
|
'panel-width': '23rem',
|
|
|
'panel-gap': 'var(--space-2)',
|
|
|
'header-gap': 'var(--space-2)',
|
|
|
// ── Grid (a scrollable CSS grid of emoji cells) ───────────────────
|
|
|
'grid-gap': 'var(--space-0-5)',
|
|
|
'grid-max-block-size': 'min(50vh, 17rem)',
|
|
|
// ── Option (one emoji cell — hover/selected from archetype 'item') ─
|
|
|
'option-radius': 'var(--radius-md)',
|
|
|
'option-font-size': 'var(--size-lg-font-size)',
|
|
|
// ── Footer (preview + skin-tone selector) ─────────────────────────
|
|
|
'footer-gap': 'var(--space-2)',
|
|
|
'footer-border-fg': 'var(--color-neutral-separator)',
|
|
|
'preview-gap': 'var(--space-2)',
|
|
|
'preview-glyph-size': 'var(--size-xl-font-size)',
|
|
|
'preview-name-font-size': 'var(--size-sm-font-size)',
|
|
|
'preview-name-fg': 'var(--color-content-secondary)',
|
|
|
'preview-min-block-size': 'var(--size-lg-control-height)'
|
|
|
},
|
|
|
checkbox: {
|
|
|
'size-xs-box-size': '14px',
|
|
|
'size-xs-indicator-size': '8px',
|
|
|
'size-xs-indicator-stroke': '2px',
|
|
|
'size-sm-box-size': '16px',
|
|
|
'size-sm-indicator-size': '10px',
|
|
|
'size-sm-indicator-stroke': '2px',
|
|
|
'size-md-box-size': '18px',
|
|
|
'size-md-indicator-size': '12px',
|
|
|
'size-md-indicator-stroke': '2px',
|
|
|
'size-lg-box-size': '22px',
|
|
|
'size-lg-indicator-size': '14px',
|
|
|
'size-lg-indicator-stroke': '2px',
|
|
|
'size-xl-box-size': '26px',
|
|
|
'size-xl-indicator-size': '16px',
|
|
|
'size-xl-indicator-stroke': '2px',
|
|
|
// Invalid wins over the active colour across every variant: the border and
|
|
|
// the double focus ring (gap + ring) that marks it.
|
|
|
'invalid-border': 'var(--color-threat-element)',
|
|
|
'invalid-ring': 'var(--color-threat-element)',
|
|
|
'invalid-ring-gap': 'var(--color-surface-default)',
|
|
|
radius: 'var(--radius-sm)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
// Checkmark stroke draw — synced with the box transition (--duration-fast).
|
|
|
// Was a hardcoded 220ms, which read as laggy next to the 120ms box fill.
|
|
|
'stroke-duration': 'var(--duration-fast)',
|
|
|
'stroke-ease': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
|
'bg-off': 'var(--color-surface-default)',
|
|
|
'border-off': 'var(--color-border-default)',
|
|
|
'hover-border-off': 'var(--color-border-strong)',
|
|
|
'disabled-bg':
|
|
|
'color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) )',
|
|
|
'disabled-border':
|
|
|
'color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) )',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'indicator-fg': 'var(--color-content-on-solid)',
|
|
|
'disabled-indicator-fg':
|
|
|
'color-mix( in srgb, var(--color-content-on-solid) 82%, var(--color-surface-default) )',
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-solid-hover': 'var(--color-primary-solid-hover)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
|
|
|
// Private palette (TSC v2.1 `_` prefix → emits `--_checkbox-palette-*`).
|
|
|
// Recipe-internal cascade: host default = primary, plus overrides
|
|
|
// for the 3 supported color values. Migrated from manual blocks
|
|
|
// in checkbox.css.
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--checkbox-primary-solid)', scope: 'host' },
|
|
|
{ value: 'var(--checkbox-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--checkbox-neutral-solid)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--checkbox-affirm-solid)', scope: 'color:affirm' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-solid-hover': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--checkbox-primary-solid-hover)', scope: 'host' },
|
|
|
{ value: 'var(--checkbox-secondary-solid-hover)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--checkbox-neutral-solid-hover)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--checkbox-affirm-solid-hover)', scope: 'color:affirm' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-text': { value: 'var(--checkbox-indicator-fg)', scope: 'host' },
|
|
|
'group-gap': 'var(--space-3)',
|
|
|
'group-label-font-family': 'var(--style-label-font-family)',
|
|
|
'group-label-font-size': 'var(--size-sm-font-size)',
|
|
|
'group-label-font-weight': 'var(--font-weight-medium)',
|
|
|
'group-label-line-height': 'var(--leading-ui)',
|
|
|
'group-label-fg': 'var(--color-content-primary)',
|
|
|
'disabled-group-label-fg': 'var(--color-content-secondary)'
|
|
|
},
|
|
|
collapsible: {
|
|
|
'trigger-font-family': 'var(--style-label-font-family)',
|
|
|
'trigger-font-size': 'var(--size-sm-font-size)',
|
|
|
'trigger-font-weight': 'var(--font-weight-medium)',
|
|
|
'trigger-fg': 'var(--color-content-primary)',
|
|
|
'hover-trigger-fg': 'var(--color-content-primary)',
|
|
|
'trigger-radius': 'var(--radius-sm)',
|
|
|
'trigger-padding': 'var(--space-2) var(--space-3)',
|
|
|
'content-padding': 'var(--space-3)',
|
|
|
'transition-duration': 'var(--duration-normal)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
dialog: {
|
|
|
'overlay-bg': 'var(--color-overlay)',
|
|
|
'overlay-opacity': '62%',
|
|
|
'overlay-blur': 'var(--blur-lg)',
|
|
|
'overlay-z': 'var(--z-index-overlay-backdrop)',
|
|
|
'content-z': 'var(--z-index-overlay-content)',
|
|
|
// Elevation (bg · border · shadow) now comes from the `modal` plane via
|
|
|
// `data-depth` (A1, Decisión 8). The recipe keeps only non-elevation:
|
|
|
// content color + the panel radius (separate axis — Radix factor, A2).
|
|
|
'content-fg': 'var(--color-content-primary)',
|
|
|
'content-radius': 'var(--radius-xl)',
|
|
|
'content-max-height': 'calc(100dvh - var(--space-8))',
|
|
|
'content-max-height-sheet': '85dvh',
|
|
|
'content-width-sm': '420px',
|
|
|
'content-width-md': '560px',
|
|
|
'content-width-lg': '760px',
|
|
|
'content-width-xl': '1024px',
|
|
|
'content-width-inset': 'var(--space-6)',
|
|
|
'content-full-inset': 'var(--space-3)',
|
|
|
'content-full-radius': 'var(--radius-xxl)',
|
|
|
'content-position-inset': 'var(--space-4)',
|
|
|
'content-padding-sm': 'var(--space-4)',
|
|
|
'content-padding-md': 'var(--space-5)',
|
|
|
'content-padding-lg': 'var(--space-6)',
|
|
|
'content-padding-full': 'var(--space-6)',
|
|
|
'stack-gap': 'var(--space-4)',
|
|
|
'header-gap': 'var(--space-3)',
|
|
|
'footer-gap': 'var(--space-3)',
|
|
|
'footer-margin-top': 'var(--space-2)',
|
|
|
// Title = the canonical `h3` heading role (Arq. 5 / THEMING §3). These stay
|
|
|
// public knobs (a theme / instance overrides them to restyle dialog titles
|
|
|
// without retuning every h3); their DEFAULT is the role. Adopting `h3` moves
|
|
|
// the title off the old `--font-family-display` (Lora) — off the type scale at an
|
|
|
// h3 size — onto `primary` (sans), per h3–h6 = primary.
|
|
|
'title-font-family': 'var(--style-h3-font-family)',
|
|
|
'title-font-size': 'var(--style-h3-font-size)',
|
|
|
'title-font-weight': 'var(--style-h3-font-weight)',
|
|
|
'title-line-height': 'var(--style-h3-line-height)',
|
|
|
'title-letter-spacing': 'var(--style-h3-letter-spacing)',
|
|
|
'title-fg': 'var(--style-h3-color)',
|
|
|
'description-font-family': 'var(--style-label-font-family)',
|
|
|
'description-font-size': 'var(--size-sm-font-size)',
|
|
|
'description-line-height': 'var(--leading-ui)',
|
|
|
'description-fg': 'var(--color-content-secondary)',
|
|
|
// The close composes <Button>/<IconButton>, which own all visual chrome
|
|
|
// (bg / hover / focus-ring / disabled / radius). Only the dialog-specific
|
|
|
// floating geometry stays here: `close-size` reserves header room beside a
|
|
|
// floating close, `close-inset` is its corner offset.
|
|
|
'close-size': 'var(--size-sm-control-height)',
|
|
|
'close-inset': 'var(--space-3)'
|
|
|
},
|
|
|
field: {
|
|
|
'gap-xs': 'var(--space-1)',
|
|
|
'gap-sm': 'var(--space-1-5)',
|
|
|
'gap-md': 'var(--space-2)',
|
|
|
'gap-lg': 'var(--space-2-5)',
|
|
|
'gap-xl': 'var(--space-3)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'label-gap': 'var(--space-1)',
|
|
|
// Label size is derived in field.css (control font − one step), not a
|
|
|
// per-size token — keeps it coherent with the segmented fields' calc.
|
|
|
'label-font-weight': 'var(--font-weight-regular)',
|
|
|
'label-line-height': 'var(--leading-ui)',
|
|
|
'label-fg': 'var(--color-content-primary)',
|
|
|
'invalid-label-fg': 'var(--color-risk-text)',
|
|
|
'disabled-label-fg': 'var(--color-content-disabled)',
|
|
|
// Floated-label vertical anchor (floatingLabel mode). `0` = the label's
|
|
|
// centre lands ON the control's top border (outlined style, default). A
|
|
|
// theme can retarget it — negative to float fully above, or a positive
|
|
|
// offset to float top-inside (filled style) — without touching the recipe.
|
|
|
'floating-label-top': '0px',
|
|
|
// Side breathing room of the floated label: it sits ON the control's
|
|
|
// border, so the padding is what CUTS the border under the text.
|
|
|
'floating-label-padding-inline': 'var(--space-1)',
|
|
|
'layout-column-gap': 'var(--space-4)',
|
|
|
'required-fg': 'var(--color-risk-text)',
|
|
|
// The optional marker is the required marker's twin: muted, and a weight
|
|
|
// step below the label it trails.
|
|
|
'optional-fg': 'var(--color-content-muted)',
|
|
|
'optional-font-weight': 'var(--font-weight-regular)',
|
|
|
'control-height-xs': 'var(--size-xs-control-height)',
|
|
|
'control-height-sm': 'var(--size-sm-control-height)',
|
|
|
'control-height-md': 'var(--size-md-control-height)',
|
|
|
'control-height-lg': 'var(--size-lg-control-height)',
|
|
|
'control-height-xl': 'var(--size-xl-control-height)',
|
|
|
// Segment height — derived from the SIZE axis at the FIELD family level
|
|
|
// (B.1 mandate, audit S6): the segmented fields (date/time/color + ranges)
|
|
|
// read `--field-segment-height` instead of each re-declaring `height-{k}`.
|
|
|
// The `size:` TSC scope emits `[data-field][data-size='{k}']`; host default
|
|
|
// = md (the field's default size). Inert until a field wraps in `[data-field]`.
|
|
|
'segment-height': {
|
|
|
declarations: [
|
|
|
{ value: 'calc(var(--field-control-height-md) - var(--space-2))', scope: 'host' },
|
|
|
{ value: 'calc(var(--field-control-height-xs) - var(--space-2))', scope: 'size:xs' },
|
|
|
{ value: 'calc(var(--field-control-height-sm) - var(--space-2))', scope: 'size:sm' },
|
|
|
{ value: 'calc(var(--field-control-height-lg) - var(--space-2))', scope: 'size:lg' },
|
|
|
{ value: 'calc(var(--field-control-height-xl) - var(--space-2))', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'control-padding-inline-xs': 'var(--space-2)',
|
|
|
'control-padding-inline-sm': 'var(--space-2-5)',
|
|
|
'control-padding-inline-md': 'var(--space-3)',
|
|
|
'control-padding-inline-lg': 'var(--space-3-5)',
|
|
|
'control-padding-inline-xl': 'var(--space-4)',
|
|
|
'control-gap-xs': 'var(--space-1)',
|
|
|
'control-gap-sm': 'var(--space-1-5)',
|
|
|
'control-gap-md': 'var(--space-2)',
|
|
|
'control-gap-lg': 'var(--space-2-5)',
|
|
|
'control-gap-xl': 'var(--space-3)',
|
|
|
'control-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'control-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'control-font-size-md': 'var(--size-md-font-size)',
|
|
|
'control-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'control-font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'control-line-height': 'var(--leading-ui)',
|
|
|
// Value/control text sits one weight step ABOVE the label: the value is
|
|
|
// `medium`, the label stays `regular` (the "label one step below the
|
|
|
// component" doctrine — the size offset is derived in field.css).
|
|
|
'control-font-weight': 'var(--font-weight-medium)',
|
|
|
'control-radius': 'var(--radius-md)',
|
|
|
'control-border-width': 'var(--border-width)',
|
|
|
'control-border': 'var(--color-border-default)',
|
|
|
'hover-control-border': 'var(--color-border-strong)',
|
|
|
'focus-control-border': 'var(--focus-ring-color)',
|
|
|
'invalid-control-border': 'var(--color-risk-border)',
|
|
|
// Per-instance palette (THM-2 forward). The `color` prop's only live accent
|
|
|
// is the floating-label focus tint (`--_field-palette-text`). Host default =
|
|
|
// primary; the shared forward routes roles + 33 scales + custom automatically.
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' },
|
|
|
'control-bg': 'var(--color-surface-default)',
|
|
|
'control-ghost-border': 'transparent',
|
|
|
'control-ghost-bg': 'transparent',
|
|
|
'control-bg-readonly': 'var(--color-surface-muted)',
|
|
|
'disabled-control-bg': 'var(--color-surface-muted)',
|
|
|
'control-fg': 'var(--color-content-primary)',
|
|
|
'control-placeholder-fg': 'var(--color-content-muted)',
|
|
|
'affix-fg': 'var(--color-content-secondary)',
|
|
|
// The icon affordance inside the control (date/time/colour pickers,
|
|
|
// password reveal). ONE flush treatment for every field: no border, no
|
|
|
// shadow, the system's neutral hover tint — never the field's accent.
|
|
|
// Its box is sized from the field font, so the em ratio travels with the
|
|
|
// size axis instead of being re-declared per step.
|
|
|
'control-trigger-size': '1.75em',
|
|
|
'control-trigger-radius': 'var(--radius-sm)',
|
|
|
'control-trigger-fg': 'var(--color-content-secondary)',
|
|
|
'hover-control-trigger-fg': 'var(--color-content-primary)',
|
|
|
'focus-control-trigger-fg': 'var(--color-content-primary)',
|
|
|
// Shared field-family segment highlight (single source, theme-retunable
|
|
|
// from one place): the date/time/color ACTIVE segment being edited.
|
|
|
// `segment-active-bg` bakes its own transparency (color-mix), so a
|
|
|
// theme retunes colour + opacity + text together. Segment HOVER is the
|
|
|
// state-layer (`--state-hover`), not a field token
|
|
|
// (field-segment-state.css).
|
|
|
'segment-active-bg': 'color-mix(in srgb, var(--color-primary-border) 40%, transparent)',
|
|
|
'segment-active-text': 'var(--color-primary-text)',
|
|
|
'message-line-height': 'var(--leading-ui)',
|
|
|
'helper-fg': 'var(--color-content-secondary)',
|
|
|
'error-fg': 'var(--color-risk-text)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
form: {
|
|
|
'gap-xs': 'var(--space-3)',
|
|
|
'gap-sm': 'var(--space-4)',
|
|
|
'gap-md': 'var(--space-5)',
|
|
|
'gap-lg': 'var(--space-6)',
|
|
|
'gap-xl': 'var(--space-7)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
radius: 'var(--radius-lg)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
'panel-padding': 'var(--space-5)',
|
|
|
'panel-border': 'var(--color-border-default)',
|
|
|
'panel-bg': 'var(--color-surface-raised)',
|
|
|
'panel-shadow': 'var(--depth-raised-shadow), var(--depth-raised-halo)',
|
|
|
'grid-min': '18rem',
|
|
|
'grid-column-gap': 'var(--space-5)',
|
|
|
'action-height-xs': 'var(--size-xs-control-height)',
|
|
|
'action-height-sm': 'var(--size-sm-control-height)',
|
|
|
'action-height-md': 'var(--size-md-control-height)',
|
|
|
'action-height-lg': 'var(--size-lg-control-height)',
|
|
|
'action-height-xl': 'var(--size-xl-control-height)',
|
|
|
'action-padding-inline-xs': 'var(--space-2-5)',
|
|
|
'action-padding-inline-sm': 'var(--space-3)',
|
|
|
'action-padding-inline-md': 'var(--space-3-5)',
|
|
|
'action-padding-inline-lg': 'var(--space-4)',
|
|
|
'action-padding-inline-xl': 'var(--space-4-5)',
|
|
|
'action-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'action-font-size-sm': 'var(--size-xs-font-size)',
|
|
|
'action-font-size-md': 'var(--size-sm-font-size)',
|
|
|
'action-font-size-lg': 'var(--size-sm-font-size)',
|
|
|
'action-font-size-xl': 'var(--size-md-font-size)',
|
|
|
'action-font-weight': 'var(--font-weight-medium)',
|
|
|
'action-line-height': 'var(--leading-ui)',
|
|
|
'action-gap': 'var(--space-2)',
|
|
|
'action-radius': 'var(--radius-md)',
|
|
|
'action-border-width': 'var(--border-width)',
|
|
|
// Action palette forward (THM-2) — submit/reset tint through the shared
|
|
|
// layer (roles + 33 scales + custom). Host = primary (submit's stamped
|
|
|
// default); `'neutral'` stamps nothing and keeps the base action look.
|
|
|
'_palette-solid': {
|
|
|
parts: ['submit', 'reset'],
|
|
|
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-solid-hover': {
|
|
|
parts: ['submit', 'reset'],
|
|
|
declarations: [{ value: 'var(--color-primary-solid-hover)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
parts: ['submit', 'reset'],
|
|
|
declarations: [{ value: 'var(--color-primary-border)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-contrast': {
|
|
|
parts: ['submit', 'reset'],
|
|
|
declarations: [{ value: 'var(--color-primary-contrast)', scope: 'host' }]
|
|
|
},
|
|
|
'error-summary-padding': 'var(--space-3)',
|
|
|
'error-summary-border-width': 'var(--border-width)',
|
|
|
'error-summary-radius': 'var(--radius-md)',
|
|
|
'error-summary-font-size': 'var(--size-sm-font-size)',
|
|
|
'error-summary-line-height': 'var(--leading-ui)',
|
|
|
'error-summary-list-indent': 'var(--space-4)',
|
|
|
'link-underline-offset': '0.18em',
|
|
|
'auto-fields-gap': 'var(--space-3)',
|
|
|
'auto-fields-array-padding': 'var(--space-3)',
|
|
|
'auto-fields-border-width': 'var(--border-width)',
|
|
|
'auto-fields-radius': 'var(--radius-md)',
|
|
|
'auto-fields-item-gap': 'var(--space-2)',
|
|
|
'auto-fields-item-padding': 'var(--space-3)',
|
|
|
'widget-padding-inline': 'var(--space-3)',
|
|
|
'widget-radius': 'var(--radius-md)',
|
|
|
'widget-border-width': 'var(--border-width)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
drawer: {
|
|
|
// El disparador por defecto — el mismo cromo que el de popover, y como allí
|
|
|
// `min-block-size` lee el BUNDLE de talla y no el primitivo crudo que el CSS
|
|
|
// traía. Su hover comparte valor con el estado abierto de popover; aquí no
|
|
|
// hay estado abierto que teñir, así que una sola clave.
|
|
|
'trigger-gap': 'var(--space-2)',
|
|
|
'trigger-min-block-size': 'var(--size-md-control-height)',
|
|
|
'trigger-padding-inline': 'var(--space-3)',
|
|
|
'trigger-border-width': 'var(--border-width)',
|
|
|
'trigger-border': 'var(--color-border-default)',
|
|
|
'trigger-radius': 'var(--radius-md)',
|
|
|
'trigger-bg': 'var(--color-surface-raised)',
|
|
|
'trigger-fg': 'var(--color-content-primary)',
|
|
|
'trigger-font-weight': 'var(--font-weight-medium)',
|
|
|
'hover-trigger-bg': 'var(--color-surface-overlay)',
|
|
|
'overlay-bg': 'var(--color-overlay)',
|
|
|
'overlay-blur': 'var(--blur-lg)',
|
|
|
'overlay-opacity': '62%',
|
|
|
'overlay-z': 'var(--z-index-overlay-backdrop)',
|
|
|
'content-z': 'var(--z-index-overlay-content)',
|
|
|
'inline-z': 'var(--z-index-overlay-inline)',
|
|
|
'handle-bg': 'var(--color-neutral-track)',
|
|
|
'active-handle-bg': 'var(--color-content-secondary)',
|
|
|
'handle-radius': 'var(--radius-full)',
|
|
|
'handle-length': '36px',
|
|
|
'handle-thickness': '4px',
|
|
|
'handle-active-scale': '1.18',
|
|
|
'handle-transition-duration': 'var(--duration-fast)',
|
|
|
'handle-transition-ease': 'var(--ease-out)',
|
|
|
// Surface · border · base-shadow now come from the `modal` plane (data-depth,
|
|
|
// A1); the in-flight drag shadow keeps its own token.
|
|
|
'content-fg': 'var(--color-content-primary)',
|
|
|
'content-radius': 'var(--radius-xl)',
|
|
|
'content-shadow-dragging':
|
|
|
'var(--shadow-overlay-strong, var(--depth-overlay-shadow)), var(--depth-overlay-halo)',
|
|
|
'content-ring-dragging-width': 'var(--border-width-medium)',
|
|
|
'content-ring-dragging-fg':
|
|
|
'color-mix( in srgb, var(--color-content-secondary) 32%, transparent )',
|
|
|
'content-ring-dragging-offset': '2px',
|
|
|
'content-inset': 'var(--space-3)',
|
|
|
'content-width-sm': '360px',
|
|
|
'content-width-md': '440px',
|
|
|
'content-width-lg': '560px',
|
|
|
'content-height-sm': '260px',
|
|
|
'content-height-md': '360px',
|
|
|
'content-height-lg': '480px',
|
|
|
'content-padding-sm': 'var(--space-4)',
|
|
|
'content-padding-md': 'var(--space-5)',
|
|
|
'content-padding-lg': 'var(--space-6)',
|
|
|
'content-padding-full': 'var(--space-6)',
|
|
|
'stack-gap': 'var(--space-4)',
|
|
|
'header-gap': 'var(--space-3)',
|
|
|
'footer-gap': 'var(--space-3)',
|
|
|
'footer-margin-top': 'var(--space-2)',
|
|
|
'title-font-family': 'var(--font-family-display)',
|
|
|
'title-font-size': 'var(--size-lg-font-size)',
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'title-line-height': 'var(--leading-heading)',
|
|
|
'title-fg': 'var(--color-content-primary)',
|
|
|
'description-font-family': 'var(--style-label-font-family)',
|
|
|
'description-font-size': 'var(--size-sm-font-size)',
|
|
|
'description-line-height': 'var(--leading-ui)',
|
|
|
'description-fg': 'var(--color-content-secondary)'
|
|
|
// `close-*` tokens removed: Drawer.Close composes the canonical
|
|
|
// <IconButton>/<Button>, so its chrome lives in button.css.
|
|
|
},
|
|
|
icon: {
|
|
|
size: 'var(--size-md-icon-size)',
|
|
|
'stroke-width': 'var(--icon-stroke-width-md)'
|
|
|
},
|
|
|
image: {
|
|
|
// ── Size presets (square boxes) ──────────────────────────────────
|
|
|
'size-xs': '64px',
|
|
|
'size-sm': '96px',
|
|
|
'size-md': '160px',
|
|
|
'size-lg': '240px',
|
|
|
'size-xl': '360px',
|
|
|
// ── Radius scale ──────────────────────────────────────────────────
|
|
|
'radius-none': '0',
|
|
|
'radius-sm': 'var(--radius-sm)',
|
|
|
'radius-md': 'var(--radius-md)',
|
|
|
'radius-lg': 'var(--radius-lg)',
|
|
|
'radius-full': 'var(--radius-full)',
|
|
|
// ── Fade-in transition for the inner <img> ───────────────────────
|
|
|
'fade-duration': 'var(--duration-normal)',
|
|
|
'fade-ease': 'var(--ease-out)',
|
|
|
// ── Skeleton placeholder shimmer ──────────────────────────────────
|
|
|
'placeholder-base': 'var(--color-surface-raised)',
|
|
|
// The shimmering bar itself: the system gradient, named here so a theme can
|
|
|
// retune the skeleton without touching every recipe that shimmers.
|
|
|
'skeleton-bg': 'var(--gradient-shimmer)',
|
|
|
'shimmer-duration': '1.8s',
|
|
|
// Placeholder palette forward (THM-2) — `placeholderColor` tints the
|
|
|
// colour block via the shared layer (roles + 33 scales + custom).
|
|
|
// Colored-only: `'neutral'` (the default) stamps no `data-color`, so
|
|
|
// the host value here is the forward fallback only.
|
|
|
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
|
|
|
// ── Fallback typography (for text-only fallback content) ─────────
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'fallback-fg': 'var(--color-content-secondary)',
|
|
|
'fallback-font-size': 'var(--size-sm-font-size)',
|
|
|
'fallback-line-height': 'var(--leading-ui)',
|
|
|
// ── Error state ──────────────────────────────────────────────────
|
|
|
'error-bg': 'var(--color-surface-raised)',
|
|
|
'error-fg': 'var(--color-content-muted)',
|
|
|
'error-font-size': 'var(--size-sm-font-size)',
|
|
|
'error-line-height': 'var(--leading-ui)'
|
|
|
},
|
|
|
meter: {
|
|
|
'height-xs': '2px',
|
|
|
'height-sm': '4px',
|
|
|
'height-md': '6px',
|
|
|
'height-lg': '8px',
|
|
|
'height-xl': '12px',
|
|
|
'radius-xs': 'var(--radius-full)',
|
|
|
'radius-sm': 'var(--radius-full)',
|
|
|
'radius-md': 'var(--radius-full)',
|
|
|
'radius-lg': 'var(--radius-full)',
|
|
|
'radius-xl': 'var(--radius-full)',
|
|
|
'ring-size-xs': '2rem',
|
|
|
'ring-size-sm': '2.5rem',
|
|
|
'ring-size-md': '3rem',
|
|
|
'ring-size-lg': '3.5rem',
|
|
|
'ring-size-xl': '4rem',
|
|
|
'ring-thickness-xs': '3px',
|
|
|
'ring-thickness-sm': '4px',
|
|
|
'ring-thickness-md': '6px',
|
|
|
'ring-thickness-lg': '8px',
|
|
|
'ring-thickness-xl': '10px',
|
|
|
'track-bg': 'var(--color-neutral-track)',
|
|
|
'ring-track-bg':
|
|
|
'color-mix(in srgb, var(--color-content-primary) 12%, var(--color-surface-default))',
|
|
|
'ring-center-bg': 'var(--color-surface-default)',
|
|
|
'indicator-bg-below': 'var(--color-risk-solid)',
|
|
|
'indicator-bg-optimum': 'var(--color-fulfill-solid)',
|
|
|
'indicator-bg-above': 'var(--color-threat-solid)',
|
|
|
'transition-duration': 'var(--duration-normal)',
|
|
|
'transition-ease': 'var(--ease-default)'
|
|
|
},
|
|
|
// spin-field — shared visual for the stepper-field family (number-field +
|
|
|
// css-field). Both reuse these tokens via structural identity (`data-spin-
|
|
|
// field*`) and the shared `spin-field.css`. A theme tints per component by
|
|
|
// scoping `[data-number-field] { --spin-field-… }`. See THEMING §34.
|
|
|
'spin-field': {
|
|
|
border: 'var(--color-border-default)',
|
|
|
// Steppers are square segments flush to the field edge (the field clips
|
|
|
// their outer corners), separated from the input by a divider on the
|
|
|
// inner side.
|
|
|
'control-radius': '0',
|
|
|
'control-border-width': 'var(--border-width)',
|
|
|
'control-border': 'transparent',
|
|
|
'control-divider': 'var(--color-border-default)',
|
|
|
'hover-control-border': 'var(--color-border-strong)',
|
|
|
'active-control-border': 'var(--color-primary-border)',
|
|
|
'control-bg': 'transparent',
|
|
|
// Higher-contrast hover/focus fill than the field surface.
|
|
|
'focus-control-bg': 'var(--color-neutral-hover)',
|
|
|
'active-control-bg': 'var(--color-primary-track)',
|
|
|
'control-fg': 'var(--color-content-secondary)',
|
|
|
'hover-control-fg': 'var(--color-content-primary)',
|
|
|
'active-control-fg': 'var(--color-primary-text)',
|
|
|
// Default stepper glyphs — themeable, decorative (the buttons carry an
|
|
|
// aria-label). Rendered via `:empty::before`, so passing children to a
|
|
|
// trigger overrides them. Split uses +/−; the stacked column uses up/down
|
|
|
// triangles. A theme retints/reshapes by overriding any of the four.
|
|
|
'control-increment-glyph': "'+'",
|
|
|
'control-decrement-glyph': "'\\2212'",
|
|
|
'control-increment-glyph-stacked': "'\\25B2'",
|
|
|
'control-decrement-glyph-stacked': "'\\25BC'",
|
|
|
'control-disabled-opacity': '0.48',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)'
|
|
|
},
|
|
|
'float-panel': {
|
|
|
// Las dos variantes que ajustan desde el default elevado, y los botones de
|
|
|
// la barra. El asa de redimensionado leía
|
|
|
// `var(--color-content-tertiary, …)`: ese primitivo NO EXISTE —pintaba el
|
|
|
// respaldo—, así que el contrato guarda lo que de verdad pintaba.
|
|
|
'outline-bg': 'var(--color-surface-default)',
|
|
|
'outline-border': 'var(--color-border-strong)',
|
|
|
'ghost-bg': 'var(--color-surface-default)',
|
|
|
'ghost-border': 'var(--color-border-subtle)',
|
|
|
'ghost-shadow': 'var(--shadow-2)',
|
|
|
'action-radius': 'var(--radius-sm)',
|
|
|
'action-fg': 'var(--color-content-secondary)',
|
|
|
'action-font-size': 'var(--size-md-font-size)',
|
|
|
'hover-action-bg': 'var(--color-surface-muted)',
|
|
|
'hover-action-fg': 'var(--color-content-primary)',
|
|
|
'resize-grip-fg': 'var(--color-content-secondary)',
|
|
|
'resize-grip-radius': 'var(--radius-sm)',
|
|
|
bg: 'var(--color-surface-overlay)',
|
|
|
border: 'var(--color-border-default)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
radius: 'var(--radius-lg)',
|
|
|
shadow: 'var(--shadow-4)',
|
|
|
'shadow-topmost': 'var(--shadow-5)',
|
|
|
// Elevation worn WHILE grabbed (drag/resize): the panel LIFTS to the
|
|
|
// theme's highest elevation. Applied on pointerdown so the lift transition
|
|
|
// finishes before the move; the shadow is then static during the drag, so
|
|
|
// the GPU layer just moves (no per-frame shadow repaint). See float-panel.css.
|
|
|
'active-shadow': 'var(--shadow-6)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size': 'var(--size-sm-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'header-bg': 'var(--color-surface-raised)',
|
|
|
'header-padding-inline': 'var(--space-3)',
|
|
|
'header-padding-block': 'var(--space-2)',
|
|
|
'header-gap': 'var(--space-2)',
|
|
|
'controls-gap': 'var(--space-1)',
|
|
|
'title-font-size': 'var(--size-sm-font-size)',
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'title-fg': 'var(--color-content-primary)',
|
|
|
'body-padding-inline': 'var(--space-3)',
|
|
|
'body-padding-block': 'var(--space-3)',
|
|
|
'resize-size': 'var(--space-3-5)',
|
|
|
'resize-edge': 'var(--space-1-5)',
|
|
|
// Header icon-button (close / action / stage-trigger) hit target, and the
|
|
|
// se-corner resize grip — sized for comfort, not the tiny resize-handle.
|
|
|
'button-size': 'var(--space-7)',
|
|
|
'grip-size': 'var(--space-5)',
|
|
|
accent: 'var(--color-neutral-border)',
|
|
|
// Accent palette forward (THM-2) — `color` tints the header rule, focus
|
|
|
// ring and resize grip via the shared layer (roles + 33 scales + custom)
|
|
|
// on the `content` part (the panel portals; it stamps its own
|
|
|
// `data-color`). Host = neutral-border, the stamped default's value.
|
|
|
'_palette-border': {
|
|
|
parts: ['content'],
|
|
|
declarations: [{ value: 'var(--color-neutral-border)', scope: 'host' }]
|
|
|
},
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)'
|
|
|
},
|
|
|
'image-adjustments': {
|
|
|
// Vertical rhythm: gap between adjustment rows, and within a row
|
|
|
// (label/value line → slider).
|
|
|
gap: 'var(--space-3)',
|
|
|
'row-gap': 'var(--space-1-5)',
|
|
|
'label-fg': 'var(--color-content-secondary)',
|
|
|
'label-font-size': 'var(--size-sm-font-size)',
|
|
|
'label-font-weight': 'var(--font-weight-medium)',
|
|
|
// Read-out: tabular numerals so the value doesn't jitter while dragging.
|
|
|
// The TERTIARY ACCENT ROLE's text slot (audit B.3, 2026-07-07): the
|
|
|
// original `--color-content-tertiary` mixed the content namespace with
|
|
|
// the role name and never existed — the read-out inherited its color
|
|
|
// since birth (7da7285d).
|
|
|
'value-fg': 'var(--color-tertiary-text)',
|
|
|
'value-font-size': 'var(--size-xs-font-size)',
|
|
|
'value-font-family': 'var(--font-family-mono)',
|
|
|
'reset-gap': 'var(--space-2)',
|
|
|
'reset-fg': 'var(--color-content-secondary)',
|
|
|
'reset-font-size': 'var(--size-xs-font-size)',
|
|
|
// The `hue` row paints its slider track as a hue wheel so the control
|
|
|
// reads as a hue picker. A literal rainbow (the hues ARE the value) — kept
|
|
|
// as a token so it's themeable and stays out of the component CSS.
|
|
|
'hue-track':
|
|
|
'linear-gradient(to right, hsl(0 85% 60%), hsl(60 85% 60%), hsl(120 85% 60%), hsl(180 85% 60%), hsl(240 85% 60%), hsl(300 85% 60%), hsl(360 85% 60%))'
|
|
|
},
|
|
|
'image-picker': {
|
|
|
// Vertical stack: preview → adjustments.
|
|
|
gap: 'var(--space-3)',
|
|
|
// Preview box (ready state). The composed <Image> fills it.
|
|
|
'preview-radius': 'var(--radius-lg)',
|
|
|
'preview-bg': 'var(--color-surface-muted)',
|
|
|
'preview-border': 'var(--color-border-default)',
|
|
|
'preview-aspect': '4 / 3',
|
|
|
// Toolbar floats over the preview's top-right corner.
|
|
|
'toolbar-gap': 'var(--space-1-5)',
|
|
|
'toolbar-inset': 'var(--space-2)',
|
|
|
// Icon buttons (rotate / remove).
|
|
|
'button-size': 'var(--space-8)',
|
|
|
'button-bg': 'color-mix(in srgb, var(--color-surface-default) 88%, transparent)',
|
|
|
'button-fg': 'var(--color-content-primary)',
|
|
|
'button-border': 'var(--color-border-default)',
|
|
|
'button-radius': 'var(--radius-md)',
|
|
|
'button-font-size': 'var(--size-md-font-size)'
|
|
|
},
|
|
|
cropper: {
|
|
|
'viewport-bg': 'var(--color-surface-muted)',
|
|
|
'viewport-radius': 'var(--radius-lg)',
|
|
|
// The darken-outside mask + the thirds grid carry literal rgba/rgb (an
|
|
|
// overlay tint, not a theme color) — kept as tokens so they stay out of
|
|
|
// the component CSS (raw-color contract) and remain themeable.
|
|
|
'mask-bg': 'rgb(0 0 0 / 0.55)',
|
|
|
'grid-line': 'rgb(255 255 255 / 0.4)',
|
|
|
'selection-border': 'var(--color-surface-default)',
|
|
|
'selection-border-width': 'var(--border-width)',
|
|
|
'handle-size': 'var(--space-3-5)',
|
|
|
'handle-bg': 'var(--color-surface-default)',
|
|
|
'handle-border': 'var(--color-primary-solid)',
|
|
|
'handle-border-width': 'var(--border-width)',
|
|
|
'zoom-gap': 'var(--space-2)',
|
|
|
'zoom-btn-size': 'var(--space-7)',
|
|
|
'zoom-btn-bg': 'var(--color-surface-muted)',
|
|
|
'zoom-btn-fg': 'var(--color-content-primary)',
|
|
|
// The zoom buttons' own chrome. `font-size` is the SEAM: the value is the
|
|
|
// system's type scale, the knob is this component's, so a theme can size
|
|
|
// the ± glyphs without moving `md` text everywhere.
|
|
|
'zoom-btn-radius': 'var(--radius-md)',
|
|
|
'zoom-btn-font-size': 'var(--size-md-font-size)',
|
|
|
'zoom-btn-border': 'var(--color-border-default)'
|
|
|
},
|
|
|
'date-field': {
|
|
|
'gap-xs': 'var(--space-0-5)',
|
|
|
'gap-sm': 'var(--space-0-5)',
|
|
|
'gap-md': 'var(--space-1)',
|
|
|
'gap-lg': 'var(--space-1-5)',
|
|
|
'gap-xl': 'var(--space-2)',
|
|
|
'segment-padding-inline-xs': 'var(--space-0-5)',
|
|
|
'segment-padding-inline-sm': 'var(--space-0-5)',
|
|
|
'segment-padding-inline-md': 'var(--space-1)',
|
|
|
'segment-padding-inline-lg': 'var(--space-1-5)',
|
|
|
'segment-padding-inline-xl': 'var(--space-2)',
|
|
|
'placeholder-fg': 'var(--color-content-muted)',
|
|
|
'literal-fg': 'var(--color-content-muted)',
|
|
|
'segment-radius': 'var(--radius-sm)',
|
|
|
'segment-fg': 'var(--color-content-primary)',
|
|
|
'segment-invalid-fg': 'var(--color-risk-text)',
|
|
|
'segment-readonly-bg': 'var(--color-surface-overlay)',
|
|
|
'segment-readonly-fg': 'var(--color-content-secondary)',
|
|
|
'segment-readonly-decoration': 'underline dotted var(--color-content-muted)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
'time-field': {
|
|
|
'gap-xs': 'var(--space-0-5)',
|
|
|
'gap-sm': 'var(--space-0-5)',
|
|
|
'gap-md': 'var(--space-1)',
|
|
|
'gap-lg': 'var(--space-1-5)',
|
|
|
'gap-xl': 'var(--space-2)',
|
|
|
'segment-padding-inline-xs': 'var(--space-0-5)',
|
|
|
'segment-padding-inline-sm': 'var(--space-0-5)',
|
|
|
'segment-padding-inline-md': 'var(--space-1)',
|
|
|
'segment-padding-inline-lg': 'var(--space-1-5)',
|
|
|
'segment-padding-inline-xl': 'var(--space-2)',
|
|
|
'placeholder-fg': 'var(--color-content-muted)',
|
|
|
'literal-fg': 'var(--color-content-muted)',
|
|
|
'segment-radius': 'var(--radius-sm)',
|
|
|
'segment-fg': 'var(--color-content-primary)',
|
|
|
'segment-invalid-fg': 'var(--color-risk-text)',
|
|
|
'segment-readonly-bg': 'var(--color-surface-overlay)',
|
|
|
'segment-readonly-fg': 'var(--color-content-secondary)',
|
|
|
'segment-readonly-decoration': 'underline dotted var(--color-content-muted)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
'color-field': {
|
|
|
'segment-padding-inline-xs': 'var(--space-0-5)',
|
|
|
'segment-padding-inline-sm': 'var(--space-0-5)',
|
|
|
'segment-padding-inline-md': 'var(--space-1)',
|
|
|
'segment-padding-inline-lg': 'var(--space-1-5)',
|
|
|
'segment-padding-inline-xl': 'var(--space-2)',
|
|
|
'placeholder-fg': 'var(--color-content-muted)',
|
|
|
'literal-fg': 'var(--color-content-muted)',
|
|
|
'segment-radius': 'var(--radius-sm)',
|
|
|
'segment-fg': 'var(--color-content-primary)',
|
|
|
'segment-invalid-fg': 'var(--color-risk-text)',
|
|
|
'segment-readonly-bg': 'var(--color-surface-overlay)',
|
|
|
'segment-readonly-fg': 'var(--color-content-secondary)',
|
|
|
'segment-readonly-decoration': 'underline dotted var(--color-content-muted)',
|
|
|
// Leading value swatch: exactly the control icon size for this field size
|
|
|
// (user spec: "del mismo tamaño que los iconos del control"). A concrete
|
|
|
// length per size — never `1em` — so it can't recompute on re-render.
|
|
|
'swatch-size-xs': 'var(--size-xs-icon-size)',
|
|
|
'swatch-size-sm': 'var(--size-sm-icon-size)',
|
|
|
'swatch-size-md': 'var(--size-md-icon-size)',
|
|
|
'swatch-size-lg': 'var(--size-lg-icon-size)',
|
|
|
'swatch-size-xl': 'var(--size-xl-icon-size)',
|
|
|
'format-select-fg': 'var(--color-content-secondary)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
// The channel SEPARATORS (",", " / ") render as thin vertical divider
|
|
|
// lines between the editable segments — a rule of the recipe's own, so
|
|
|
// its ink and its thickness are knobs. `1px` IS `--border-width`: the
|
|
|
// divider is a hairline, and `dropdown-menu.separator-thickness` reads
|
|
|
// the same family. (The format's prefix / suffix GLYPHS, "#" and "%",
|
|
|
// are the same part rendered as real text and take `literal-fg`.)
|
|
|
'separator-bg': 'var(--color-border-default)',
|
|
|
'separator-thickness': 'var(--border-width)',
|
|
|
// A segment holds a FIXED number of characters so the field never
|
|
|
// reflows while typing: 3 for a channel (RGB 0-255 · HSL 0-360/0-100 ·
|
|
|
// alpha) and 6 for hex (RRGGBB). Font-relative on purpose — the width
|
|
|
// has to follow the face the theme picks. The twins (`date-field` /
|
|
|
// `time-field`) carry the same fact inside a `calc(2ch + padding)`, so
|
|
|
// theirs already rode a token; this one was a bare literal.
|
|
|
'segment-min-width': '3ch',
|
|
|
'hex-segment-min-width': '6ch'
|
|
|
},
|
|
|
'color-picker': {
|
|
|
// ── La costura (2026-08-22) ──────────────────────────────────────────
|
|
|
// El cromo que montaba sobre primitivos crudos. Los valores son los del
|
|
|
// canon; el knob pasa a ser del componente (PLAN §2-A).
|
|
|
gap: 'var(--space-1-5)',
|
|
|
'control-gap': 'var(--space-2)',
|
|
|
'label-fg': 'var(--color-content-primary)',
|
|
|
'trigger-font-family': 'var(--style-label-font-family)',
|
|
|
'trigger-line-height': 'var(--leading-ui)',
|
|
|
// La lectura del valor es metadato: mono, regular, y su propia tinta.
|
|
|
'value-font-family': 'var(--font-family-mono)',
|
|
|
'value-font-weight': 'var(--font-weight-regular)',
|
|
|
'value-fg': 'var(--color-content-primary)',
|
|
|
'value-line-height': 'var(--leading-ui)',
|
|
|
'swatch-border-width': 'var(--border-width)',
|
|
|
// El raíl de canal y su pulgar: el pulgar lleva DOS sombras que hacen una
|
|
|
// sola cosa (contorno + elevación), así que es un token, no dos.
|
|
|
'channel-track-border-width': 'var(--border-width)',
|
|
|
'channel-track-border': 'var(--color-border-subtle)',
|
|
|
'channel-thumb-border-width': 'var(--border-width-medium)',
|
|
|
'channel-thumb-border': 'var(--color-surface-default)',
|
|
|
'channel-thumb-shadow':
|
|
|
'0 0 0 1px color-mix(in srgb, var(--color-neutral-contrast) 45%, transparent), 0 1px 2px color-mix(in srgb, var(--color-neutral-contrast) 20%, transparent)',
|
|
|
// El hueco entre la muestra y su anillo de selección: es superficie, y por
|
|
|
// eso se llama por lo que hace y no por el color que lleva hoy.
|
|
|
'swatch-ring-gap': 'var(--color-surface-default)',
|
|
|
'eye-dropper-border-width': 'var(--border-width)',
|
|
|
// Popover content shell. The panel padding is the popover's canonical
|
|
|
// size-aware `--popover-padding-*` (shared by every floating panel) — the
|
|
|
// picker declares NO padding of its own. Inter-row gap is owned by
|
|
|
// [data-picker-body]; both kept the picker from being a padding outlier.
|
|
|
'content-width-sm': '17rem',
|
|
|
'content-width-md': '19.5rem',
|
|
|
'content-width-lg': '22rem',
|
|
|
|
|
|
// Trigger button (compact field-shaped pill with a swatch dot)
|
|
|
'trigger-height-sm': 'var(--size-sm-control-height)',
|
|
|
'trigger-height-md': 'var(--size-md-control-height)',
|
|
|
'trigger-height-lg': 'var(--size-lg-control-height)',
|
|
|
'trigger-padding-inline-sm': 'var(--space-2)',
|
|
|
'trigger-padding-inline-md': 'var(--space-2-5)',
|
|
|
'trigger-padding-inline-lg': 'var(--space-3)',
|
|
|
'trigger-gap': 'var(--space-2)',
|
|
|
'trigger-radius': 'var(--radius-md)',
|
|
|
'trigger-border-width': 'var(--border-width)',
|
|
|
'trigger-border': 'var(--color-border-default)',
|
|
|
'trigger-bg': 'var(--color-surface-default)',
|
|
|
'trigger-fg': 'var(--color-content-primary)',
|
|
|
'trigger-hover-border': 'var(--color-border-strong)',
|
|
|
'trigger-swatch-size-sm': 'calc(var(--size-sm-control-height) - var(--space-2))',
|
|
|
'trigger-swatch-size-md': 'calc(var(--size-md-control-height) - var(--space-2))',
|
|
|
'trigger-swatch-size-lg': 'calc(var(--size-lg-control-height) - var(--space-2))',
|
|
|
'trigger-swatch-radius': 'var(--color-picker-trigger-radius)',
|
|
|
|
|
|
// 2D area
|
|
|
'area-height': '11rem',
|
|
|
'area-radius': 'var(--radius-md)',
|
|
|
'area-border-width': 'var(--border-width)',
|
|
|
'area-border': 'var(--color-border-subtle)',
|
|
|
'area-thumb-size': '16px',
|
|
|
'area-thumb-radius': 'var(--radius-full)',
|
|
|
'area-thumb-border-width': 'var(--border-width-medium)',
|
|
|
'area-thumb-border': 'var(--color-surface-default)',
|
|
|
'area-thumb-shadow':
|
|
|
'0 0 0 1px color-mix(in srgb, var(--color-neutral-contrast) 45%, transparent), 0 1px 2px color-mix(in srgb, var(--color-neutral-contrast) 20%, transparent)',
|
|
|
|
|
|
// Channel sliders consume the generic `--slider-*` recipe tokens.
|
|
|
// Only the picker-specific decoration (gradient on track) lives
|
|
|
// in the picker CSS; sizes, shadows and radii come from the
|
|
|
// slider recipe so they stay in lockstep with the standalone
|
|
|
// Slider and the time-picker sliders.
|
|
|
|
|
|
// Transparency checker (alpha background)
|
|
|
'transparency-cell': 'var(--color-content-muted)',
|
|
|
'transparency-cell-alt': 'var(--color-surface-default)',
|
|
|
|
|
|
// Swatches
|
|
|
'swatch-size-sm': '1.25rem',
|
|
|
'swatch-size-md': '1.5rem',
|
|
|
'swatch-size-lg': '1.75rem',
|
|
|
'swatch-radius': 'var(--radius-sm)',
|
|
|
'swatch-border': 'var(--color-border-subtle)',
|
|
|
'swatch-checked-ring': 'var(--color-primary-solid)',
|
|
|
'swatch-gap': 'var(--space-1-5)',
|
|
|
|
|
|
// Eye-dropper button
|
|
|
'eye-dropper-size': 'var(--size-sm-control-height)',
|
|
|
'eye-dropper-radius': 'var(--radius-md)',
|
|
|
'eye-dropper-bg': 'transparent',
|
|
|
'eye-dropper-fg': 'var(--color-content-secondary)',
|
|
|
'eye-dropper-hover-fg': 'var(--color-content-primary)',
|
|
|
|
|
|
// Hue spectrum — drives the hue ring + saturation overlay. These
|
|
|
// sit in a sidecar file (color-picker-spectrum.css) so the raw
|
|
|
// hsl literals stay out of the main recipe; declaring them here
|
|
|
// keeps the recipe contract whole.
|
|
|
'hue-0': 'hsl(0, 100%, 50%)',
|
|
|
'hue-60': 'hsl(60, 100%, 50%)',
|
|
|
'hue-120': 'hsl(120, 100%, 50%)',
|
|
|
'hue-180': 'hsl(180, 100%, 50%)',
|
|
|
'hue-240': 'hsl(240, 100%, 50%)',
|
|
|
'hue-300': 'hsl(300, 100%, 50%)',
|
|
|
'hue-360': 'hsl(360, 100%, 50%)',
|
|
|
'saturation-floor': 'hsl(0, 0%, 50%)',
|
|
|
|
|
|
// Misc
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
|
|
|
// Accent palette forward (THM-2) — `color` tints the trigger focus
|
|
|
// border and the portaled content's focus rings. The content portals
|
|
|
// OUTSIDE the root, so both parts stamp `data-color` themselves and the
|
|
|
// forward targets them; roles + 33 scales + custom route through the
|
|
|
// shared layer. Host = primary (the previous hard-coded accent).
|
|
|
'_palette-border': {
|
|
|
parts: ['trigger', 'content'],
|
|
|
declarations: [{ value: 'var(--color-primary-border)', scope: 'host' }]
|
|
|
}
|
|
|
},
|
|
|
button: {
|
|
|
// Size tokens
|
|
|
'height-xs': 'var(--size-xs-control-height)',
|
|
|
'height-sm': 'var(--size-sm-control-height)',
|
|
|
'height-md': 'var(--size-md-control-height)',
|
|
|
'height-lg': 'var(--size-lg-control-height)',
|
|
|
'height-xl': 'var(--size-xl-control-height)',
|
|
|
'padding-inline-xs': 'var(--space-2)',
|
|
|
'padding-inline-sm': 'var(--space-3)',
|
|
|
'padding-inline-md': 'var(--space-4)',
|
|
|
'padding-inline-lg': 'var(--space-5)',
|
|
|
'padding-inline-xl': 'var(--space-6)',
|
|
|
'gap-xs': 'var(--space-1)',
|
|
|
'gap-sm': 'var(--space-1-5)',
|
|
|
'gap-md': 'var(--space-2)',
|
|
|
'gap-lg': 'var(--space-2-5)',
|
|
|
'gap-xl': 'var(--space-3)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'letter-spacing': 'var(--tracking-normal)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'font-weight-xs': 'var(--font-weight-medium)',
|
|
|
'font-weight-sm': 'var(--font-weight-medium)',
|
|
|
'font-weight-md': 'var(--font-weight-medium)',
|
|
|
'font-weight-lg': 'var(--font-weight-medium)',
|
|
|
'font-weight-xl': 'var(--font-weight-medium)',
|
|
|
// Icon follows the button's font-size 1:1 (canonical icon↔type correlation):
|
|
|
// the font now scales 1:1 with the size (no longer compact), so the icon
|
|
|
// matches at the same step. The parallel icon scale preserves icon ≈ font+2
|
|
|
// in the body sizes (md: font 16 → icon 18). Density-scaled; never hardcoded px.
|
|
|
'icon-size-xs': 'var(--size-xs-icon-size)',
|
|
|
'icon-size-sm': 'var(--size-sm-icon-size)',
|
|
|
'icon-size-md': 'var(--size-md-icon-size)',
|
|
|
'icon-size-lg': 'var(--size-lg-icon-size)',
|
|
|
'icon-size-xl': 'var(--size-xl-icon-size)',
|
|
|
'radius-sm': 'var(--radius-sm)',
|
|
|
'radius-md': 'var(--radius-md)',
|
|
|
'radius-lg': 'var(--radius-lg)',
|
|
|
'radius-xl': 'var(--radius-xl)',
|
|
|
// The pill step of the SAME scale: `data-rounded='full'` read the global
|
|
|
// primitive straight, so the one shape a theme is most likely to retune was
|
|
|
// the only step outside the contract.
|
|
|
'radius-full': 'var(--radius-full)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
shadow: 'none',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
// Per-color palettes — 7 slots per color, uniform across all 8: track /
|
|
|
// element / border / solid / solid-hover / text / contrast. soft + surface
|
|
|
// + outline variants read track/border, so every color must define them
|
|
|
// (was only fulfill/loss — the others rendered with no bg/border).
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-element': 'var(--color-primary-element)',
|
|
|
'primary-border': 'var(--color-primary-border)',
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-solid-hover': 'var(--color-primary-solid-hover)',
|
|
|
'primary-text': 'var(--color-primary-text)',
|
|
|
'primary-contrast': 'var(--color-primary-contrast)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-element': 'var(--color-secondary-element)',
|
|
|
'secondary-border': 'var(--color-secondary-border)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
|
|
|
'secondary-text': 'var(--color-secondary-text)',
|
|
|
'secondary-contrast': 'var(--color-secondary-contrast)',
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-element': 'var(--color-neutral-element)',
|
|
|
'neutral-border': 'var(--color-neutral-border)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
|
|
|
'neutral-text': 'var(--color-neutral-text)',
|
|
|
'neutral-contrast': 'var(--color-neutral-contrast)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-element': 'var(--color-affirm-element)',
|
|
|
'affirm-border': 'var(--color-affirm-border)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
|
|
|
'affirm-text': 'var(--color-affirm-text)',
|
|
|
'affirm-contrast': 'var(--color-affirm-contrast)',
|
|
|
'fulfill-track': 'var(--color-fulfill-track)',
|
|
|
'fulfill-element': 'var(--color-fulfill-element)',
|
|
|
'fulfill-border': 'var(--color-fulfill-border)',
|
|
|
'fulfill-solid': 'var(--color-fulfill-solid)',
|
|
|
'fulfill-solid-hover': 'var(--color-fulfill-solid-hover)',
|
|
|
'fulfill-text': 'var(--color-fulfill-text)',
|
|
|
'fulfill-contrast': 'var(--color-fulfill-contrast)',
|
|
|
'risk-track': 'var(--color-risk-track)',
|
|
|
'risk-element': 'var(--color-risk-element)',
|
|
|
'risk-border': 'var(--color-risk-border)',
|
|
|
'risk-solid': 'var(--color-risk-solid)',
|
|
|
'risk-solid-hover': 'var(--color-risk-solid-hover)',
|
|
|
'risk-text': 'var(--color-risk-text)',
|
|
|
'risk-contrast': 'var(--color-risk-contrast)',
|
|
|
'threat-track': 'var(--color-threat-track)',
|
|
|
'threat-element': 'var(--color-threat-element)',
|
|
|
'threat-border': 'var(--color-threat-border)',
|
|
|
'threat-solid': 'var(--color-threat-solid)',
|
|
|
'threat-solid-hover': 'var(--color-threat-solid-hover)',
|
|
|
'threat-text': 'var(--color-threat-text)',
|
|
|
'threat-contrast': 'var(--color-threat-contrast)',
|
|
|
'loss-track': 'var(--color-loss-track)',
|
|
|
'loss-element': 'var(--color-loss-element)',
|
|
|
'loss-border': 'var(--color-loss-border)',
|
|
|
'loss-solid': 'var(--color-loss-solid)',
|
|
|
'loss-solid-hover': 'var(--color-loss-solid-hover)',
|
|
|
'loss-text': 'var(--color-loss-text)',
|
|
|
'loss-contrast': 'var(--color-loss-contrast)',
|
|
|
// Active palette — 8 custom properties, each declared with 8
|
|
|
// declarations (host default = primary, plus per-color overrides
|
|
|
// for the 7 other supported values). Single source of truth via
|
|
|
// TSC v2 `declarations[]`. Replaces the per-color blocks that
|
|
|
// used to live in `button.css`.
|
|
|
'palette-track': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--button-primary-track)', scope: 'host' },
|
|
|
{ value: 'var(--button-secondary-track)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--button-neutral-track)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--button-affirm-track)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--button-fulfill-track)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--button-risk-track)', scope: 'color:risk' },
|
|
|
{ value: 'var(--button-threat-track)', scope: 'color:threat' },
|
|
|
{ value: 'var(--button-loss-track)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'palette-element': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--button-primary-element)', scope: 'host' },
|
|
|
{ value: 'var(--button-secondary-element)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--button-neutral-element)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--button-affirm-element)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--button-fulfill-element)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--button-risk-element)', scope: 'color:risk' },
|
|
|
{ value: 'var(--button-threat-element)', scope: 'color:threat' },
|
|
|
{ value: 'var(--button-loss-element)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'palette-border': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--button-primary-border)', scope: 'host' },
|
|
|
{ value: 'var(--button-secondary-border)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--button-neutral-border)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--button-affirm-border)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--button-fulfill-border)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--button-risk-border)', scope: 'color:risk' },
|
|
|
{ value: 'var(--button-threat-border)', scope: 'color:threat' },
|
|
|
{ value: 'var(--button-loss-border)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--button-primary-solid)', scope: 'host' },
|
|
|
{ value: 'var(--button-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--button-neutral-solid)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--button-affirm-solid)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--button-fulfill-solid)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--button-risk-solid)', scope: 'color:risk' },
|
|
|
{ value: 'var(--button-threat-solid)', scope: 'color:threat' },
|
|
|
{ value: 'var(--button-loss-solid)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'palette-solid-hover': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--button-primary-solid-hover)', scope: 'host' },
|
|
|
{ value: 'var(--button-secondary-solid-hover)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--button-neutral-solid-hover)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--button-affirm-solid-hover)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--button-fulfill-solid-hover)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--button-risk-solid-hover)', scope: 'color:risk' },
|
|
|
{ value: 'var(--button-threat-solid-hover)', scope: 'color:threat' },
|
|
|
{ value: 'var(--button-loss-solid-hover)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'palette-text': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--button-primary-text)', scope: 'host' },
|
|
|
{ value: 'var(--button-secondary-text)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--button-neutral-text)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--button-affirm-text)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--button-fulfill-text)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--button-risk-text)', scope: 'color:risk' },
|
|
|
{ value: 'var(--button-threat-text)', scope: 'color:threat' },
|
|
|
{ value: 'var(--button-loss-text)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'palette-contrast': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--button-primary-contrast)', scope: 'host' },
|
|
|
{ value: 'var(--button-secondary-contrast)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--button-neutral-contrast)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--button-affirm-contrast)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--button-fulfill-contrast)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--button-risk-contrast)', scope: 'color:risk' },
|
|
|
{ value: 'var(--button-threat-contrast)', scope: 'color:threat' },
|
|
|
{ value: 'var(--button-loss-contrast)', scope: 'color:loss' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
'month-grid': {
|
|
|
// Palette forward (THM-2): host default = primary; the shared `--palette-*`
|
|
|
// layer routes roles + 33 scales + custom into `--_month-grid-palette-*`
|
|
|
// (was a per-role cascade in month-grid.css). MonthGrid reuses calendar
|
|
|
// tokens for chrome but owns its selected-cell palette.
|
|
|
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
|
|
|
'_palette-solid-hover': { value: 'var(--color-primary-solid-hover)', scope: 'host' },
|
|
|
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
|
|
|
'_palette-border': { value: 'var(--color-primary-border)', scope: 'host' },
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
|
|
|
},
|
|
|
'year-grid': {
|
|
|
// Palette forward (THM-2) — same as month-grid: host default = primary,
|
|
|
// shared layer routes roles + 33 scales + custom into `--_year-grid-palette-*`.
|
|
|
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
|
|
|
'_palette-solid-hover': { value: 'var(--color-primary-solid-hover)', scope: 'host' },
|
|
|
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
|
|
|
'_palette-border': { value: 'var(--color-primary-border)', scope: 'host' },
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
|
|
|
},
|
|
|
'range-calendar': {
|
|
|
// Palette forward (THM-2): the END accent + `--calendar-range-end-*` bind to
|
|
|
// `--_range-calendar-palette-*`; host default = primary, shared layer routes
|
|
|
// roles + 33 scales + custom. The affirm/fulfill START-endpoint swap stays a
|
|
|
// per-role rule in range-calendar.css (it's a distinctness refinement, not
|
|
|
// palette). (Was a per-role cascade over `--_calendar-accent-*`.)
|
|
|
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
|
|
|
'_palette-solid-hover': { value: 'var(--color-primary-solid-hover)', scope: 'host' },
|
|
|
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
|
|
|
'_palette-border': { value: 'var(--color-primary-border)', scope: 'host' },
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
|
|
|
},
|
|
|
// ── List / data-grid family — the shared 2-slot selection palette ──────────
|
|
|
// Uniform pattern: `--_{c}-palette-solid` (accent: sort/active indicator) +
|
|
|
// `--_{c}-palette-element` (soft: selected-row tint). Host default = primary;
|
|
|
// the shared layer routes roles + 33 scales + custom. Was a per-role cascade
|
|
|
// in each component's CSS (`--_{c}-accent` / `--_{c}-accent-soft`). The `-root`
|
|
|
// components stamp `data-color` on `[data-{c}-root]` (not the bare provider),
|
|
|
// so their forward targets that part via `parts: ['root']`. drag-drop / combobox
|
|
|
// / timeline already own a recipe → their palette slots live in those entries.
|
|
|
table: {
|
|
|
// Row height per size - 1:1 with the bundle, measured. NOTE the name: the
|
|
|
// value is the ROW's height (the centred content of a cell), not the
|
|
|
// root's; the old private was called `root-*` and named the wrong thing.
|
|
|
'row-height-xs': 'var(--size-xs-control-height)',
|
|
|
'row-height-sm': 'var(--size-sm-control-height)',
|
|
|
'row-height-md': 'var(--size-md-control-height)',
|
|
|
'row-height-lg': 'var(--size-lg-control-height)',
|
|
|
'row-height-xl': 'var(--size-xl-control-height)',
|
|
|
|
|
|
// Cell padding - the table keeps its OWN rhythm, denser than the control
|
|
|
// bundle at xs and looser from md up. Verbatim on purpose: a data grid
|
|
|
// packs rows tighter than a control stack, and forcing the bundle here
|
|
|
// would change the default. (Spacing is outside the bundle guard, which
|
|
|
// only governs control-height / font-size / icon-size.)
|
|
|
'cell-padding-inline-xs': 'var(--space-2)',
|
|
|
'cell-padding-inline-sm': 'var(--space-2)',
|
|
|
'cell-padding-inline-md': 'var(--space-3)',
|
|
|
'cell-padding-inline-lg': 'var(--space-4)',
|
|
|
'cell-padding-inline-xl': 'var(--space-5)',
|
|
|
'cell-padding-block-xs': 'var(--space-1)',
|
|
|
'cell-padding-block-sm': 'var(--space-1)',
|
|
|
'cell-padding-block-md': 'var(--space-2)',
|
|
|
'cell-padding-block-lg': 'var(--space-3)',
|
|
|
'cell-padding-block-xl': 'var(--space-3)',
|
|
|
|
|
|
// Type. The table reads ONE STEP BELOW the 1:1 from md up - a grid is read
|
|
|
// in bulk, not glanced at. The coordinate names that step explicitly, so
|
|
|
// the deviation is legible instead of hidden in a raw primitive.
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-sm-font-size)',
|
|
|
'font-size-lg': 'var(--size-md-font-size)',
|
|
|
'font-size-xl': 'var(--size-lg-font-size)',
|
|
|
|
|
|
// The RESOLVED names. `data-size` is stamped on `[data-table-root]`, so the
|
|
|
// per-size cascade targets that part, not the host.
|
|
|
'row-height': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--table-row-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--table-row-height-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--table-row-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--table-row-height-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--table-row-height-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'cell-padding-inline': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--table-cell-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--table-cell-padding-inline-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--table-cell-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--table-cell-padding-inline-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--table-cell-padding-inline-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'cell-padding-block': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--table-cell-padding-block-md)', scope: 'host' },
|
|
|
{ value: 'var(--table-cell-padding-block-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--table-cell-padding-block-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--table-cell-padding-block-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--table-cell-padding-block-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'font-size': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--table-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--table-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--table-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--table-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--table-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// Chassis. No `root-` prefix: the wrapper IS the component, so the token is
|
|
|
// named by its axis - recipe-contract §1 does not repeat the part when the
|
|
|
// part is the component itself.
|
|
|
radius: 'var(--radius-md)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
divider: 'var(--color-border-default)',
|
|
|
|
|
|
// Variants. `surface` carries the card chrome; `outline` keeps the border
|
|
|
// over nothing; `ghost` has no chrome at all.
|
|
|
'surface-bg': 'var(--color-surface-default)',
|
|
|
'row-bg': 'var(--color-surface-default)',
|
|
|
|
|
|
// Header / footer share one surface.
|
|
|
'header-bg': 'var(--color-surface-muted)',
|
|
|
'header-fg': 'var(--color-content-primary)',
|
|
|
'header-font-weight': 'var(--font-weight-semibold)',
|
|
|
'header-z': '2',
|
|
|
|
|
|
// Row states. The striped and hover tints are the table's own: the row's
|
|
|
// `archetype: 'item'` veil is currently cancelled by the row background
|
|
|
// (see README - the `hoverable` prop depends on it).
|
|
|
'striped-row-bg': 'var(--color-surface-muted)',
|
|
|
'hover-row-bg': 'var(--color-surface-overlay)',
|
|
|
|
|
|
// Row detail (disclosure).
|
|
|
'row-detail-bg': 'var(--color-surface-muted)',
|
|
|
'row-detail-fg': 'var(--color-content-secondary)',
|
|
|
'row-detail-padding-block': 'var(--space-3)',
|
|
|
|
|
|
// Sort trigger + its indicator.
|
|
|
'sort-trigger-gap': 'var(--space-1)',
|
|
|
'sort-trigger-radius': 'var(--radius-sm)',
|
|
|
'sort-indicator-size': '1em',
|
|
|
'sort-indicator-opacity': 'var(--opacity-disabled)',
|
|
|
|
|
|
'_palette-solid': {
|
|
|
parts: ['root'],
|
|
|
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-element': {
|
|
|
parts: ['root'],
|
|
|
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
|
|
|
}
|
|
|
},
|
|
|
// tree-grid / tree-view only tint the SELECTED row (element); their `accent`
|
|
|
// (solid) token had no consumer, so they declare only `_palette-element`.
|
|
|
'tree-grid': {
|
|
|
// ── Per-size coordinates (xs..xl) ──
|
|
|
// Row height is 1:1 with the bundle. Padding and indent are the grid's OWN
|
|
|
// rhythm: denser than the control bundle at xs, looser from md up, because
|
|
|
// a tree is read in bulk. Spacing sits outside the bundle guard, which
|
|
|
// governs control-height / font-size / icon-size only.
|
|
|
'row-height-xs': 'var(--size-xs-control-height)',
|
|
|
'row-height-sm': 'var(--size-sm-control-height)',
|
|
|
'row-height-md': 'var(--size-md-control-height)',
|
|
|
'row-height-lg': 'var(--size-lg-control-height)',
|
|
|
'row-height-xl': 'var(--size-xl-control-height)',
|
|
|
'row-padding-block-xs': 'var(--space-1)',
|
|
|
'row-padding-block-sm': 'var(--space-1)',
|
|
|
'row-padding-block-md': 'var(--space-2)',
|
|
|
'row-padding-block-lg': 'var(--space-3)',
|
|
|
'row-padding-block-xl': 'var(--space-3)',
|
|
|
'row-padding-inline-xs': 'var(--space-2)',
|
|
|
'row-padding-inline-sm': 'var(--space-2)',
|
|
|
'row-padding-inline-md': 'var(--space-3)',
|
|
|
'row-padding-inline-lg': 'var(--space-4)',
|
|
|
'row-padding-inline-xl': 'var(--space-5)',
|
|
|
// The depth step. It is the grid's own axis - no bundle coordinate means
|
|
|
// "one level of nesting".
|
|
|
'indent-xs': 'var(--space-3)',
|
|
|
'indent-sm': 'var(--space-3)',
|
|
|
'indent-md': 'var(--space-4)',
|
|
|
'indent-lg': 'var(--space-5)',
|
|
|
'indent-xl': 'var(--space-6)',
|
|
|
// Type: like `table`, the grid reads ONE STEP BELOW the 1:1 from md up.
|
|
|
// The coordinate names that step instead of hiding it in a primitive.
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-sm-font-size)',
|
|
|
'font-size-lg': 'var(--size-md-font-size)',
|
|
|
'font-size-xl': 'var(--size-lg-font-size)',
|
|
|
|
|
|
// The RESOLVED names. `data-size` is stamped on `[data-tree-grid-root]`,
|
|
|
// so the per-size cascade targets that part (precedent: proof-of-human).
|
|
|
'row-height': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--tree-grid-row-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--tree-grid-row-height-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--tree-grid-row-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--tree-grid-row-height-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--tree-grid-row-height-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'row-padding-block': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--tree-grid-row-padding-block-md)', scope: 'host' },
|
|
|
{ value: 'var(--tree-grid-row-padding-block-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--tree-grid-row-padding-block-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--tree-grid-row-padding-block-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--tree-grid-row-padding-block-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'row-padding-inline': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--tree-grid-row-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--tree-grid-row-padding-inline-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--tree-grid-row-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--tree-grid-row-padding-inline-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--tree-grid-row-padding-inline-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
indent: {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--tree-grid-indent-md)', scope: 'host' },
|
|
|
{ value: 'var(--tree-grid-indent-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--tree-grid-indent-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--tree-grid-indent-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--tree-grid-indent-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'font-size': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--tree-grid-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--tree-grid-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--tree-grid-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--tree-grid-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--tree-grid-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// ── Chassis ── no `root-` prefix: the wrapper IS the component.
|
|
|
radius: 'var(--radius-md)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
divider: 'var(--color-border-default)',
|
|
|
'surface-bg': 'var(--color-surface-default)',
|
|
|
'row-bg': 'var(--color-surface-default)',
|
|
|
|
|
|
// Header.
|
|
|
'header-bg': 'var(--color-surface-muted)',
|
|
|
'header-font-weight': 'var(--font-weight-semibold)',
|
|
|
'header-z': '2',
|
|
|
|
|
|
// Row states. See the README: the striped tint outweighs the hover, so on
|
|
|
// a banded row the hover is inert - a pre-existing defect, measured.
|
|
|
'striped-row-bg': 'var(--color-surface-muted)',
|
|
|
'hover-row-bg': 'var(--color-surface-overlay)',
|
|
|
'disabled-row-fg': 'var(--color-content-muted)',
|
|
|
|
|
|
// ── Indent guides ──
|
|
|
// ONE colour. The five stacked gradients that draw one line per ancestor
|
|
|
// level are CONSTRUCTION and stay in the recipe - the same split as
|
|
|
// gradient-builder's checkerboard: the colour is the knob, the pattern is
|
|
|
// the recipe's.
|
|
|
'guide-fg': 'var(--color-border-default)',
|
|
|
'guide-width': '1px',
|
|
|
|
|
|
// Branchless rows keep a placeholder so labels align with their siblings.
|
|
|
'branch-placeholder-size': '1em',
|
|
|
|
|
|
'_palette-element': {
|
|
|
parts: ['root'],
|
|
|
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
|
|
|
}
|
|
|
},
|
|
|
'tree-view': {
|
|
|
// ── Per-size coordinates (xs..xl) ──
|
|
|
// Row height is 1:1 with the bundle. Padding and indent are the tree's OWN
|
|
|
// rhythm (spacing sits outside the bundle guard, which governs
|
|
|
// control-height / font-size / icon-size only) — same split as `tree-grid`.
|
|
|
'row-height-xs': 'var(--size-xs-control-height)',
|
|
|
'row-height-sm': 'var(--size-sm-control-height)',
|
|
|
'row-height-md': 'var(--size-md-control-height)',
|
|
|
'row-height-lg': 'var(--size-lg-control-height)',
|
|
|
'row-height-xl': 'var(--size-xl-control-height)',
|
|
|
// xs collapses the block padding to zero: at that size the row height
|
|
|
// alone carries the rhythm.
|
|
|
'row-padding-block-xs': '0',
|
|
|
'row-padding-block-sm': 'var(--space-1)',
|
|
|
'row-padding-block-md': 'var(--space-1)',
|
|
|
'row-padding-block-lg': 'var(--space-1)',
|
|
|
'row-padding-block-xl': 'var(--space-1)',
|
|
|
'row-padding-inline-xs': 'var(--space-1)',
|
|
|
'row-padding-inline-sm': 'var(--space-2)',
|
|
|
'row-padding-inline-md': 'var(--space-2)',
|
|
|
'row-padding-inline-lg': 'var(--space-3)',
|
|
|
'row-padding-inline-xl': 'var(--space-4)',
|
|
|
// The depth step — the tree's own axis, multiplied by soma's
|
|
|
// `--tree-depth`. It is NOT the row's inline padding: the two add up in
|
|
|
// one calc, which is why the generated proposal fused them into a single
|
|
|
// name and why they must stay apart.
|
|
|
'indent-xs': 'var(--space-3)',
|
|
|
'indent-sm': 'var(--space-3)',
|
|
|
'indent-md': 'var(--space-4)',
|
|
|
'indent-lg': 'var(--space-5)',
|
|
|
'indent-xl': 'var(--space-6)',
|
|
|
// Type: like `table` and `tree-grid`, one step below the 1:1 from lg up.
|
|
|
// The coordinate names that step instead of hiding it in a primitive.
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-sm-font-size)',
|
|
|
'font-size-lg': 'var(--size-md-font-size)',
|
|
|
'font-size-xl': 'var(--size-lg-font-size)',
|
|
|
|
|
|
// The RESOLVED names. `data-size` is stamped on `[data-tree-view-root]` —
|
|
|
// an eidos wrapper, not a morfo part — so the per-size cascade targets
|
|
|
// that part (precedent: tree-grid, proof-of-human).
|
|
|
'row-height': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--tree-view-row-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--tree-view-row-height-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--tree-view-row-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--tree-view-row-height-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--tree-view-row-height-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'row-padding-block': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--tree-view-row-padding-block-md)', scope: 'host' },
|
|
|
{ value: 'var(--tree-view-row-padding-block-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--tree-view-row-padding-block-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--tree-view-row-padding-block-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--tree-view-row-padding-block-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'row-padding-inline': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--tree-view-row-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--tree-view-row-padding-inline-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--tree-view-row-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--tree-view-row-padding-inline-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--tree-view-row-padding-inline-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
indent: {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--tree-view-indent-md)', scope: 'host' },
|
|
|
{ value: 'var(--tree-view-indent-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--tree-view-indent-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--tree-view-indent-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--tree-view-indent-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'font-size': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--tree-view-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--tree-view-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--tree-view-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--tree-view-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--tree-view-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// ── Chassis ── no `root-` prefix: the wrapper IS the component.
|
|
|
radius: 'var(--radius-md)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
// Shared by the surface and outline variants. Ghost's `transparent` is
|
|
|
// variant IDENTITY, not a knob, and stays literal.
|
|
|
border: 'var(--color-border-default)',
|
|
|
'surface-bg': 'var(--color-surface-default)',
|
|
|
// Surface and outline pad alike; ghost's `0` is identity.
|
|
|
padding: 'var(--space-2)',
|
|
|
|
|
|
// ── Rows ── one rule paints BOTH `branch-control` and `item`.
|
|
|
'row-gap': 'var(--space-2)',
|
|
|
'row-radius': 'var(--radius-sm)',
|
|
|
// Coined on purpose, unlike grid-list's: `branch-control` carries NO
|
|
|
// archetype, so on that node the recipe's plane is the ONLY paint (veil
|
|
|
// measured absent) and this token is the hover's whole theming surface.
|
|
|
// On `[data-tree-view-item]` the system veil stacks on top; see the
|
|
|
// README and next-features §12 for the branch-veil spill.
|
|
|
'hover-row-bg': 'var(--color-surface-overlay)',
|
|
|
'disabled-row-fg': 'var(--color-content-muted)',
|
|
|
|
|
|
// ── BranchIndicator (chevron) ──
|
|
|
'branch-indicator-size': '1em',
|
|
|
'branch-indicator-fg': 'var(--color-content-secondary)',
|
|
|
|
|
|
// ── Indent guides ──
|
|
|
// The generated proposal called these `root-width` / `root-bg`: they are
|
|
|
// neither. The painted node is the branch's `::before`, a vertical line.
|
|
|
// Same names as tree-grid's, for the same thing.
|
|
|
'guide-width': '1px',
|
|
|
'guide-fg': 'var(--color-border-default)',
|
|
|
|
|
|
'_palette-element': {
|
|
|
parts: ['root'],
|
|
|
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
|
|
|
}
|
|
|
},
|
|
|
// virtual-list / virtual-grid: their accent cascade was declaration-only (no CSS
|
|
|
// consumer) — pure virtualized containers with no colour-driven visual. The type
|
|
|
// stays open (guard/uniformity) + the wrapper stamps a harmless `data-color`, but
|
|
|
// there is nothing to forward, so NO recipe here (avoids an orphan palette token).
|
|
|
feed: {
|
|
|
// ── Per-size coordinates (xs..xl) ──
|
|
|
// The feed types BELOW the 1:1 from md up (`font-size-md` reads the sm
|
|
|
// coordinate): a stream is read in bulk, like `table` and `tree-grid`.
|
|
|
// The coordinate names that step instead of hiding it in a primitive.
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-sm-font-size)',
|
|
|
'font-size-lg': 'var(--size-md-font-size)',
|
|
|
'font-size-xl': 'var(--size-lg-font-size)',
|
|
|
'article-padding-block-xs': 'var(--space-2)',
|
|
|
'article-padding-block-sm': 'var(--space-2)',
|
|
|
'article-padding-block-md': 'var(--space-3)',
|
|
|
'article-padding-block-lg': 'var(--space-4)',
|
|
|
'article-padding-block-xl': 'var(--space-5)',
|
|
|
'article-padding-inline-xs': 'var(--space-3)',
|
|
|
'article-padding-inline-sm': 'var(--space-3)',
|
|
|
'article-padding-inline-md': 'var(--space-4)',
|
|
|
'article-padding-inline-lg': 'var(--space-5)',
|
|
|
'article-padding-inline-xl': 'var(--space-6)',
|
|
|
// How far a nested thread steps in from its parent.
|
|
|
'thread-indent-xs': 'var(--space-4)',
|
|
|
'thread-indent-sm': 'var(--space-4)',
|
|
|
'thread-indent-md': 'var(--space-5)',
|
|
|
'thread-indent-lg': 'var(--space-6)',
|
|
|
'thread-indent-xl': 'var(--space-7)',
|
|
|
|
|
|
// The RESOLVED names. `data-size` is stamped on `[data-feed-root]`.
|
|
|
'font-size': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--feed-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--feed-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--feed-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--feed-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--feed-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'article-padding-block': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--feed-article-padding-block-md)', scope: 'host' },
|
|
|
{ value: 'var(--feed-article-padding-block-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--feed-article-padding-block-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--feed-article-padding-block-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--feed-article-padding-block-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'article-padding-inline': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--feed-article-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--feed-article-padding-inline-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--feed-article-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--feed-article-padding-inline-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--feed-article-padding-inline-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'thread-indent': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--feed-thread-indent-md)', scope: 'host' },
|
|
|
{ value: 'var(--feed-thread-indent-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--feed-thread-indent-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--feed-thread-indent-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--feed-thread-indent-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// ── Chassis ──
|
|
|
radius: 'var(--radius-md)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
divider: 'var(--color-border-default)',
|
|
|
gap: 'var(--space-2)',
|
|
|
'surface-bg': 'var(--color-surface-default)',
|
|
|
|
|
|
// ── Article ──
|
|
|
'article-gap': 'var(--space-1)',
|
|
|
'article-title-fg': 'var(--color-content-primary)',
|
|
|
'article-title-font-weight': 'var(--font-weight-semibold)',
|
|
|
// The title steps by HEADING LEVEL, not by the component's size - the
|
|
|
// audit read these four as a size collision, and they are `aria-level`
|
|
|
// 3..6. A deeper heading is smaller because it is deeper.
|
|
|
'article-title-font-size-3': 'var(--size-md-font-size)',
|
|
|
'article-title-font-size-4': 'var(--size-sm-font-size)',
|
|
|
'article-title-font-size-5': 'var(--size-sm-font-size)',
|
|
|
'article-title-font-size-6': 'var(--size-xs-font-size)',
|
|
|
'article-description-fg': 'var(--color-content-secondary)',
|
|
|
|
|
|
// ── Thread ──
|
|
|
'thread-margin-block-start': 'var(--space-2)',
|
|
|
'thread-rail-width': 'var(--border-width-medium)',
|
|
|
|
|
|
// ── Sentinel ──
|
|
|
// TWO different measures, which the audit flagged as one ⚠: the probe row
|
|
|
// keeps a minimum height so the observer has something to intersect, and
|
|
|
// the spinner inside it has its own diameter.
|
|
|
'sentinel-min-block-size': 'var(--space-6)',
|
|
|
'sentinel-fg': 'var(--color-content-secondary)',
|
|
|
'sentinel-font-size': 'var(--size-xs-font-size)',
|
|
|
'sentinel-spinner-size': '0.75rem',
|
|
|
'sentinel-spinner-width': 'var(--border-width-medium)',
|
|
|
'sentinel-spinner-gap': 'var(--space-2)',
|
|
|
'sentinel-spinner-duration': '0.7s',
|
|
|
'sentinel-spinner-reduced-duration': '4s',
|
|
|
|
|
|
'_palette-solid': {
|
|
|
parts: ['root'],
|
|
|
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-element': {
|
|
|
parts: ['root'],
|
|
|
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
|
|
|
}
|
|
|
},
|
|
|
'grid-list': {
|
|
|
// ── Per-size coordinates (xs..xl) ──
|
|
|
// Row height is 1:1 with the bundle (`--size-{k}-control-height` IS
|
|
|
// `--control-height-{k}`, verified in generated/base.css). Padding keeps
|
|
|
// the list's OWN rhythm — spacing sits outside the bundle guard, which
|
|
|
// governs control-height / font-size / icon-size only.
|
|
|
'row-height-xs': 'var(--size-xs-control-height)',
|
|
|
'row-height-sm': 'var(--size-sm-control-height)',
|
|
|
'row-height-md': 'var(--size-md-control-height)',
|
|
|
'row-height-lg': 'var(--size-lg-control-height)',
|
|
|
'row-height-xl': 'var(--size-xl-control-height)',
|
|
|
'row-padding-inline-xs': 'var(--space-2)',
|
|
|
'row-padding-inline-sm': 'var(--space-2)',
|
|
|
'row-padding-inline-md': 'var(--space-3)',
|
|
|
'row-padding-inline-lg': 'var(--space-4)',
|
|
|
'row-padding-inline-xl': 'var(--space-5)',
|
|
|
// Type: like `table` and `tree-grid`, the list reads ONE STEP BELOW the
|
|
|
// 1:1 from md up — a list of rows is read in bulk, not glanced at. The
|
|
|
// coordinate names that step instead of hiding it in a raw primitive.
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-sm-font-size)',
|
|
|
'font-size-lg': 'var(--size-md-font-size)',
|
|
|
'font-size-xl': 'var(--size-lg-font-size)',
|
|
|
|
|
|
// The RESOLVED names — the only ones the recipe reads. `data-size` is
|
|
|
// stamped on the PROVIDER (the eidos wrapper's only root), so the
|
|
|
// per-size cascade hangs from the host and needs no `parts` (precedent:
|
|
|
// command, not table/tree-grid — those stamp on a `root` part).
|
|
|
'row-height': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--grid-list-row-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--grid-list-row-height-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--grid-list-row-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--grid-list-row-height-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--grid-list-row-height-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'row-padding-inline': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--grid-list-row-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--grid-list-row-padding-inline-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--grid-list-row-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--grid-list-row-padding-inline-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--grid-list-row-padding-inline-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'font-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--grid-list-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--grid-list-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--grid-list-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--grid-list-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--grid-list-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// ── Chassis ── no `root-` prefix: the wrapper IS the component.
|
|
|
gap: '1px',
|
|
|
padding: 'var(--space-1)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-border-default)',
|
|
|
radius: 'var(--radius-md)',
|
|
|
bg: 'var(--color-surface-default)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'max-block': 'min(60vh, 22rem)',
|
|
|
'invalid-border': 'var(--color-threat-element)',
|
|
|
|
|
|
// ── Row ──
|
|
|
// NO `hover-row-bg`: the row carries `archetype: 'item'`, so the system
|
|
|
// state layer paints it (full specificity, by design) and this recipe's
|
|
|
// own plane loses. Measured 2026-08-21 — the hover paints TWICE on the
|
|
|
// same node. Coining a token here would bless a condemned duplicate
|
|
|
// (same call as listbox.highlighted). Registered in next-features §12.
|
|
|
'row-gap': 'var(--space-2)',
|
|
|
'row-padding-block': 'var(--space-1)',
|
|
|
'row-radius': 'var(--radius-sm)',
|
|
|
'row-fg': 'var(--color-content-primary)',
|
|
|
'disabled-row-fg': 'var(--color-content-muted)',
|
|
|
|
|
|
// ── SelectionCheckbox ──
|
|
|
// It MIRRORS the menu-indicator visual (empty outlined square → filled
|
|
|
// accent + glyph); it does not consume that layer — no selector of
|
|
|
// `lib/menu-indicator.css` targets grid-list. Its border width is the
|
|
|
// chassis' `border-width`: one border weight per component.
|
|
|
'selection-checkbox-size': '0.875rem',
|
|
|
'selection-checkbox-border': 'var(--color-border-default)',
|
|
|
'selection-checkbox-radius': 'var(--radius-sm)',
|
|
|
'checked-selection-checkbox-fg': 'var(--color-content-on-solid)',
|
|
|
|
|
|
// The accent the CHECKED checkbox fills with. The row's own
|
|
|
// `_palette-element` forward was retired 2026-08-21: its only consumer
|
|
|
// selected `[data-grid-list-row][data-selected]`, an attribute neither
|
|
|
// the morfo declares nor soma stamps (it stamps `data-state='selected'`
|
|
|
// + `aria-selected`), so the rule never matched and the forward fed
|
|
|
// nothing. A selected row reads through the archetype's neutral veil;
|
|
|
// repairing that moves pixels and awaits signature (next-features §13).
|
|
|
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' }
|
|
|
},
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// DateRangePicker — only the DECADE / MONTH view is its own invention; the
|
|
|
// calendar half borrows `--calendar-*` (that is the calendar-family decision
|
|
|
// in theming-audit §B, not this axis) and the field half borrows `--field-*`
|
|
|
// (the field-composition mandate). Theme-reach 2026-08-20.
|
|
|
'date-range-picker': {
|
|
|
'accent-border': 'var(--color-primary-border)',
|
|
|
'accent-track': 'var(--color-primary-track)',
|
|
|
'accent-text': 'var(--color-primary-text)',
|
|
|
'overview-gap': 'var(--space-3)',
|
|
|
'overview-width': 'min(20rem, 100%)',
|
|
|
'overview-header-gap': 'var(--space-2)',
|
|
|
'overview-heading-fg': 'var(--color-content-primary)',
|
|
|
'overview-heading-font-weight': 'var(--font-weight-semibold)',
|
|
|
'overview-nav-size': 'var(--size-sm-control-height)',
|
|
|
'overview-nav-radius': 'var(--radius-md)',
|
|
|
'overview-nav-fg': 'var(--color-content-primary)',
|
|
|
'overview-nav-font-size': 'var(--size-md-font-size)',
|
|
|
'overview-grid-gap': 'var(--space-2)',
|
|
|
'overview-cell-height': 'var(--size-md-control-height)',
|
|
|
'overview-cell-radius': 'var(--radius-md)',
|
|
|
'overview-cell-fg': 'var(--color-content-primary)',
|
|
|
'overview-cell-font-size': 'var(--size-sm-font-size)',
|
|
|
'in-range-overview-cell-bg': 'var(--color-primary-track)',
|
|
|
'in-range-overview-cell-fg': 'var(--color-primary-text)',
|
|
|
'endpoint-overview-cell-bg': 'var(--color-primary-solid)',
|
|
|
'endpoint-overview-cell-fg': 'var(--color-content-on-solid)',
|
|
|
'endpoint-overview-cell-border': 'var(--color-primary-border)'
|
|
|
},
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// TimeRangePicker — same split as `time-picker`: the two-column clock and
|
|
|
// the AM/PM strip are its own, the field-shaped half stays on `--field-*`
|
|
|
// (it composes Field). Theme-reach 2026-08-20.
|
|
|
'time-range-picker': {
|
|
|
'accent-border': 'var(--color-primary-border)',
|
|
|
'accent-track': 'var(--color-primary-track)',
|
|
|
'accent-text': 'var(--color-primary-text)',
|
|
|
'clock-column-gap': 'var(--space-10)',
|
|
|
'clock-width': '22rem',
|
|
|
'clock-fg': 'var(--color-content-primary)',
|
|
|
'clock-font-family': 'var(--style-label-font-family)',
|
|
|
'clock-font-size': 'var(--size-sm-font-size)',
|
|
|
'clock-line-height': 'var(--leading-ui)',
|
|
|
'clock-hand-hour': 'var(--color-primary-border)',
|
|
|
'clock-hand-minute': 'var(--color-secondary-border)',
|
|
|
'clock-hand-second': 'var(--color-neutral-border)',
|
|
|
'day-period-toggle-gap': 'var(--space-1)',
|
|
|
'day-period-toggle-radius': 'var(--radius-md)',
|
|
|
'day-period-toggle-bg': 'var(--color-surface-default)',
|
|
|
'day-period-toggle-border': 'var(--color-border-default)',
|
|
|
'day-period-item-min-width': '2.25rem',
|
|
|
'day-period-item-padding-inline': 'var(--space-2)',
|
|
|
'day-period-item-padding-block': 'var(--space-1)',
|
|
|
'day-period-item-font-size': 'var(--size-xs-font-size)',
|
|
|
'day-period-item-font-weight': 'var(--font-weight-semibold)',
|
|
|
'day-period-item-fg': 'var(--color-content-muted)'
|
|
|
},
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// TimePicker — the CLOCK face and the day-period strip, which are its own
|
|
|
// invention; the field-shaped half keeps consuming `--field-*` because it
|
|
|
// composes Field (the field-composition mandate, theming-audit §5.3-3), and
|
|
|
// the accent that used to hide behind a `--_time-field-*` prefix is public
|
|
|
// now. Theme-reach 2026-08-20.
|
|
|
'time-picker': {
|
|
|
'accent-border': 'var(--color-primary-border)',
|
|
|
'accent-track': 'var(--color-primary-track)',
|
|
|
'accent-text': 'var(--color-primary-text)',
|
|
|
gap: 'var(--space-1-5)',
|
|
|
'clock-gap': 'var(--space-2)',
|
|
|
'clock-width': '14rem',
|
|
|
'clock-fg': 'var(--color-content-primary)',
|
|
|
'clock-font-family': 'var(--style-label-font-family)',
|
|
|
'clock-font-size': 'var(--size-sm-font-size)',
|
|
|
'clock-line-height': 'var(--leading-ui)',
|
|
|
'clock-hand-hour': 'var(--color-primary-border)',
|
|
|
'clock-hand-minute': 'var(--color-secondary-border)',
|
|
|
'clock-hand-second': 'var(--color-neutral-border)',
|
|
|
'clock-thumb-shadow': 'var(--slider-thumb-shadow)',
|
|
|
'clock-thumb-ring': 'var(--color-surface-default)',
|
|
|
'trigger-radius': 'var(--radius-sm)'
|
|
|
},
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// NaturalTimePicker — the daylight band and its readout. The PANEL keeps
|
|
|
// borrowing `--popover-*`: its surface IS the canonical float (documented in
|
|
|
// the recipe header), so aliasing it under a private prefix would be the
|
|
|
// parallel vocabulary the contract forbids. The sky palette stays a set of
|
|
|
// annotated literals — the fixed-tone exception the recipe guard already
|
|
|
// carries (`FIXED_TONE_COMPONENTS`): a sky does not change with the theme.
|
|
|
// What IS the component's own chrome is here (theme-reach 2026-08-20).
|
|
|
'natural-time-picker': {
|
|
|
// Band geometry, derived from the control height like the recipe did.
|
|
|
'band-height-sm': 'calc(var(--size-sm-control-height) * 1.7 + var(--space-2))',
|
|
|
'band-height-md': 'calc(var(--size-md-control-height) * 1.7 + var(--space-2))',
|
|
|
'band-height-lg': 'calc(var(--size-lg-control-height) * 1.7 + var(--space-2))',
|
|
|
'knob-size-sm': 'calc(var(--size-sm-control-height) * 0.62)',
|
|
|
'knob-size-md': 'calc(var(--size-md-control-height) * 0.62)',
|
|
|
'knob-size-lg': 'calc(var(--size-lg-control-height) * 0.62)',
|
|
|
'band-height': {
|
|
|
parts: ['panel'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--natural-time-picker-band-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--natural-time-picker-band-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--natural-time-picker-band-height-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'knob-size': {
|
|
|
parts: ['panel'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--natural-time-picker-knob-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--natural-time-picker-knob-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--natural-time-picker-knob-size-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'panel-gap-sm': 'var(--space-2-5)',
|
|
|
'panel-gap-md': 'var(--space-3)',
|
|
|
'panel-gap-lg': 'var(--space-4)',
|
|
|
'panel-gap': {
|
|
|
parts: ['panel'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--natural-time-picker-panel-gap-md)', scope: 'host' },
|
|
|
{ value: 'var(--natural-time-picker-panel-gap-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--natural-time-picker-panel-gap-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'band-radius': 'var(--radius-md)',
|
|
|
'band-border': 'var(--color-border-subtle)',
|
|
|
'band-border-width': 'var(--border-width)',
|
|
|
'line-width': 'calc(var(--border-width) * 1.5)',
|
|
|
'line-radius': 'var(--radius-sm)',
|
|
|
'knob-radius': 'var(--radius-full, 50%)',
|
|
|
'knob-shadow': 'var(--shadow-2)',
|
|
|
'knob-focus-ring': 'var(--color-primary-border)',
|
|
|
'knob-focus-ring-offset': 'var(--space-1)',
|
|
|
'ticks-inset-inline': 'var(--space-2)',
|
|
|
'ticks-inset-block-end': 'var(--space-1)',
|
|
|
'tick-font-size': 'var(--size-xxs-font-size)',
|
|
|
'tick-font-family': 'var(--font-family-mono, ui-monospace, monospace)',
|
|
|
// Readout
|
|
|
'display-gap': 'var(--space-3)',
|
|
|
'time-font-family': 'var(--font-family-mono, ui-monospace, monospace)',
|
|
|
'time-font-size': 'var(--size-xl-font-size)',
|
|
|
'time-font-weight': 'var(--font-weight-bold)',
|
|
|
'time-letter-spacing': 'var(--tracking-wide)',
|
|
|
'meridiem-margin-inline-start': 'var(--space-1)',
|
|
|
'meridiem-font-size': 'var(--size-md-font-size)',
|
|
|
'meridiem-font-weight': 'var(--font-weight-bold)',
|
|
|
'meridiem-fg': 'var(--color-primary-text)',
|
|
|
'period-gap': 'var(--space-1)',
|
|
|
'period-font-size': 'var(--size-xs-font-size)',
|
|
|
'period-font-weight': 'var(--font-weight-semibold)',
|
|
|
'period-fg': 'var(--color-primary-text)',
|
|
|
// Steppers + chips
|
|
|
'steppers-gap': 'var(--space-2)',
|
|
|
'stepper-group-gap': 'var(--space-1-5)',
|
|
|
'stepper-label-font-size': 'var(--size-xs-font-size)',
|
|
|
'stepper-label-font-weight': 'var(--font-weight-medium)',
|
|
|
'stepper-label-fg': 'var(--color-content-muted)',
|
|
|
'stepper-label-letter-spacing': 'var(--tracking-caps)',
|
|
|
'periods-gap': 'var(--space-2)',
|
|
|
'periods-margin-block': 'var(--space-3)',
|
|
|
'chip-font-size': 'var(--size-xs-font-size)',
|
|
|
// Trigger
|
|
|
'trigger-gap': 'var(--space-2)',
|
|
|
'trigger-min-width': '11rem',
|
|
|
'trigger-padding-block': 'var(--space-2-5)',
|
|
|
'trigger-padding-inline': 'var(--space-3)',
|
|
|
'trigger-bg': 'var(--color-surface-default)',
|
|
|
'trigger-fg': 'var(--color-content-primary)',
|
|
|
'trigger-border': 'var(--color-border-default)',
|
|
|
'trigger-border-width': 'var(--border-width)',
|
|
|
'trigger-radius': 'var(--radius-md)',
|
|
|
'trigger-font-size': 'var(--size-md-font-size)',
|
|
|
'hover-trigger-border': 'var(--color-border-strong)',
|
|
|
'open-trigger-border': 'var(--color-primary-border)',
|
|
|
'open-trigger-ring': 'var(--color-primary-border)',
|
|
|
'trigger-value-empty-fg': 'var(--color-content-muted)',
|
|
|
'trigger-icon-fg': 'var(--color-content-muted)'
|
|
|
},
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// GradientBuilder — the gradient editor's own chrome. Sized on its OWN
|
|
|
// geometry (px × `--scaling`), not on the `--size-{k}-*` bundle: an editor
|
|
|
// rail has no control-height coordinate. The two-tier shape is the canon
|
|
|
// (theme-reach F2): per-size coordinates at a stable scope + ONE resolved
|
|
|
// name per axis (`host` = md, `size:` overrides) which is the only thing the
|
|
|
// recipe CSS consumes.
|
|
|
'gradient-builder': {
|
|
|
'preview-height-sm': 'calc(56px * var(--scaling, 1))',
|
|
|
'preview-height-md': 'calc(128px * var(--scaling, 1))',
|
|
|
'preview-height-lg': 'calc(88px * var(--scaling, 1))',
|
|
|
'track-height-sm': 'calc(20px * var(--scaling, 1))',
|
|
|
'track-height-md': 'calc(24px * var(--scaling, 1))',
|
|
|
'track-height-lg': 'calc(28px * var(--scaling, 1))',
|
|
|
'stop-size-sm': 'calc(18px * var(--scaling, 1))',
|
|
|
'stop-size-md': 'calc(20px * var(--scaling, 1))',
|
|
|
'stop-size-lg': 'calc(22px * var(--scaling, 1))',
|
|
|
'gap-sm': 'var(--space-3)',
|
|
|
'gap-md': 'var(--space-4)',
|
|
|
'gap-lg': 'var(--space-5)',
|
|
|
'padding-block-sm': 'var(--space-3)',
|
|
|
'padding-block-md': 'var(--space-4)',
|
|
|
'padding-block-lg': 'var(--space-5)',
|
|
|
'padding-inline-sm': 'var(--space-3)',
|
|
|
'padding-inline-md': 'var(--space-4)',
|
|
|
'padding-inline-lg': 'var(--space-5)',
|
|
|
'preview-height': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--gradient-builder-preview-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--gradient-builder-preview-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--gradient-builder-preview-height-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'track-height': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--gradient-builder-track-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--gradient-builder-track-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--gradient-builder-track-height-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'stop-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--gradient-builder-stop-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--gradient-builder-stop-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--gradient-builder-stop-size-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
gap: {
|
|
|
declarations: [
|
|
|
{ value: 'var(--gradient-builder-gap-md)', scope: 'host' },
|
|
|
{ value: 'var(--gradient-builder-gap-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--gradient-builder-gap-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'padding-block': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--gradient-builder-padding-block-md)', scope: 'host' },
|
|
|
{ value: 'var(--gradient-builder-padding-block-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--gradient-builder-padding-block-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'padding-inline': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--gradient-builder-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--gradient-builder-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--gradient-builder-padding-inline-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
// The card
|
|
|
radius: 'var(--radius-lg)',
|
|
|
bg: 'var(--color-surface-raised, var(--color-surface-default))',
|
|
|
border: 'var(--color-border-default)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
// Preview + track
|
|
|
'preview-radius': 'var(--radius-md)',
|
|
|
'preview-bg': 'var(--color-surface-default)',
|
|
|
'preview-border': 'var(--color-border-subtle)',
|
|
|
'track-radius': 'var(--radius-md)',
|
|
|
'track-border': 'var(--color-border-subtle)',
|
|
|
// The transparency checker: the CELL COLOUR is the knob; the four-gradient
|
|
|
// pattern that composes it stays private to the recipe.
|
|
|
'checker-fg': 'var(--color-border-subtle)',
|
|
|
'checker-cell': 'calc(7px * var(--scaling, 1))',
|
|
|
// Stop handle
|
|
|
'stop-radius': 'var(--radius-full)',
|
|
|
'stop-bg': 'var(--color-surface-default)',
|
|
|
'stop-ring': 'var(--color-surface-default)',
|
|
|
'stop-ring-width': 'calc(2px * var(--scaling, 1))',
|
|
|
'stop-shadow':
|
|
|
'0 0 0 1px color-mix(in srgb, var(--color-neutral-contrast) 45%, transparent), 0 1px 3px color-mix(in srgb, var(--color-neutral-contrast) 30%, transparent)',
|
|
|
'selected-stop-shadow':
|
|
|
'0 0 0 calc(2px * var(--scaling, 1)) var(--color-primary-solid), 0 1px 3px color-mix(in srgb, var(--color-neutral-contrast) 35%, transparent)',
|
|
|
// Rows + angle dial
|
|
|
'stop-actions-gap': 'var(--space-2)',
|
|
|
'angle-dial-gap': 'var(--space-2)',
|
|
|
'angle-dial-head-gap': 'var(--space-2)',
|
|
|
'label-font-size': 'var(--size-sm-font-size)',
|
|
|
'label-fg': 'var(--color-content-secondary)',
|
|
|
'value-font-size': 'var(--size-sm-font-size)',
|
|
|
'value-fg': 'var(--color-content-primary)',
|
|
|
// Stop color editor
|
|
|
'stop-color-title-font-size': 'var(--size-sm-font-size)',
|
|
|
'stop-color-title-font-weight': 'var(--font-weight-medium, 500)',
|
|
|
'stop-color-title-fg': 'var(--color-content-primary)',
|
|
|
'stop-color-sliders-gap': 'var(--space-2)',
|
|
|
// Stop list
|
|
|
'stop-list-gap': 'var(--space-1)',
|
|
|
'stop-list-row-gap': 'var(--space-2)',
|
|
|
'stop-list-value-gap': 'var(--space-2)',
|
|
|
'stop-list-value-padding-block': 'var(--space-1)',
|
|
|
'stop-list-value-padding-inline': 'var(--space-2)',
|
|
|
'stop-list-value-radius': 'var(--radius-md)',
|
|
|
'stop-list-value-bg': 'var(--color-surface-default)',
|
|
|
'stop-list-value-fg': 'var(--color-content-primary)',
|
|
|
'stop-list-value-border': 'var(--color-border-subtle)',
|
|
|
'stop-list-value-font-size': 'var(--size-sm-font-size)',
|
|
|
'selected-stop-list-value-border': 'var(--color-primary-border)',
|
|
|
'stop-list-pos-fg': 'var(--color-content-secondary)',
|
|
|
// Presets
|
|
|
'presets-gap': 'var(--space-2)',
|
|
|
'preset-grid-gap': 'var(--space-2)',
|
|
|
'preset-height': 'calc(32px * var(--scaling, 1))',
|
|
|
'preset-radius': 'var(--radius-md)',
|
|
|
'preset-border': 'var(--color-border-subtle)',
|
|
|
'hover-preset-border': 'var(--color-border-strong)',
|
|
|
'selected-preset-border': 'var(--color-primary-border)',
|
|
|
'selected-preset-outline': 'var(--color-primary-border)'
|
|
|
},
|
|
|
|
|
|
// GradientPicker. Composes picker-shell + Popover + GradientBuilder, so it
|
|
|
// mints nothing those layers own — only its own trigger pill, the gradient
|
|
|
// chip, the panel widths and the presets grid.
|
|
|
'gradient-picker': {
|
|
|
// Per-size coordinates (sm | md | lg). Height and type are 1:1 with the
|
|
|
// bundle; the trigger's inline padding is its OWN rhythm (md and lg sit
|
|
|
// tighter than the bundle, so forcing it would change the default).
|
|
|
// The chip is the trigger's height minus its vertical breathing room, so
|
|
|
// it tracks the control instead of carrying a size of its own.
|
|
|
'chip-size-sm': 'calc(var(--size-sm-control-height) - var(--space-2))',
|
|
|
'chip-size-md': 'calc(var(--size-md-control-height) - var(--space-2))',
|
|
|
'chip-size-lg': 'calc(var(--size-lg-control-height) - var(--space-2))',
|
|
|
|
|
|
// The RESOLVED names. `data-size` is stamped on the root and the trigger
|
|
|
// hangs from it, so `host` scope is enough.
|
|
|
'chip-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--gradient-picker-chip-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--gradient-picker-chip-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--gradient-picker-chip-size-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// Root rhythm.
|
|
|
gap: 'var(--space-1-5)',
|
|
|
|
|
|
// Trigger — a field-shaped pill.
|
|
|
'trigger-gap': 'var(--space-2)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
|
|
|
// The gradient chip. Its FILL is not a knob: soma stamps the user's own
|
|
|
// gradient inline (`--gp-current-gradient`, a value channel), and a theme
|
|
|
// has no business overriding what the user picked.
|
|
|
'chip-radius': 'var(--radius-sm)',
|
|
|
'chip-border': 'var(--color-border-subtle)',
|
|
|
|
|
|
// Panel widths. The content is PORTALLED and sizes off `data-picker-size`
|
|
|
// (picker-shell's attr), not `data-size`, so these stay flat tokens read
|
|
|
// by three rules instead of a TSC size cascade - the scope vocabulary has
|
|
|
// no word for another component's attribute.
|
|
|
'content-width-sm': '18rem',
|
|
|
'content-width-md': '20rem',
|
|
|
'content-width-lg': '22rem',
|
|
|
|
|
|
// Stop list — content-sized up to a max, then scrolls.
|
|
|
'stoplist-max-block': 'calc(132px * var(--scaling, 1))',
|
|
|
'stoplist-scrollbar-size': 'var(--space-1-5)',
|
|
|
'stoplist-scrollbar-inset': 'var(--space-3)',
|
|
|
|
|
|
// Presets panel + its swatch grid.
|
|
|
'presets-panel-width': '15rem',
|
|
|
'presets-gap': 'var(--space-2)',
|
|
|
'presets-min-column': '48px',
|
|
|
'preset-height': 'calc(32px * var(--scaling, 1))',
|
|
|
'preset-radius': 'var(--radius-md)',
|
|
|
'preset-border': 'var(--color-border-subtle)',
|
|
|
'hover-preset-border': 'var(--color-border-strong)',
|
|
|
'selected-preset-border': 'var(--color-primary-border)',
|
|
|
'selected-preset-outline-offset': '2px',
|
|
|
|
|
|
// Actions row under the stops.
|
|
|
'actions-gap': 'var(--space-2)'
|
|
|
},
|
|
|
clipboard: {
|
|
|
// Host is `display: contents` and the trigger reuses Button's recipe, so
|
|
|
// everything here is the INDICATOR — the brief "Copied!" pill.
|
|
|
// `font-family` is absent: it reads the `--style-label-*` layer.
|
|
|
'indicator-gap': 'var(--space-1)',
|
|
|
'indicator-padding-block': '0.125rem',
|
|
|
'indicator-padding-inline': '0.5rem',
|
|
|
'indicator-radius': 'var(--radius-full)',
|
|
|
'indicator-font-size': 'var(--size-xs-font-size)',
|
|
|
'indicator-font-weight': 'var(--font-weight-medium, 500)',
|
|
|
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
|
|
|
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
|
|
|
},
|
|
|
// Carousel. The prev/next triggers compose `<IconButton>`, so their chrome is
|
|
|
// NOT minted here — a theme dresses them through the Button contract.
|
|
|
carousel: {
|
|
|
// The paginator dots. These rem values are the carousel's OWN geometry:
|
|
|
// the size bundle has no coordinate that means "a dot", so they are
|
|
|
// verbatim per size rather than a deviation to flag.
|
|
|
'indicator-size-xs': '0.375rem',
|
|
|
'indicator-size-sm': '0.4375rem',
|
|
|
'indicator-size-md': '0.5rem',
|
|
|
'indicator-size-lg': '0.625rem',
|
|
|
'indicator-size-xl': '0.75rem',
|
|
|
'indicator-gap-xs': 'var(--space-1)',
|
|
|
'indicator-gap-sm': 'var(--space-1)',
|
|
|
'indicator-gap-md': 'var(--space-2)',
|
|
|
'indicator-gap-lg': 'var(--space-3)',
|
|
|
'indicator-gap-xl': 'var(--space-3)',
|
|
|
// There is NO token for the gap BETWEEN SLIDES, on purpose: soma stamps
|
|
|
// `gap` INLINE on the item group from the consumer's `gap` prop (it needs
|
|
|
// the number for the flex-basis math), so a recipe declaration can never
|
|
|
// win and a token here would be a token that lies. Measured 2026-08-21
|
|
|
// (adversarial review of F2-A): the previous `item-gap` trio moved
|
|
|
// nothing; retiring it gave 0 computed diffs. The knob is soma's value
|
|
|
// channel, like `--gp-current-gradient`.
|
|
|
|
|
|
'indicator-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--carousel-indicator-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--carousel-indicator-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--carousel-indicator-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--carousel-indicator-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--carousel-indicator-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'indicator-gap': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--carousel-indicator-gap-md)', scope: 'host' },
|
|
|
{ value: 'var(--carousel-indicator-gap-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--carousel-indicator-gap-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--carousel-indicator-gap-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--carousel-indicator-gap-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
// Chassis.
|
|
|
gap: 'var(--space-2)',
|
|
|
radius: 'var(--radius-md)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
// The viewport and the slides round INSIDE the frame, so their corner is
|
|
|
// the frame's minus the border - concentric, not a second radius.
|
|
|
'inner-radius-inset': '2px',
|
|
|
// How far the prev/next controls sit from the frame edge.
|
|
|
'trigger-offset': 'var(--space-3)',
|
|
|
// Variants. `surface` and `outline` inset the frame so the slides do not
|
|
|
// touch its edge; `ghost` keeps the border box (no layout jump when the
|
|
|
// consumer switches) but paints nothing.
|
|
|
'frame-padding': 'var(--space-2)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-border-default)',
|
|
|
'surface-bg': 'var(--color-surface-default)',
|
|
|
// Vertical orientation needs a bounded block axis or the viewport
|
|
|
// collapses and the slides spill; this is that floor.
|
|
|
'vertical-block-size': '16rem',
|
|
|
|
|
|
// The dot strip.
|
|
|
'indicator-group-padding-block': 'var(--space-1)',
|
|
|
'indicator-bg': 'var(--color-surface-overlay)',
|
|
|
'indicator-radius': 'var(--radius-full)',
|
|
|
// The active dot stretches into a pill; the factor is what makes it read
|
|
|
// as "you are here" rather than as a bigger dot.
|
|
|
'active-indicator-scale': '2.25',
|
|
|
|
|
|
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
|
|
|
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
|
|
|
},
|
|
|
// Listbox. Consumes TWO shared layers, and the rule of those layers decides
|
|
|
// most of this entry: a consumer does NOT mint `--{c}-{axis}` for an axis the
|
|
|
// layer already owns. So the row rhythm (height, inline/block padding, gap
|
|
|
// and font-size) is NOT here — `lib/list-surface.css` owns it through
|
|
|
// `--list-item-*` / `--list-font-size`, per size, and the recipe adopts it
|
|
|
// through its private slots. Minting `--listbox-item-height` would be the
|
|
|
// parallel vocabulary that doctrine forbids, and it would silently outrank
|
|
|
// the layer for every surface that shares the rhythm (select, combobox,
|
|
|
// command and the menus).
|
|
|
listbox: {
|
|
|
// The surface the rows sit on - this IS the listbox's own.
|
|
|
gap: 'var(--space-1)',
|
|
|
padding: 'var(--space-1)',
|
|
|
radius: 'var(--radius-md)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-border-default)',
|
|
|
bg: 'var(--color-surface-default)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
// Generous but bounded; the consumer overrides it inline for a taller list.
|
|
|
'max-block-size': 'min(60vh, 22rem)',
|
|
|
'invalid-border': 'var(--color-threat-element)',
|
|
|
|
|
|
// Rows. The rhythm comes from the layer; what is left is the row's own
|
|
|
// corner and ink.
|
|
|
'item-radius': 'var(--radius-sm)',
|
|
|
'item-fg': 'var(--color-content-primary)',
|
|
|
'disabled-item-fg': 'var(--color-content-muted)',
|
|
|
|
|
|
// The check glyph. Its COLOUR is the palette forward, not a knob.
|
|
|
'item-indicator-size': '0.875rem',
|
|
|
|
|
|
// Groups.
|
|
|
'group-gap': 'var(--space-1)',
|
|
|
'group-label-padding-block': 'var(--space-1)',
|
|
|
'group-label-fg': 'var(--color-content-muted)',
|
|
|
'group-label-font-size': 'var(--size-xs-font-size)',
|
|
|
'group-label-font-weight': 'var(--font-weight-medium)',
|
|
|
'group-label-tracking': 'var(--tracking-caps)',
|
|
|
|
|
|
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
|
|
|
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }
|
|
|
},
|
|
|
// Content-ink links — `color` tints the link text. Host = primary-text; the
|
|
|
// shared layer routes roles + 33 scales + custom. (Was a per-role `color:`
|
|
|
// cascade in link.css.) The hover ink rides the palette too since 2026-08-05:
|
|
|
// it was pinned to `--color-primary-solid-hover`, so a coloured link went
|
|
|
// purple under the pointer.
|
|
|
link: {
|
|
|
radius: 'var(--radius-sm)',
|
|
|
// Gap to the external-link glyph, and the glyph itself — em-relative so
|
|
|
// both track the link's inherited type.
|
|
|
gap: '0.25em',
|
|
|
'external-icon-size': '0.85em',
|
|
|
// The underline. Both rules that draw it (`always` and `hover`) share these
|
|
|
// two values, so it is one knob each.
|
|
|
'underline-width': '1px',
|
|
|
'underline-offset': '2px',
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' },
|
|
|
'_palette-solid-hover': { value: 'var(--color-primary-solid-hover)', scope: 'host' }
|
|
|
},
|
|
|
// Mark (inline highlight) — `element` tints the background (default = fulfill,
|
|
|
// the classic highlight), `text` tints the ink but ONLY when data-color is set
|
|
|
// (the base stays `color: inherit`), so text host is the forward fallback only.
|
|
|
mark: {
|
|
|
// No type knobs on purpose: a highlight inherits the ambient typography of
|
|
|
// the paragraph it lives in (`font: inherit`). What it owns is decoration.
|
|
|
// The padding is em-relative and deliberately tiny: the tint has to read
|
|
|
// WITHOUT breaking the line-height around it.
|
|
|
'padding-inline': '0.16em',
|
|
|
'padding-block': '0.04em',
|
|
|
radius: 'var(--radius-sm)',
|
|
|
'_palette-element': { value: 'var(--color-fulfill-element)', scope: 'host' },
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
|
|
|
},
|
|
|
// Typographic primitives (Fase 4, tinta de contenido) — `color` beyond the
|
|
|
// content-ink axis routes through the shared palette forward (roles + 33
|
|
|
// scales + custom). Colored-only: the ink axis (primary…on-solid) never
|
|
|
// stamps `data-color`, so these hosts are the forward fallback only.
|
|
|
text: {
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
|
|
|
},
|
|
|
heading: {
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
|
|
|
},
|
|
|
display: {
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
|
|
|
},
|
|
|
code: {
|
|
|
// The six TYPE axes are absent on purpose: they consume `--style-code-*`,
|
|
|
// the typographic layer, through the per-instance hatch. A primitive does
|
|
|
// not re-coin its layer's vocabulary (D-TH.2-b). What lives here is the
|
|
|
// chrome the `soft` / `outline` variants add on top.
|
|
|
radius: 'var(--radius-sm)',
|
|
|
'soft-bg': 'var(--color-neutral-element)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-neutral-border)',
|
|
|
// em-relative on purpose: the pill grows with the code it wraps. Declared
|
|
|
// identically by both chromed variants, so one knob per axis.
|
|
|
'padding-inline': '0.32em',
|
|
|
'padding-block': '0.1em',
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
|
|
|
},
|
|
|
label: {
|
|
|
// The six TYPE axes are absent on purpose: `label` is a typographic
|
|
|
// primitive and they consume `--style-label-*` through the per-instance
|
|
|
// hatch (D-TH.2-b). These two are what the primitive adds on its own.
|
|
|
gap: 'var(--space-1)',
|
|
|
'disabled-fg': 'var(--color-content-disabled)',
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
|
|
|
},
|
|
|
// ProofOfHuman — `color` tints the challenge accent (path / needle / hand +
|
|
|
// soft corridor) via the shared layer (roles + 33 scales + custom) on the
|
|
|
// `root` part; the root renders `display: contents` and cascades to every
|
|
|
// challenge. Host = primary (the stamped default). The captcha's DEPICTION
|
|
|
// literals (pigeon / letter) stay fixed-tone — see recipe-css-contract.
|
|
|
'proof-of-human': {
|
|
|
'min-height-sm': 'calc(var(--size-sm-control-height) * 3)',
|
|
|
'min-height-md': 'calc(var(--size-md-control-height) * 3)',
|
|
|
'min-height-lg': 'calc(var(--size-lg-control-height) * 3)',
|
|
|
'min-height': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--proof-of-human-min-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--proof-of-human-min-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--proof-of-human-min-height-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
radius: 'var(--radius-md)',
|
|
|
bg: 'var(--color-surface-raised)',
|
|
|
border: 'var(--color-border-default)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
'verified-border': 'var(--color-fulfill-solid)',
|
|
|
'failed-border': 'var(--color-risk-solid)',
|
|
|
// The guide path + its token: the accent is the only colour axis a theme
|
|
|
// should reach here (the scene is fixed-tone).
|
|
|
'locked-opacity': '0.55',
|
|
|
'tolerance-opacity': '0.5',
|
|
|
'guide-opacity': '0.65',
|
|
|
'guide-width': 'var(--border-width-thick, 2px)',
|
|
|
'trail-width': '4px',
|
|
|
'goal-width': '3px',
|
|
|
'goal-opacity': '0.8',
|
|
|
'token-size': '1.25rem',
|
|
|
'token-radius': 'var(--radius-full)',
|
|
|
'token-focus-offset': '3px',
|
|
|
'_palette-solid': {
|
|
|
parts: ['root'],
|
|
|
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-element': {
|
|
|
parts: ['root'],
|
|
|
declarations: [{ value: 'var(--color-primary-element)', 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: {
|
|
|
// The one size coordinate: block height, circle diameter (via aspect-ratio)
|
|
|
// and the gap between text lines, which is `calc(size * 0.5)` — a
|
|
|
// PROPORTION, so construction rather than a knob.
|
|
|
'size-xs': '0.5rem',
|
|
|
'size-sm': '0.75rem',
|
|
|
'size-md': '1rem',
|
|
|
'size-lg': '1.5rem',
|
|
|
'size-xl': '2rem',
|
|
|
size: {
|
|
|
declarations: [
|
|
|
{ value: 'var(--skeleton-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--skeleton-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--skeleton-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--skeleton-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--skeleton-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
// Radius is per SHAPE, not per size: the rectangle's is a knob, the
|
|
|
// circle's `50%` is identity.
|
|
|
radius: 'var(--radius-md)',
|
|
|
'line-radius': 'var(--radius-sm)',
|
|
|
// The shimmer period. Bare `1.4s` until now — same call as spinner's.
|
|
|
duration: '1.4s',
|
|
|
// `--_skeleton-bg` / `-highlight` stay PRIVATE: each is one variable with
|
|
|
// two sources (the cross-mode neutral mix, or the palette forward when
|
|
|
// `data-color` lands), and flattening them would duplicate every rule per
|
|
|
// colour.
|
|
|
'_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: {
|
|
|
// ONE size coordinate drives the whole component: the ring's box, the dot
|
|
|
// track's height, the bar's height. Every other dimension is a PROPORTION
|
|
|
// of it (`calc(size * 0.3)` = the dot, `* 0.18` = the bar, `* 0.2` / `* 0.12`
|
|
|
// = the gaps) and those stay in the recipe: they are the SHAPE of each
|
|
|
// variant, not knobs. Same split as tree-grid's indent guides.
|
|
|
//
|
|
|
// The scale is the spinner's OWN, not the icon bundle's: measured
|
|
|
// 12/16/20/28/40 px here against 14/16/18/20/32 in `--icon-size-*`, which
|
|
|
// only agree at sm. Verbatim on purpose — forcing the bundle would move the
|
|
|
// default in four sizes out of five.
|
|
|
'size-xs': '0.75rem',
|
|
|
'size-sm': '1rem',
|
|
|
'size-md': '1.25rem',
|
|
|
'size-lg': '1.75rem',
|
|
|
'size-xl': '2.5rem',
|
|
|
'thickness-xs': '1.5px',
|
|
|
'thickness-sm': '2px',
|
|
|
'thickness-md': '2px',
|
|
|
'thickness-lg': '3px',
|
|
|
'thickness-xl': '4px',
|
|
|
size: {
|
|
|
declarations: [
|
|
|
{ value: 'var(--spinner-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--spinner-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--spinner-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--spinner-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--spinner-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
thickness: {
|
|
|
declarations: [
|
|
|
{ value: 'var(--spinner-thickness-md)', scope: 'host' },
|
|
|
{ value: 'var(--spinner-thickness-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--spinner-thickness-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--spinner-thickness-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--spinner-thickness-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
gap: 'var(--space-2)',
|
|
|
// The spin period. It used to be `0.9s` bare; feed's spinner already earned
|
|
|
// duration tokens and media-player's did not (next-features §13) — done
|
|
|
// right here from the start.
|
|
|
duration: '0.9s',
|
|
|
// Only ever consumed under `prefers-reduced-motion`, where the ring stops
|
|
|
// and dims instead of spinning.
|
|
|
'track-opacity': 'var(--opacity-60)',
|
|
|
'bar-radius': 'var(--radius-sm)',
|
|
|
'label-font-size': 'var(--size-sm-font-size)',
|
|
|
'label-fg': 'var(--color-content-muted)',
|
|
|
|
|
|
// The ink and the track stay PRIVATE: each is one variable with two
|
|
|
// sources, switched by `data-color='inherit'` (palette forward →
|
|
|
// currentColor), and the track wraps its source in a different color-mix per
|
|
|
// branch (60 % normal, 25 % inherit). Flattening them would mean duplicating
|
|
|
// every rule per colour — same call as textarea's focus border.
|
|
|
'_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: {
|
|
|
// ── Per-size coordinates (xs..xl) ──
|
|
|
// Type is 1:1 with the bundle in all five (measured: `--font-size-{k}` IS
|
|
|
// `--size-{k}-font-size`), so it reads the bundle coordinate.
|
|
|
'input-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'input-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'input-font-size-md': 'var(--size-md-font-size)',
|
|
|
'input-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'input-font-size-xl': 'var(--size-xl-font-size)',
|
|
|
// Spacing keeps the field's OWN rhythm, verbatim. Measured against the
|
|
|
// bundle 2026-08-21: padding-inline matches at sm/md/lg/xl but NOT at xs
|
|
|
// (8px here, 6px there), and padding-block matches at xs only — from sm up
|
|
|
// this field is looser (6/8/12/16 vs 4/6/8/…). A multi-line field breathes
|
|
|
// more than a one-line control, and that is the intended default. Spacing
|
|
|
// sits outside the bundle guard, which governs control-height / font-size /
|
|
|
// icon-size only (same call as table's cell padding).
|
|
|
'input-padding-inline-xs': 'var(--space-2)',
|
|
|
'input-padding-inline-sm': 'var(--space-2)',
|
|
|
'input-padding-inline-md': 'var(--space-3)',
|
|
|
'input-padding-inline-lg': 'var(--space-4)',
|
|
|
'input-padding-inline-xl': 'var(--space-5)',
|
|
|
'input-padding-block-xs': 'var(--space-1)',
|
|
|
'input-padding-block-sm': 'var(--space-1-5)',
|
|
|
'input-padding-block-md': 'var(--space-2)',
|
|
|
'input-padding-block-lg': 'var(--space-3)',
|
|
|
'input-padding-block-xl': 'var(--space-4)',
|
|
|
|
|
|
// The RESOLVED names. `data-size` is stamped on `[data-textarea]` and the
|
|
|
// input inherits, so the cascade hangs from the host — no `parts` needed
|
|
|
// (precedent: command).
|
|
|
'input-font-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--textarea-input-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--textarea-input-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--textarea-input-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--textarea-input-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--textarea-input-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'input-padding-inline': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--textarea-input-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--textarea-input-padding-inline-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--textarea-input-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--textarea-input-padding-inline-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--textarea-input-padding-inline-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'input-padding-block': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--textarea-input-padding-block-md)', scope: 'host' },
|
|
|
{ value: 'var(--textarea-input-padding-block-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--textarea-input-padding-block-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--textarea-input-padding-block-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--textarea-input-padding-block-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// ── Chassis ──
|
|
|
gap: 'var(--space-2)',
|
|
|
|
|
|
// ── Input ──
|
|
|
// Radius is DECOUPLED from size on purpose (Radix model): the control
|
|
|
// archetype default, constant across the five.
|
|
|
'input-radius': 'var(--radius-default)',
|
|
|
'input-border-width': 'var(--border-width)',
|
|
|
'input-border': 'var(--color-neutral-border)',
|
|
|
'hover-input-border': 'var(--color-border-strong)',
|
|
|
// The FOCUS border, not the resting one. Its per-color swap
|
|
|
// (`--_textarea-palette-solid`) is the THM-2 palette forward and stays
|
|
|
// private — it is one variable with two sources, not two knobs. The ghost
|
|
|
// variant reads this same token: a `ghost-input-border` would be one value
|
|
|
// under two names.
|
|
|
'focus-input-border': 'var(--focus-ring-color)',
|
|
|
'invalid-input-border': 'var(--color-threat-solid)',
|
|
|
'input-bg': 'var(--color-surface-default)',
|
|
|
'input-fg': 'var(--color-content-primary)',
|
|
|
'input-placeholder-fg': 'var(--color-content-muted)',
|
|
|
'input-font-family': 'var(--font-family-primary)',
|
|
|
'input-font-weight': 'var(--font-weight-medium)',
|
|
|
'input-line-height': 'var(--leading-normal)',
|
|
|
|
|
|
// ── Count chip ──
|
|
|
// ONE token, not five per size: the caption is PROPORTIONAL to the field
|
|
|
// (0.85×) and the input's own font-size already resolves per size, so the
|
|
|
// scale is written once. Five per-size tokens would have dropped the 0.85
|
|
|
// and grown the counter 18 % — the default does not move (D-TH.5).
|
|
|
// `scope: 'host'` is REQUIRED, not decoration: it reads the resolved
|
|
|
// `input-font-size`, which only exists on `[data-textarea]`. Emitted at
|
|
|
// :root the var() would resolve where its dependency is missing — the
|
|
|
// recipe scope contract catches it.
|
|
|
'count-font-size': {
|
|
|
value: 'calc(var(--textarea-input-font-size) * 0.85)',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'count-gap': '0.25em',
|
|
|
'count-fg': 'var(--color-content-muted)',
|
|
|
'count-font-family': 'var(--font-family-mono, ui-monospace, monospace)',
|
|
|
'overflow-count-fg': 'var(--color-threat-text)',
|
|
|
'overflow-count-font-weight': 'var(--font-weight-semibold)',
|
|
|
|
|
|
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' }
|
|
|
},
|
|
|
calendar: {
|
|
|
gap: 'var(--space-3)',
|
|
|
'padding-xs': 'var(--space-2)',
|
|
|
'padding-sm': 'var(--space-3)',
|
|
|
'padding-md': 'var(--space-4)',
|
|
|
'padding-lg': 'var(--space-5)',
|
|
|
radius: 'var(--radius-lg)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-border-subtle)',
|
|
|
'invalid-border': 'var(--color-risk-border)',
|
|
|
bg: 'var(--color-surface-raised)',
|
|
|
'outline-bg': 'var(--color-surface-default)',
|
|
|
'outline-shadow': 'none',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
shadow: 'var(--depth-raised-shadow), var(--depth-raised-halo)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'header-gap': 'var(--space-2)',
|
|
|
'heading-fg': 'var(--color-content-primary)',
|
|
|
'heading-font-size': 'var(--size-sm-font-size)',
|
|
|
'heading-font-weight': 'var(--font-weight-semibold)',
|
|
|
'control-size-xs': 'var(--size-xxs-control-height)',
|
|
|
'control-size-sm': 'var(--size-xs-control-height)',
|
|
|
'control-size-md': 'var(--size-sm-control-height)',
|
|
|
'control-size-lg': 'var(--size-md-control-height)',
|
|
|
'control-radius': 'var(--radius-md)',
|
|
|
'control-border-width': 'var(--border-width)',
|
|
|
'control-border': 'transparent',
|
|
|
'hover-control-border': 'var(--color-border-subtle)',
|
|
|
'control-bg': 'transparent',
|
|
|
'control-fg': 'var(--color-content-secondary)',
|
|
|
'hover-control-fg': 'var(--color-content-primary)',
|
|
|
'select-min-width': '8ch',
|
|
|
'select-padding-inline': 'var(--space-2)',
|
|
|
// Between month COLUMNS in the paginated view — a full step, unlike the
|
|
|
// hairline `grid-gap` that separates day cells inside one month.
|
|
|
'months-gap': 'var(--space-4)',
|
|
|
'grid-gap': 'var(--space-0-5)',
|
|
|
'weekday-size': 'var(--space-5)',
|
|
|
'weekday-fg': 'var(--color-content-muted)',
|
|
|
'weekday-font-size': 'var(--size-xs-font-size)',
|
|
|
'weekday-font-weight': 'var(--font-weight-medium)',
|
|
|
'day-size-xs': 'var(--size-xxs-control-height)',
|
|
|
'day-size-sm': 'var(--size-xs-control-height)',
|
|
|
'day-size-md': 'var(--size-sm-control-height)',
|
|
|
'day-size-lg': 'var(--size-md-control-height)',
|
|
|
'day-radius': 'var(--radius-md)',
|
|
|
'day-border-width': 'var(--border-width)',
|
|
|
'day-border': 'transparent',
|
|
|
'hover-day-border': 'var(--color-border-subtle)',
|
|
|
'day-bg': 'transparent',
|
|
|
'day-fg': 'var(--color-content-primary)',
|
|
|
'hover-day-fg': 'var(--color-content-primary)',
|
|
|
'day-font-weight': 'var(--font-weight-regular)',
|
|
|
'day-selected-fg': 'var(--color-content-on-solid)',
|
|
|
// Range endpoints — the calendar-surface PUBLIC range API (S8). RangeCalendar
|
|
|
// paints start / end / dual-endpoint day cells from these; a consumer
|
|
|
// (DateRangePicker, a theme) retunes the range palette by setting the public
|
|
|
// token, never RangeCalendar's private. `:root` defaults; RangeCalendar binds
|
|
|
// `range-end-*` to its live accent and swaps `range-start-*` per data-color
|
|
|
// (both depend on the `[data-range-calendar]` host cascade, so they stay in
|
|
|
// range-calendar.css). Start defaults to `affirm` (the range "opens"); end
|
|
|
// follows the accent (default primary).
|
|
|
'range-start-solid': 'var(--color-affirm-solid)',
|
|
|
'range-start-solid-hover': 'var(--color-affirm-solid-hover)',
|
|
|
'range-start-border': 'var(--color-affirm-border)',
|
|
|
'range-start-text': 'var(--color-affirm-contrast)',
|
|
|
'range-end-solid': 'var(--color-primary-solid)',
|
|
|
'range-end-solid-hover': 'var(--color-primary-solid-hover)',
|
|
|
'range-end-border': 'var(--color-primary-solid)',
|
|
|
'range-end-text': 'var(--color-content-on-solid)',
|
|
|
'day-today-border': 'var(--color-border-strong)',
|
|
|
'day-today-fg': 'var(--color-content-primary)',
|
|
|
'day-today-font-weight': 'var(--font-weight-semibold)',
|
|
|
'day-outside-fg': 'var(--color-content-muted)',
|
|
|
'day-outside-opacity': '0.62',
|
|
|
'day-weekend-fg': 'var(--color-content-secondary)',
|
|
|
// Per-instance palette (THM-2 forward): the `color` accent slots (selected
|
|
|
// day, today ring, range track). Host default = primary; the shared
|
|
|
// `--palette-*` layer routes roles + 33 scales + custom into
|
|
|
// `--_calendar-palette-*` (was a per-role cascade in calendar.css).
|
|
|
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
|
|
|
'_palette-solid-hover': { value: 'var(--color-primary-solid-hover)', scope: 'host' },
|
|
|
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
|
|
|
'_palette-border': { value: 'var(--color-primary-border)', scope: 'host' },
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' },
|
|
|
// `day-holiday-shadow` and `event-shadow` are NOT here any more: they live
|
|
|
// in `lib/calendar-surface.css` (2026-08-21). Audit B.2 had host-scoped
|
|
|
// them because their value reads `--_calendar-palette-border`, a private
|
|
|
// that only exists on `[data-calendar]` — correct for this component and
|
|
|
// silently broken for the other three surfaces of the family, which read
|
|
|
// the same two tokens from hosts that never carry that attribute (the
|
|
|
// variable resolved EMPTY, the box-shadow was invalid at computed-value
|
|
|
// time and the ring never painted; measured). The layer declares the
|
|
|
// SHAPE on the shared hook and takes the ACCENT through the
|
|
|
// `--_calendar-surface-accent` slot each surface feeds from its own
|
|
|
// palette forward.
|
|
|
'day-unavailable-decoration': 'var(--color-risk-border)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
chronos: {
|
|
|
// Component-semantic dimensions only. Spacing, typography and colors come
|
|
|
// straight from the eidos primitives in chronos.css (no per-component
|
|
|
// font/color tokens — those belong to the eidos foundation).
|
|
|
'cell-min-block-sm': 'calc(var(--size-md-control-height) * 2.6)',
|
|
|
'cell-min-block-md': 'calc(var(--size-md-control-height) * 3.2)',
|
|
|
'cell-min-block-lg': 'calc(var(--size-md-control-height) * 4)',
|
|
|
'num-row': 'var(--space-7)',
|
|
|
'lane-height-sm': 'var(--space-5)',
|
|
|
'lane-height-md': 'var(--space-6)',
|
|
|
'lane-height-lg': 'var(--space-7)',
|
|
|
// Week/Day time-grid viewport height (scrolls internally).
|
|
|
'timegrid-block-size': 'min(70vh, 40rem)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
pagination: {
|
|
|
'gap-xs': 'var(--space-0-5)',
|
|
|
'gap-sm': 'var(--space-1)',
|
|
|
'gap-md': 'var(--space-1-5)',
|
|
|
'gap-lg': 'var(--space-2)',
|
|
|
'control-size-xs': 'var(--size-xxs-control-height)',
|
|
|
'control-size-sm': 'var(--size-xs-control-height)',
|
|
|
'control-size-md': 'var(--size-sm-control-height)',
|
|
|
'control-size-lg': 'var(--size-md-control-height)',
|
|
|
'control-padding-inline-xs': 'var(--space-1-5)',
|
|
|
'control-padding-inline-sm': 'var(--space-2)',
|
|
|
'control-padding-inline-md': 'var(--space-2-5)',
|
|
|
'control-padding-inline-lg': 'var(--space-3)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'control-radius': 'var(--radius-md)',
|
|
|
'control-border-width': 'var(--border-width)',
|
|
|
'control-border': 'transparent',
|
|
|
'hover-control-border': 'var(--color-border-subtle)',
|
|
|
'control-bg': 'transparent',
|
|
|
'control-fg': 'var(--color-content-secondary)',
|
|
|
'hover-control-fg': 'var(--color-content-primary)',
|
|
|
'selected-border': 'var(--color-primary-border)',
|
|
|
'hover-selected-border': 'var(--color-primary-border)',
|
|
|
'selected-bg': 'var(--color-primary-solid)',
|
|
|
'hover-selected-bg': 'var(--color-primary-solid-hover)',
|
|
|
'selected-fg': 'var(--color-content-on-solid)',
|
|
|
'ellipsis-fg': 'var(--color-content-muted)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
popover: {
|
|
|
// El disparador GENÉRICO (el que no lleva el arquetipo `field-trigger`, que
|
|
|
// tiene su propio tratamiento): su cromo es de este componente. `height`
|
|
|
// lee el BUNDLE de talla, no el primitivo crudo que el CSS traía.
|
|
|
'trigger-height': 'var(--size-md-control-height)',
|
|
|
'trigger-padding-inline': 'var(--space-3)',
|
|
|
'trigger-border-width': 'var(--border-width)',
|
|
|
'trigger-border': 'var(--color-border-default)',
|
|
|
'trigger-radius': 'var(--radius-md)',
|
|
|
'trigger-bg': 'var(--color-surface-raised)',
|
|
|
'trigger-fg': 'var(--color-content-primary)',
|
|
|
// Hover y abierto comparten valor HOY, y son dos estados distintos: un
|
|
|
// tema puede querer el abierto más marcado que el hover.
|
|
|
'hover-trigger-bg': 'var(--color-surface-overlay)',
|
|
|
'hover-trigger-border': 'var(--color-border-strong)',
|
|
|
'open-trigger-bg': 'var(--color-surface-overlay)',
|
|
|
'open-trigger-border': 'var(--color-border-strong)',
|
|
|
'content-z': 'var(--z-index-overlay-floating)',
|
|
|
'overlay-z': 'var(--z-index-overlay-backdrop)',
|
|
|
'overlay-bg': 'var(--color-overlay)',
|
|
|
'trigger-font-size': 'var(--size-sm-font-size)',
|
|
|
gap: 'var(--space-3)',
|
|
|
'max-width': '32rem',
|
|
|
'min-width-sm': '14rem',
|
|
|
'min-width-md': '18rem',
|
|
|
'min-width-lg': '22rem',
|
|
|
'min-width-xl': '28rem',
|
|
|
'padding-block-sm': 'var(--space-2)',
|
|
|
'padding-inline-sm': 'var(--space-2-5)',
|
|
|
'padding-block-md': 'var(--space-3)',
|
|
|
'padding-inline-md': 'var(--space-3)',
|
|
|
'padding-block-lg': 'var(--space-3-5)',
|
|
|
'padding-inline-lg': 'var(--space-4)',
|
|
|
'padding-block-xl': 'var(--space-4)',
|
|
|
'padding-inline-xl': 'var(--space-5)',
|
|
|
'font-size': 'var(--size-sm-font-size)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-weight': 'var(--font-weight-regular)',
|
|
|
'line-height': 'var(--leading-text)',
|
|
|
'title-font-family': 'var(--font-family-display)',
|
|
|
'title-font-size': 'var(--size-md-font-size)',
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'title-line-height': 'var(--leading-heading)',
|
|
|
'title-fg': 'var(--color-content-primary)',
|
|
|
'description-font-family': 'var(--style-label-font-family)',
|
|
|
'description-font-size': 'var(--size-sm-font-size)',
|
|
|
'description-line-height': 'var(--leading-ui)',
|
|
|
'description-fg': 'var(--color-content-secondary)',
|
|
|
// Elevation (surface · border · shadow · z) now comes from the `overlay`
|
|
|
// plane via `data-depth` (A1, Decisión 8) — the panel + arrow consume the
|
|
|
// plane directly. The recipe keeps only what's NOT elevation: the panel
|
|
|
// radius (separate axis — Radix factor, A2), the content `color`, and `bg`
|
|
|
// (still referenced by the close button's hover mix).
|
|
|
radius: 'var(--radius-lg)',
|
|
|
// `bg` RETIRADO 2026-08-22: ningún CSS lo consumía. El único sitio que lo
|
|
|
// nombraba era un COMENTARIO de `popover-arrow.svelte` («fill:
|
|
|
// var(--popover-bg)»), y por eso el test de huérfanos lo daba por vivo —
|
|
|
// busca el nombre en el corpus del componente, comentarios incluidos. La
|
|
|
// superficie del panel la pinta el PLANO de profundidad
|
|
|
// (`--depth-overlay-surface`), que es lo que un plano debe poseer.
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
// Arrow size — canonical density-aware tokens (NOT soma's hardcoded 10×5).
|
|
|
// width ≈ 2.5× height for a balanced triangle; both scale with density/scaling.
|
|
|
'arrow-width': 'var(--space-5)',
|
|
|
'arrow-height': 'var(--space-2)'
|
|
|
// `close-*` + `transition-*` tokens removed: Popover.Close composes the
|
|
|
// canonical <IconButton>/<Button>, so its chrome lives in button.css.
|
|
|
},
|
|
|
progress: {
|
|
|
'height-xs': '2px',
|
|
|
'height-sm': '4px',
|
|
|
'height-md': '6px',
|
|
|
'height-lg': '8px',
|
|
|
'height-xl': '12px',
|
|
|
'vertical-height': '10rem',
|
|
|
'radius-xs': 'var(--radius-full)',
|
|
|
'radius-sm': 'var(--radius-full)',
|
|
|
'radius-md': 'var(--radius-full)',
|
|
|
'radius-lg': 'var(--radius-full)',
|
|
|
'radius-xl': 'var(--radius-full)',
|
|
|
'ring-size-xs': '2rem',
|
|
|
'ring-size-sm': '2.5rem',
|
|
|
'ring-size-md': '3rem',
|
|
|
'ring-size-lg': '3.5rem',
|
|
|
'ring-size-xl': '4rem',
|
|
|
'ring-thickness-xs': '3px',
|
|
|
'ring-thickness-sm': '4px',
|
|
|
'ring-thickness-md': '6px',
|
|
|
'ring-thickness-lg': '8px',
|
|
|
'ring-thickness-xl': '10px',
|
|
|
'label-font-family': 'var(--style-label-font-family)',
|
|
|
'label-font-size': 'var(--size-sm-font-size)',
|
|
|
'label-font-weight': 'var(--font-weight-medium)',
|
|
|
'label-line-height': 'var(--leading-ui)',
|
|
|
'label-fg': 'var(--color-content-primary)',
|
|
|
'value-text-font-family': 'var(--style-label-font-family)',
|
|
|
'value-text-font-size': 'var(--size-sm-font-size)',
|
|
|
'value-text-font-weight': 'var(--font-weight-regular)',
|
|
|
'value-text-line-height': 'var(--leading-ui)',
|
|
|
'value-text-fg': 'var(--color-content-secondary)',
|
|
|
'track-bg':
|
|
|
'color-mix(in srgb, var(--color-content-primary) 12%, var(--color-surface-default))',
|
|
|
'ring-track-bg':
|
|
|
'color-mix(in srgb, var(--color-content-primary) 12%, var(--color-surface-default))',
|
|
|
'ring-center-bg': 'var(--color-surface-default)',
|
|
|
'indicator-bg': 'var(--color-primary-solid)',
|
|
|
'indicator-bg-loaded': 'var(--color-fulfill-solid)',
|
|
|
'transition-duration': 'var(--duration-normal)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'indeterminate-width': '35%',
|
|
|
'indeterminate-duration': '1.1s',
|
|
|
'indeterminate-ease': 'var(--ease-default)'
|
|
|
},
|
|
|
'radio-group': {
|
|
|
'gap-vertical': 'var(--space-3)',
|
|
|
'gap-horizontal': 'var(--space-4)',
|
|
|
'item-size-xs': '14px',
|
|
|
'item-size-sm': '16px',
|
|
|
'item-size-md': '18px',
|
|
|
'item-size-lg': '22px',
|
|
|
'item-size-xl': '26px',
|
|
|
'dot-size-xs': '5px',
|
|
|
'dot-size-sm': '6px',
|
|
|
'dot-size-md': '7px',
|
|
|
'dot-size-lg': '9px',
|
|
|
'dot-size-xl': '11px',
|
|
|
'item-gap': 'var(--space-3)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'font-weight': 'var(--font-weight-medium)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'item-label-fg': 'var(--color-content-primary)',
|
|
|
'disabled-item-label-fg': 'var(--color-content-disabled)',
|
|
|
'indicator-radius': 'var(--radius-full)',
|
|
|
'indicator-border-width': 'var(--border-width)',
|
|
|
'indicator-bg': 'var(--color-surface-default)',
|
|
|
'indicator-border': 'var(--color-border-default)',
|
|
|
'hover-indicator-border': 'var(--color-border-strong)',
|
|
|
'disabled-indicator-bg':
|
|
|
'color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) )',
|
|
|
'disabled-indicator-border':
|
|
|
'color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) )',
|
|
|
'checked-disabled-indicator-bg':
|
|
|
'color-mix( in srgb, var(--color-content-disabled) 40%, var(--color-surface-overlay) )',
|
|
|
'dot-fg': 'var(--color-content-on-solid)',
|
|
|
'disabled-dot-fg':
|
|
|
'color-mix( in srgb, var(--color-content-on-solid) 82%, var(--color-surface-default) )',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-solid-hover': 'var(--color-primary-solid-hover)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
|
|
|
'label-font-family': 'var(--style-label-font-family)',
|
|
|
'label-font-size': 'var(--size-sm-font-size)',
|
|
|
'label-font-weight': 'var(--font-weight-medium)',
|
|
|
'label-line-height': 'var(--leading-ui)',
|
|
|
'label-fg': 'var(--color-content-primary)',
|
|
|
// Segmented variant — a recessed track holds flush text segments; a
|
|
|
// NEUTRAL pill (surface-coloured, not the accent) slides to the checked
|
|
|
// one (iOS UISegmentedControl / Radix SegmentedControl / Ark SegmentGroup).
|
|
|
// Neutral so it reads on the muted track in light AND dark; text stays
|
|
|
// content-primary. Pill radius is concentric (track radius − padding).
|
|
|
'segmented-track-bg': 'var(--color-surface-muted)',
|
|
|
// Recess the track with an INNER shadow, so depth reads from the shadow —
|
|
|
// not from the surface tone. No single neutral tone is lighter than the
|
|
|
// muted track in BOTH light and dark (the elevation ladder inverts:
|
|
|
// `overlay` is lighter than `muted` in dark but darker in light), so a
|
|
|
// tone-only "elevated pill" loses contrast in one mode. Shadows are
|
|
|
// mode-robust — the iOS / macOS segmented approach.
|
|
|
'segmented-track-shadow': 'var(--shadow-inset-subtle)',
|
|
|
'segmented-track-padding': 'var(--space-1)',
|
|
|
'segmented-track-radius': 'var(--radius-md)',
|
|
|
'segmented-segment-padding-inline': 'var(--space-3)',
|
|
|
'segmented-segment-fg': 'var(--color-content-secondary)',
|
|
|
'hover-segmented-segment-fg': 'var(--color-content-primary)',
|
|
|
'segmented-segment-font-weight': 'var(--font-weight-medium)',
|
|
|
// The pill is the coloured selection marker (it consumes the shared
|
|
|
// `--_radio-group-palette-solid` in the CSS, so it follows `color`); the
|
|
|
// active segment's text flips to the on-solid ink. Only the shadow +
|
|
|
// radius are recipe tokens here — the fill is the palette.
|
|
|
'segmented-pill-shadow': 'var(--shadow-subtle)',
|
|
|
'segmented-pill-radius':
|
|
|
'max(0px, calc(var(--radio-group-segmented-track-radius) - var(--radio-group-segmented-track-padding)))',
|
|
|
'segmented-transition-duration': 'var(--duration-normal)',
|
|
|
'segmented-transition-ease': 'cubic-bezier(0.32, 0.72, 0, 1)',
|
|
|
// Private palette (TSC v2.1). Declared on the [data-radio-group]
|
|
|
// host, inherited by [data-radio-group-item] descendants via
|
|
|
// CSS custom property inheritance. Migrated from per-color
|
|
|
// blocks in radio-group.css.
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--radio-group-primary-solid)', scope: 'host' },
|
|
|
{ value: 'var(--radio-group-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--radio-group-neutral-solid)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--radio-group-affirm-solid)', scope: 'color:affirm' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-solid-hover': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--radio-group-primary-solid-hover)', scope: 'host' },
|
|
|
{ value: 'var(--radio-group-secondary-solid-hover)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--radio-group-neutral-solid-hover)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--radio-group-affirm-solid-hover)', scope: 'color:affirm' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-text': { value: 'var(--radio-group-dot-fg)', scope: 'host' }
|
|
|
},
|
|
|
// QrCode. Default fg/bg are FIXED-TONE (dark modules on a light backing),
|
|
|
// not theme tokens: a QR must stay dark-on-light to scan — inverting it in
|
|
|
// dark mode would break scanners. Theming goes through the `color` /
|
|
|
// `background` props (consumer owns the contrast).
|
|
|
chart: {
|
|
|
// Mark geometry sourced from canonical primitives. The two off-scale
|
|
|
// values (area fill tint, point radius) are NAMED here — never inlined
|
|
|
// as magic numbers in the wrapper.
|
|
|
'line-width': 'var(--border-width-medium)',
|
|
|
'axis-width': 'var(--border-width-thin)',
|
|
|
'grid-width': 'var(--border-width-thin)',
|
|
|
'bar-radius': 'var(--radius-sm)',
|
|
|
'point-radius': '3.5px',
|
|
|
'area-opacity': '0.15',
|
|
|
'slice-stroke-width': 'var(--border-width-thin)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-out)',
|
|
|
// Hover tooltip surface — matches the overlay / popover chrome.
|
|
|
'tooltip-bg': 'var(--color-surface-overlay)',
|
|
|
'tooltip-border': 'var(--color-border-default)',
|
|
|
'tooltip-radius': 'var(--radius-md)',
|
|
|
'tooltip-shadow': 'var(--shadow-3)',
|
|
|
'tooltip-padding': 'var(--space-2)',
|
|
|
'tooltip-gap': 'var(--space-1-5)',
|
|
|
'crosshair-fg': 'var(--color-border-strong)',
|
|
|
'crosshair-width': 'var(--border-width-thin)',
|
|
|
// ── The two TEXT ROLES, as a seam (2026-08-22, firmado) ──────────────
|
|
|
// Chart's text plays two roles of the canon and the value comes from
|
|
|
// them, but the KNOB has to be the component's own: PLAN §2-A asks every
|
|
|
// appearance knob to read `--{c}-{slot}`, and reading `var(--style-…)`
|
|
|
// straight from the CSS leaves a theme unable to retune THIS component
|
|
|
// without moving the role across the whole app. Same shape as the
|
|
|
// vertebration's own example, `--accordion-trigger-font-family:
|
|
|
// var(--style-label-font-family)`. Three axes, two roles, 19 text nodes:
|
|
|
// naming per part would be ~30 keys carrying two values.
|
|
|
//
|
|
|
// `caption` = the annotations (axis labels, legend, tooltip rows, smith
|
|
|
// labels); `label` = the chrome text (title, bar-list rows, funnel) and
|
|
|
// the emphasis ink + weight wherever a node stands out. The SIZE and the
|
|
|
// INK are orthogonal on purpose: `empty` and `bar-list-value` are label
|
|
|
// SIZE with caption INK, and the CSS says so.
|
|
|
'caption-font-size': 'var(--style-caption-font-size)',
|
|
|
'caption-fg': 'var(--style-caption-color)',
|
|
|
'label-font-size': 'var(--style-label-font-size)',
|
|
|
'label-fg': 'var(--style-label-color)',
|
|
|
'label-font-weight': 'var(--style-label-font-weight)',
|
|
|
// The gauge's big number is a data READOUT and no named style plays that
|
|
|
// role (an `h2` is a heading, not a datum), so this one takes the size
|
|
|
// BUNDLE. It is the escape hatch the previous commit left open.
|
|
|
'gauge-label-font-size': 'var(--size-xl-font-size)',
|
|
|
// ── The ink of everything that is NOT text (2026-08-22) ──────────────
|
|
|
// Text takes its size role from the named styles (`caption` / `label`)
|
|
|
// and its ink from the content slots — the recipe mints no typography.
|
|
|
// These four are lines and fills nothing else can name, and each is ONE
|
|
|
// concept the CSS repeats: the axis ink also draws the smith grid and the
|
|
|
// funnel connector; the grid ink also draws the radar rings; and
|
|
|
// `separator-fg` is the hairline of page surface that keeps adjacent
|
|
|
// marks apart — pie slice, active dot, smith point, polar petal, funnel
|
|
|
// segment: five parts, one idea. Naming them per part would be five keys
|
|
|
// carrying one value.
|
|
|
'axis-fg': 'var(--color-border-default)',
|
|
|
'grid-fg': 'var(--color-border-subtle)',
|
|
|
'separator-fg': 'var(--color-surface-default)',
|
|
|
// The RF disk outline is deliberately stronger than the grid it encloses.
|
|
|
'smith-boundary-fg': 'var(--color-content-secondary)',
|
|
|
// ── Rails (bar list, stacked bar, gauge) ─────────────────────────────
|
|
|
// `track-height` reads the size BUNDLE, not `--control-height-sm`: the raw
|
|
|
// coordinate is what `recipe-css-contract` forbids, and the CSS carried it
|
|
|
// (behind a dead `--space-7` fallback — the primitive does exist).
|
|
|
'track-bg': 'var(--color-surface-muted)',
|
|
|
'track-height': 'var(--size-sm-control-height)',
|
|
|
'track-radius': 'var(--radius-sm)',
|
|
|
// ── Chrome geometry ──────────────────────────────────────────────────
|
|
|
gap: 'var(--space-2)',
|
|
|
'legend-row-gap': 'var(--space-1-5)',
|
|
|
'legend-column-gap': 'var(--space-4)',
|
|
|
'legend-item-gap': 'var(--space-1-5)',
|
|
|
'swatch-size': '0.75em',
|
|
|
'swatch-radius': 'var(--radius-sm)',
|
|
|
'tooltip-swatch-size': '0.6em',
|
|
|
'bar-list-row-gap': 'var(--space-2)',
|
|
|
'bar-list-column-gap': 'var(--space-3)',
|
|
|
'bar-segment-gap': 'var(--space-3)',
|
|
|
'bar-segment-track-gap': 'var(--border-width-medium)',
|
|
|
'bar-segment-track-radius': 'var(--radius-md)',
|
|
|
'bar-segment-fill-min-inline-size': 'var(--border-width-thick)',
|
|
|
'bar-segment-legend-row-gap': 'var(--space-2)',
|
|
|
'bar-segment-legend-column-gap': 'var(--space-4)'
|
|
|
},
|
|
|
'qr-code': {
|
|
|
fg: '#18181b',
|
|
|
bg: '#ffffff',
|
|
|
'size-xs': '96px',
|
|
|
'size-sm': '128px',
|
|
|
'size-md': '160px',
|
|
|
'size-lg': '200px',
|
|
|
'size-xl': '256px',
|
|
|
radius: 'var(--radius-md)',
|
|
|
// White padding behind a center logo — FIXED-TONE for the same reason as
|
|
|
// fg/bg above (the quiet zone around the logo must stay light to scan).
|
|
|
'overlay-bg': '#ffffff',
|
|
|
'overlay-radius': 'var(--radius-sm)'
|
|
|
},
|
|
|
// RadioCards — card-style skin over RadioGroup. Reuses RadioGroup's
|
|
|
// palette (`--_radio-group-palette-solid`, declared on the shared
|
|
|
// [data-radio-group] host) and `--radio-group-focus-ring`; adds only the
|
|
|
// card chrome (padding, radius, surface, title/description type).
|
|
|
'radio-cards': {
|
|
|
gap: 'var(--space-3)',
|
|
|
// Min card width in a horizontal row before wrapping (rem → scales with
|
|
|
// root font-size / zoom).
|
|
|
'horizontal-basis': '12rem',
|
|
|
'padding-xs': 'var(--space-3)',
|
|
|
'padding-sm': 'var(--space-3)',
|
|
|
'padding-md': 'var(--space-4)',
|
|
|
'padding-lg': 'var(--space-4)',
|
|
|
'padding-xl': 'var(--space-5)',
|
|
|
radius: 'var(--radius-lg)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-border-default)',
|
|
|
'hover-border': 'var(--color-border-strong)',
|
|
|
// El borde de la tarjeta en validación fallida: propio de este skin (la
|
|
|
// tarjeta ES suya), a diferencia del indicador, que es un radio.
|
|
|
'invalid-border': 'var(--color-threat-element)',
|
|
|
bg: 'var(--color-surface-default)',
|
|
|
'content-gap': 'var(--space-3)',
|
|
|
'text-gap': 'var(--space-0-5)',
|
|
|
'title-font-family': 'var(--style-label-font-family)',
|
|
|
'title-font-size-xs': 'var(--size-sm-font-size)',
|
|
|
'title-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'title-font-size-md': 'var(--size-md-font-size)',
|
|
|
'title-font-size-lg': 'var(--size-md-font-size)',
|
|
|
'title-font-size-xl': 'var(--size-lg-font-size)',
|
|
|
'title-font-weight': 'var(--font-weight-medium)',
|
|
|
'title-line-height': 'var(--leading-ui)',
|
|
|
'title-fg': 'var(--color-content-primary)',
|
|
|
'description-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'description-font-size-sm': 'var(--size-xs-font-size)',
|
|
|
'description-font-size-md': 'var(--size-sm-font-size)',
|
|
|
'description-font-size-lg': 'var(--size-sm-font-size)',
|
|
|
'description-font-size-xl': 'var(--size-md-font-size)',
|
|
|
'description-line-height': 'var(--leading-normal)',
|
|
|
'description-fg': 'var(--color-content-secondary)',
|
|
|
// Icon follows the card's title font-size (canonical icon↔type correlation),
|
|
|
// not an inflated standalone size. A bigger card icon comes from a bigger
|
|
|
// title (the icon follows), never from inflating the icon by hand.
|
|
|
'icon-size-xs': 'var(--size-sm-icon-size)',
|
|
|
'icon-size-sm': 'var(--size-sm-icon-size)',
|
|
|
'icon-size-md': 'var(--size-md-icon-size)',
|
|
|
'icon-size-lg': 'var(--size-md-icon-size)',
|
|
|
'icon-size-xl': 'var(--size-lg-icon-size)',
|
|
|
'icon-fg': 'var(--color-content-secondary)',
|
|
|
// NOTE: the checked treatment (active border + faint tint + icon tint)
|
|
|
// is NOT a token here. It references `--_radio-group-palette-solid`,
|
|
|
// which is undefined at :root (declared only on [data-radio-group]
|
|
|
// hosts) — a :root token referencing it is invalid-at-computed-value
|
|
|
// and poisons down the tree. It's inlined in radio-cards.css instead,
|
|
|
// applied on the card item where the palette IS in scope.
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
'rating-group': {
|
|
|
'gap-xs': 'var(--space-0-5)',
|
|
|
'gap-sm': 'var(--space-1)',
|
|
|
'gap-md': 'var(--space-1-5)',
|
|
|
'gap-lg': 'var(--space-2)',
|
|
|
'gap-xl': 'var(--space-2-5)',
|
|
|
'item-size-xs': '14px',
|
|
|
'item-size-sm': '18px',
|
|
|
'item-size-md': '22px',
|
|
|
'item-size-lg': '28px',
|
|
|
'item-size-xl': '34px',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'item-fg': 'var(--color-neutral-border)',
|
|
|
'active-item-fg': 'var(--color-fulfill-solid)',
|
|
|
'partial-item-fg': 'var(--color-fulfill-solid)',
|
|
|
'item-hover-offset': '-1px',
|
|
|
'focus-radius': 'var(--radius-md)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
'search-field': {
|
|
|
'clear-hidden-opacity': '0',
|
|
|
'icon-size-xs': 'var(--size-xs-icon-size)',
|
|
|
'icon-size-sm': 'var(--size-sm-icon-size)',
|
|
|
'icon-size-md': 'var(--size-md-icon-size)',
|
|
|
'icon-size-lg': 'var(--size-lg-icon-size)',
|
|
|
'icon-size-xl': 'var(--size-xl-icon-size)',
|
|
|
'icon-fg': 'var(--color-content-muted)',
|
|
|
'icon-margin-inline-end': 'var(--space-1-5)',
|
|
|
'loading-indicator-size-xs': 'var(--size-xs-icon-size)',
|
|
|
'loading-indicator-size-sm': 'var(--size-sm-icon-size)',
|
|
|
'loading-indicator-size-md': 'var(--size-md-icon-size)',
|
|
|
'loading-indicator-size-lg': 'var(--size-md-icon-size)',
|
|
|
'loading-indicator-size-xl': 'var(--size-lg-icon-size)',
|
|
|
'loading-indicator-fg': 'var(--color-content-muted)',
|
|
|
'loading-indicator-track': 'var(--color-border-subtle)',
|
|
|
'loading-indicator-thickness': '2px',
|
|
|
'loading-indicator-duration': '900ms'
|
|
|
},
|
|
|
'password-field': {
|
|
|
// El tracking del input enmascarado (los puntos leen mejor algo sueltos) y el
|
|
|
// chip de bloq-mayús. Su peso llevaba un `500` muerto detrás del token.
|
|
|
'input-letter-spacing': 'var(--tracking-wide)',
|
|
|
'caps-radius': 'var(--radius-sm)',
|
|
|
'caps-font-weight': 'var(--font-weight-medium)',
|
|
|
// Los CUATRO peldaños del medidor de fuerza. Son una escala de intención,
|
|
|
// no cuatro colores sueltos: cada uno lee su rol —pérdida, riesgo,
|
|
|
// cumplimiento, afirmación— y un tema puede recorrer la escala entera.
|
|
|
'strength-1-bg': 'var(--color-loss-solid)',
|
|
|
'strength-2-bg': 'var(--color-risk-solid)',
|
|
|
'strength-3-bg': 'var(--color-fulfill-solid)',
|
|
|
'strength-4-bg': 'var(--color-affirm-solid)',
|
|
|
'placeholder-fg': 'var(--color-content-muted)',
|
|
|
'trigger-radius': 'var(--radius-sm)',
|
|
|
'trigger-bg': 'transparent',
|
|
|
'trigger-fg': 'var(--color-content-secondary)',
|
|
|
'icon-size-xs': 'var(--size-xs-control-height)',
|
|
|
'icon-size-sm': 'var(--size-xs-control-height)',
|
|
|
'icon-size-md': 'var(--size-sm-control-height)',
|
|
|
'icon-size-lg': 'var(--size-sm-control-height)',
|
|
|
'icon-size-xl': 'var(--size-md-control-height)',
|
|
|
'caps-fg': 'var(--color-risk-text)',
|
|
|
'caps-bg': 'color-mix(in srgb, var(--color-risk-track) 50%, transparent)',
|
|
|
'caps-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'caps-font-size-sm': 'var(--size-xs-font-size)',
|
|
|
'caps-font-size-md': 'var(--size-xs-font-size)',
|
|
|
'caps-font-size-lg': 'var(--size-sm-font-size)',
|
|
|
'caps-font-size-xl': 'var(--size-sm-font-size)',
|
|
|
'strength-segment-bg': 'var(--color-border-default)',
|
|
|
'strength-segment-gap': '4px',
|
|
|
'strength-segment-height': '6px',
|
|
|
'strength-radius': 'var(--radius-sm)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
select: {
|
|
|
'stack-gap': 'var(--space-2)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
// Per-color forwarders consumed only by the TSC v2.2 `_accent-*`
|
|
|
// cascade below. The `solid` slot is intentionally omitted —
|
|
|
// select.css doesn't read `_accent-solid`, so its forwarders
|
|
|
// would be orphans (caught by the recipe-css-contract guard).
|
|
|
'primary-text': 'var(--color-primary-text)',
|
|
|
'secondary-text': 'var(--color-secondary-text)',
|
|
|
'neutral-text': 'var(--color-neutral-text)',
|
|
|
'affirm-text': 'var(--color-affirm-text)',
|
|
|
'fulfill-track': 'var(--color-fulfill-track)',
|
|
|
'fulfill-border': 'var(--color-fulfill-border)',
|
|
|
'fulfill-text': 'var(--color-fulfill-text)',
|
|
|
'risk-text': 'var(--color-risk-text)',
|
|
|
'threat-text': 'var(--color-threat-text)',
|
|
|
'loss-track': 'var(--color-loss-track)',
|
|
|
'loss-border': 'var(--color-loss-border)',
|
|
|
'loss-text': 'var(--color-loss-text)',
|
|
|
'trigger-height-xs': 'var(--size-xs-control-height)',
|
|
|
'trigger-height-sm': 'var(--size-sm-control-height)',
|
|
|
'trigger-height-md': 'var(--size-md-control-height)',
|
|
|
'trigger-height-lg': 'var(--size-lg-control-height)',
|
|
|
'trigger-height-xl': 'var(--size-xl-control-height)',
|
|
|
'trigger-padding-inline-xs': 'var(--space-2)',
|
|
|
'trigger-padding-inline-sm': 'var(--space-2-5)',
|
|
|
'trigger-padding-inline-md': 'var(--space-3)',
|
|
|
'trigger-padding-inline-lg': 'var(--space-3-5)',
|
|
|
'trigger-padding-inline-xl': 'var(--space-4)',
|
|
|
'trigger-gap-xs': 'var(--space-1)',
|
|
|
'trigger-gap-sm': 'var(--space-1-5)',
|
|
|
'trigger-gap-md': 'var(--space-2)',
|
|
|
'trigger-gap-lg': 'var(--space-2-5)',
|
|
|
'trigger-gap-xl': 'var(--space-3)',
|
|
|
'trigger-border-width': 'var(--border-width)',
|
|
|
'trigger-border': 'var(--color-border-default)',
|
|
|
'hover-trigger-border': 'var(--color-border-strong)',
|
|
|
'trigger-radius': 'var(--radius-md)',
|
|
|
'trigger-bg': 'var(--color-surface-default)',
|
|
|
'trigger-shadow': 'var(--shadow-subtle)',
|
|
|
'trigger-fg': 'var(--color-content-primary)',
|
|
|
'trigger-indicator-fg': 'var(--color-content-secondary)',
|
|
|
'trigger-indicator-size': 'var(--size-sm-icon-size)',
|
|
|
'placeholder-fg': 'var(--color-content-muted)',
|
|
|
'disabled-border': 'var(--color-border-subtle)',
|
|
|
'disabled-bg': 'var(--color-surface-raised)',
|
|
|
'disabled-fg': 'var(--color-content-disabled)',
|
|
|
'content-z': 'var(--z-index-overlay-floating)',
|
|
|
'content-padding-block-xs': 'var(--space-0-5)',
|
|
|
'content-padding-block-sm': 'var(--space-1)',
|
|
|
'content-padding-block-md': 'var(--space-1-5)',
|
|
|
'content-padding-block-lg': 'var(--space-2)',
|
|
|
'content-padding-block-xl': 'var(--space-2-5)',
|
|
|
'content-padding-inline-xs': 'var(--space-0-5)',
|
|
|
'content-padding-inline-sm': 'var(--space-1)',
|
|
|
'content-padding-inline-md': 'var(--space-1-5)',
|
|
|
'content-padding-inline-lg': 'var(--space-2)',
|
|
|
'content-padding-inline-xl': 'var(--space-2-5)',
|
|
|
'content-min-width': '12rem',
|
|
|
'content-max-width': '24rem',
|
|
|
'content-max-height': 'min(20rem, calc(100dvh - var(--space-6)))',
|
|
|
'content-radius': 'var(--radius-lg)',
|
|
|
'content-fg': 'var(--color-content-primary)',
|
|
|
'open-duration': 'var(--duration-normal)',
|
|
|
'close-duration': 'var(--duration-fast)',
|
|
|
'viewport-gap': 'var(--space-0-5)',
|
|
|
'viewport-padding': 'var(--space-0-5)',
|
|
|
'group-padding-block': 'var(--space-1)',
|
|
|
'group-heading-padding-block': 'var(--space-1)',
|
|
|
'group-heading-padding-inline': 'var(--space-2)',
|
|
|
'group-heading-font-family': 'var(--style-label-font-family)',
|
|
|
'group-heading-font-size': 'var(--size-xs-font-size)',
|
|
|
'group-heading-font-weight': 'var(--font-weight-semibold)',
|
|
|
'group-heading-letter-spacing': '0.04em',
|
|
|
'group-heading-fg': 'var(--color-content-muted)',
|
|
|
'separator-size': 'var(--border-width)',
|
|
|
'separator-margin-block': 'var(--space-1)',
|
|
|
'separator-bg': 'var(--color-neutral-separator)',
|
|
|
'item-gap': 'var(--space-2)',
|
|
|
'item-padding-inline': 'var(--space-2)',
|
|
|
'item-fg': 'var(--color-content-primary)',
|
|
|
'item-indicator-size': 'var(--size-sm-icon-size)',
|
|
|
'item-description-mt': '0.125rem',
|
|
|
'item-description-fg': 'var(--color-content-muted)',
|
|
|
'item-description-font-size': '0.85em',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
// Restored after accidental sed-removal during switch migration.
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-border': 'var(--color-primary-border)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-border': 'var(--color-secondary-border)',
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-border': 'var(--color-neutral-border)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-border': 'var(--color-affirm-border)',
|
|
|
'risk-track': 'var(--color-risk-track)',
|
|
|
'risk-border': 'var(--color-risk-border)',
|
|
|
'threat-track': 'var(--color-threat-track)',
|
|
|
'threat-border': 'var(--color-threat-border)',
|
|
|
// TSC v2.2 — multi-part palette cascade. Select's `data-color`
|
|
|
// attribute lives on the trigger AND content parts (NOT on the
|
|
|
// root `[data-select]`), because content portals out of the
|
|
|
// document tree and must carry the color independently. The
|
|
|
// `parts: ['trigger', 'content']` field tells the generator to
|
|
|
// emit each declaration as a comma-separated rule targeting both
|
|
|
// `[data-select-trigger]` and `[data-select-content]`.
|
|
|
//
|
|
|
// 1 host default (primary) + 7 per-color cascades for each of the
|
|
|
// 3 perceptual slots that select.css consumes (`track`, `border`,
|
|
|
// `text`). The legacy CSS also declared `--_select-accent-solid`
|
|
|
// but nothing in select.css read it; that orphan stayed out of
|
|
|
// the TSC migration on purpose.
|
|
|
'_palette-track': {
|
|
|
parts: ['trigger', 'content'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--select-primary-track)', scope: 'host' },
|
|
|
{ value: 'var(--select-secondary-track)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--select-neutral-track)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--select-affirm-track)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--select-fulfill-track)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--select-risk-track)', scope: 'color:risk' },
|
|
|
{ value: 'var(--select-threat-track)', scope: 'color:threat' },
|
|
|
{ value: 'var(--select-loss-track)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
parts: ['trigger', 'content'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--select-primary-border)', scope: 'host' },
|
|
|
{ value: 'var(--select-secondary-border)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--select-neutral-border)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--select-affirm-border)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--select-fulfill-border)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--select-risk-border)', scope: 'color:risk' },
|
|
|
{ value: 'var(--select-threat-border)', scope: 'color:threat' },
|
|
|
{ value: 'var(--select-loss-border)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-text': {
|
|
|
parts: ['trigger', 'content'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--select-primary-text)', scope: 'host' },
|
|
|
{ value: 'var(--select-secondary-text)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--select-neutral-text)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--select-affirm-text)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--select-fulfill-text)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--select-risk-text)', scope: 'color:risk' },
|
|
|
{ value: 'var(--select-threat-text)', scope: 'color:threat' },
|
|
|
{ value: 'var(--select-loss-text)', scope: 'color:loss' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
toolbar: {
|
|
|
'gap-xs': 'var(--space-0-5)',
|
|
|
'gap-sm': 'var(--space-1)',
|
|
|
'gap-md': 'var(--space-1-5)',
|
|
|
'gap-lg': 'var(--space-2)',
|
|
|
'padding-inline-xs': 'var(--space-0-5)',
|
|
|
'padding-inline-sm': 'var(--space-1)',
|
|
|
'padding-inline-md': 'var(--space-1-5)',
|
|
|
'padding-inline-lg': 'var(--space-2)',
|
|
|
'padding-block-xs': 'var(--space-0-5)',
|
|
|
'padding-block-sm': 'var(--space-1)',
|
|
|
'padding-block-md': 'var(--space-1)',
|
|
|
'padding-block-lg': 'var(--space-1-5)',
|
|
|
'control-height-xs': 'var(--size-xs-control-height)',
|
|
|
'control-height-sm': 'var(--size-sm-control-height)',
|
|
|
'control-height-md': 'var(--size-md-control-height)',
|
|
|
'control-height-lg': 'var(--size-lg-control-height)',
|
|
|
'control-padding-inline-xs': 'var(--space-1-5)',
|
|
|
'control-padding-inline-sm': 'var(--space-2)',
|
|
|
'control-padding-inline-md': 'var(--space-2-5)',
|
|
|
'control-padding-inline-lg': 'var(--space-3)',
|
|
|
'control-gap-xs': 'var(--space-0-5)',
|
|
|
'control-gap-sm': 'var(--space-1)',
|
|
|
'control-gap-md': 'var(--space-1-5)',
|
|
|
'control-gap-lg': 'var(--space-2)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
radius: 'var(--radius-lg)',
|
|
|
bg: 'var(--color-surface-raised)',
|
|
|
border: 'var(--color-border-subtle)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
shadow: 'var(--shadow-subtle)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'group-gap': 'var(--space-0-5)',
|
|
|
'control-radius': 'var(--radius-md)',
|
|
|
'control-border-width': 'var(--border-width)',
|
|
|
'control-border': 'transparent',
|
|
|
'hover-control-border': 'var(--color-border-subtle)',
|
|
|
'active-control-border': 'var(--color-primary-border)',
|
|
|
'control-bg': 'transparent',
|
|
|
'active-control-bg': 'var(--color-primary-track)',
|
|
|
'control-fg': 'var(--color-content-secondary)',
|
|
|
'hover-control-fg': 'var(--color-content-primary)',
|
|
|
'active-control-fg': 'var(--color-primary-text)',
|
|
|
'separator-bg': 'var(--color-neutral-separator)',
|
|
|
'separator-thickness': 'var(--border-width)',
|
|
|
'separator-length': 'var(--size-md-control-height)',
|
|
|
'separator-radius': 'var(--radius-full)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
'file-upload': {
|
|
|
'gap-sm': 'var(--space-2)',
|
|
|
'gap-md': 'var(--space-3)',
|
|
|
'gap-lg': 'var(--space-4)',
|
|
|
'padding-sm': 'var(--space-4)',
|
|
|
'padding-md': 'var(--space-5)',
|
|
|
'padding-lg': 'var(--space-6)',
|
|
|
'control-height-sm': 'var(--size-sm-control-height)',
|
|
|
'control-height-md': 'var(--size-md-control-height)',
|
|
|
'control-height-lg': 'var(--size-lg-control-height)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'preview-size-sm': '32px',
|
|
|
'preview-size-md': '40px',
|
|
|
'preview-size-lg': '48px',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'label-fg': 'var(--color-content-secondary)',
|
|
|
'label-font-weight': 'var(--font-weight-medium)',
|
|
|
'dropzone-gap': 'var(--space-2)',
|
|
|
'dropzone-min-height': '9rem',
|
|
|
'dropzone-border-width': 'var(--border-width)',
|
|
|
'dropzone-border': 'var(--color-border-default)',
|
|
|
'dropzone-radius': 'var(--radius-lg)',
|
|
|
'dropzone-bg': 'var(--color-surface-raised)',
|
|
|
'dropzone-fg': 'var(--color-content-secondary)',
|
|
|
'button-gap': 'var(--space-1-5)',
|
|
|
'button-padding-inline': 'var(--space-3)',
|
|
|
'button-border-width': 'var(--border-width)',
|
|
|
'button-border': 'var(--color-border-subtle)',
|
|
|
'hover-button-border': 'var(--color-border-default)',
|
|
|
'button-radius': 'var(--radius-md)',
|
|
|
'button-bg': 'var(--color-surface-raised)',
|
|
|
'button-fg': 'var(--color-content-secondary)',
|
|
|
'hover-button-fg': 'var(--color-content-primary)',
|
|
|
'button-solid-fg': 'var(--color-content-on-solid)',
|
|
|
'list-gap': 'var(--space-2)',
|
|
|
'item-gap': 'var(--space-3)',
|
|
|
'item-padding': 'var(--space-2)',
|
|
|
'item-border-width': 'var(--border-width)',
|
|
|
'item-border': 'var(--color-border-subtle)',
|
|
|
'item-radius': 'var(--radius-md)',
|
|
|
'item-bg': 'var(--color-surface-raised)',
|
|
|
'item-shadow': 'var(--shadow-subtle)',
|
|
|
'preview-radius': 'var(--radius-sm)',
|
|
|
'preview-bg': 'var(--color-neutral-track)',
|
|
|
'preview-fg': 'var(--color-neutral-text)',
|
|
|
'item-name-fg': 'var(--color-content-primary)',
|
|
|
'item-name-font-weight': 'var(--font-weight-medium)',
|
|
|
'item-size-fg': 'var(--color-content-muted)',
|
|
|
'item-size-font-size': 'var(--size-xs-font-size)',
|
|
|
'progress-height': '4px',
|
|
|
'progress-radius': 'var(--radius-full)',
|
|
|
'progress-track-bg': 'var(--color-neutral-track)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-solid-hover': 'var(--color-primary-solid-hover)',
|
|
|
'primary-text': 'var(--color-primary-text)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
|
|
|
'secondary-text': 'var(--color-secondary-text)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
|
|
|
'neutral-text': 'var(--color-neutral-text)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
|
|
|
'affirm-text': 'var(--color-affirm-text)',
|
|
|
'fulfill-solid': 'var(--color-fulfill-solid)',
|
|
|
'fulfill-solid-hover': 'var(--color-fulfill-solid-hover)',
|
|
|
'fulfill-track': 'var(--color-fulfill-track)',
|
|
|
'fulfill-border': 'var(--color-fulfill-border)',
|
|
|
'fulfill-text': 'var(--color-fulfill-text)',
|
|
|
'risk-solid': 'var(--color-risk-solid)',
|
|
|
'risk-solid-hover': 'var(--color-risk-solid-hover)',
|
|
|
'risk-text': 'var(--color-risk-text)',
|
|
|
'threat-solid': 'var(--color-threat-solid)',
|
|
|
'threat-solid-hover': 'var(--color-threat-solid-hover)',
|
|
|
'threat-text': 'var(--color-threat-text)',
|
|
|
'loss-solid': 'var(--color-loss-solid)',
|
|
|
'loss-solid-hover': 'var(--color-loss-solid-hover)',
|
|
|
'loss-track': 'var(--color-loss-track)',
|
|
|
'loss-border': 'var(--color-loss-border)',
|
|
|
'loss-text': 'var(--color-loss-text)',
|
|
|
// Restored after accidental sed-removal during switch migration.
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-border': 'var(--color-primary-border)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-border': 'var(--color-secondary-border)',
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-border': 'var(--color-neutral-border)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-border': 'var(--color-affirm-border)',
|
|
|
'risk-track': 'var(--color-risk-track)',
|
|
|
'risk-border': 'var(--color-risk-border)',
|
|
|
'threat-track': 'var(--color-threat-track)',
|
|
|
'threat-border': 'var(--color-threat-border)',
|
|
|
// Private palette (TSC v2.1 · canonical `_palette-*` slots for the THM-2
|
|
|
// shared-layer forward — color="teal" + the 33 scales; renamed from
|
|
|
// `_accent-*` 2026-07-13). 8 colors, 5 slots.
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--file-upload-primary-solid)', scope: 'host' },
|
|
|
{ value: 'var(--file-upload-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--file-upload-neutral-solid)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--file-upload-affirm-solid)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--file-upload-fulfill-solid)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--file-upload-risk-solid)', scope: 'color:risk' },
|
|
|
{ value: 'var(--file-upload-threat-solid)', scope: 'color:threat' },
|
|
|
{ value: 'var(--file-upload-loss-solid)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-solid-hover': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--file-upload-primary-solid-hover)', scope: 'host' },
|
|
|
{ value: 'var(--file-upload-secondary-solid-hover)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--file-upload-neutral-solid-hover)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--file-upload-affirm-solid-hover)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--file-upload-fulfill-solid-hover)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--file-upload-risk-solid-hover)', scope: 'color:risk' },
|
|
|
{ value: 'var(--file-upload-threat-solid-hover)', scope: 'color:threat' },
|
|
|
{ value: 'var(--file-upload-loss-solid-hover)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-track': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--file-upload-primary-track)', scope: 'host' },
|
|
|
{ value: 'var(--file-upload-secondary-track)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--file-upload-neutral-track)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--file-upload-affirm-track)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--file-upload-fulfill-track)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--file-upload-risk-track)', scope: 'color:risk' },
|
|
|
{ value: 'var(--file-upload-threat-track)', scope: 'color:threat' },
|
|
|
{ value: 'var(--file-upload-loss-track)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--file-upload-primary-border)', scope: 'host' },
|
|
|
{ value: 'var(--file-upload-secondary-border)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--file-upload-neutral-border)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--file-upload-affirm-border)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--file-upload-fulfill-border)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--file-upload-risk-border)', scope: 'color:risk' },
|
|
|
{ value: 'var(--file-upload-threat-border)', scope: 'color:threat' },
|
|
|
{ value: 'var(--file-upload-loss-border)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-text': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--file-upload-primary-text)', scope: 'host' },
|
|
|
{ value: 'var(--file-upload-secondary-text)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--file-upload-neutral-text)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--file-upload-affirm-text)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--file-upload-fulfill-text)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--file-upload-risk-text)', scope: 'color:risk' },
|
|
|
{ value: 'var(--file-upload-threat-text)', scope: 'color:threat' },
|
|
|
{ value: 'var(--file-upload-loss-text)', scope: 'color:loss' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
'tag-group': {
|
|
|
'gap-xs': 'var(--space-0-5)',
|
|
|
'gap-sm': 'var(--space-1)',
|
|
|
'gap-md': 'var(--space-1-5)',
|
|
|
'gap-lg': 'var(--space-2)',
|
|
|
'item-height-xs': 'var(--size-xs-control-height)',
|
|
|
'item-height-sm': 'var(--size-sm-control-height)',
|
|
|
'item-height-md': 'var(--size-md-control-height)',
|
|
|
'item-height-lg': 'var(--size-lg-control-height)',
|
|
|
'item-padding-inline-xs': 'var(--space-1-5)',
|
|
|
'item-padding-inline-sm': 'var(--space-2)',
|
|
|
'item-padding-inline-md': 'var(--space-2-5)',
|
|
|
'item-padding-inline-lg': 'var(--space-3)',
|
|
|
'item-gap-xs': 'var(--space-0-5)',
|
|
|
'item-gap-sm': 'var(--space-1)',
|
|
|
'item-gap-md': 'var(--space-1-5)',
|
|
|
'item-gap-lg': 'var(--space-2)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'label-fg': 'var(--color-content-secondary)',
|
|
|
'label-font-weight': 'var(--font-weight-medium)',
|
|
|
'item-radius': 'var(--radius-full)',
|
|
|
'item-border-width': 'var(--border-width)',
|
|
|
'item-border': 'var(--color-border-subtle)',
|
|
|
'hover-item-border': 'var(--color-border-default)',
|
|
|
'item-bg': 'var(--color-surface-raised)',
|
|
|
'item-shadow': 'var(--shadow-subtle)',
|
|
|
'item-fg': 'var(--color-content-secondary)',
|
|
|
'hover-item-fg': 'var(--color-content-primary)',
|
|
|
'solid-fg': 'var(--color-content-on-solid)',
|
|
|
'remove-size': 'var(--size-xs-control-height)',
|
|
|
'remove-radius': 'var(--radius-full)',
|
|
|
'remove-border-width': 'var(--border-width)',
|
|
|
'remove-border': 'transparent',
|
|
|
'hover-remove-border': 'var(--color-border-subtle)',
|
|
|
'remove-bg': 'transparent',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'primary-solid-bg': 'var(--color-primary-solid)',
|
|
|
'hover-primary-solid-bg': 'var(--color-primary-solid-hover)',
|
|
|
'primary-soft-bg': 'var(--color-primary-surface)',
|
|
|
'hover-primary-soft-bg': 'var(--color-primary-surface-hover)',
|
|
|
'primary-soft-fg': 'var(--color-primary-text)',
|
|
|
'primary-outline-border': 'var(--color-primary-border)',
|
|
|
'secondary-solid-bg': 'var(--color-secondary-solid)',
|
|
|
'hover-secondary-solid-bg': 'var(--color-secondary-solid-hover)',
|
|
|
'secondary-soft-bg': 'var(--color-secondary-surface)',
|
|
|
'hover-secondary-soft-bg': 'var(--color-secondary-surface-hover)',
|
|
|
'secondary-soft-fg': 'var(--color-secondary-text)',
|
|
|
'secondary-outline-border': 'var(--color-secondary-border)',
|
|
|
'neutral-solid-bg': 'var(--color-neutral-solid)',
|
|
|
'hover-neutral-solid-bg': 'var(--color-neutral-solid-hover)',
|
|
|
'neutral-soft-bg': 'var(--color-neutral-surface)',
|
|
|
'hover-neutral-soft-bg': 'var(--color-neutral-surface-hover)',
|
|
|
'neutral-soft-fg': 'var(--color-neutral-text)',
|
|
|
'neutral-outline-border': 'var(--color-neutral-border)',
|
|
|
'affirm-solid-bg': 'var(--color-affirm-solid)',
|
|
|
'hover-affirm-solid-bg': 'var(--color-affirm-solid-hover)',
|
|
|
'affirm-soft-bg': 'var(--color-affirm-surface)',
|
|
|
'hover-affirm-soft-bg': 'var(--color-affirm-surface-hover)',
|
|
|
'affirm-soft-fg': 'var(--color-affirm-text)',
|
|
|
'affirm-outline-border': 'var(--color-affirm-border)',
|
|
|
'fulfill-solid-bg': 'var(--color-fulfill-solid)',
|
|
|
'hover-fulfill-solid-bg': 'var(--color-fulfill-solid-hover)',
|
|
|
'fulfill-soft-bg': 'var(--color-fulfill-surface)',
|
|
|
'hover-fulfill-soft-bg': 'var(--color-fulfill-surface-hover)',
|
|
|
'fulfill-soft-fg': 'var(--color-fulfill-text)',
|
|
|
'fulfill-outline-border': 'var(--color-fulfill-border)',
|
|
|
'risk-solid-bg': 'var(--color-risk-solid)',
|
|
|
'hover-risk-solid-bg': 'var(--color-risk-solid-hover)',
|
|
|
'risk-soft-bg': 'var(--color-risk-surface)',
|
|
|
'hover-risk-soft-bg': 'var(--color-risk-surface-hover)',
|
|
|
'risk-soft-fg': 'var(--color-risk-text)',
|
|
|
'risk-outline-border': 'var(--color-risk-border)',
|
|
|
'threat-solid-bg': 'var(--color-threat-solid)',
|
|
|
'hover-threat-solid-bg': 'var(--color-threat-solid-hover)',
|
|
|
'threat-soft-bg': 'var(--color-threat-surface)',
|
|
|
'hover-threat-soft-bg': 'var(--color-threat-surface-hover)',
|
|
|
'threat-soft-fg': 'var(--color-threat-text)',
|
|
|
'threat-outline-border': 'var(--color-threat-border)',
|
|
|
'loss-solid-bg': 'var(--color-loss-solid)',
|
|
|
'hover-loss-solid-bg': 'var(--color-loss-solid-hover)',
|
|
|
'loss-soft-bg': 'var(--color-loss-surface)',
|
|
|
'hover-loss-soft-bg': 'var(--color-loss-surface-hover)',
|
|
|
'loss-soft-fg': 'var(--color-loss-text)',
|
|
|
'loss-outline-border': 'var(--color-loss-border)',
|
|
|
// Private palette (TSC v2.1 · canonical `_palette-*` slots so the THM-2
|
|
|
// shared-layer forward routes color="teal" + the 33 scales, incl. the
|
|
|
// alpha `surface`/`surface-hover` slots for the soft variant; renamed
|
|
|
// from `_selected-*` 2026-07-12). solid + solid-hover (solid variant) ·
|
|
|
// surface + surface-hover + text (soft variant) · border + text (outline
|
|
|
// variant — its fg merged into `text`, same value). Cascade keyed on
|
|
|
// `data-color`.
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--tag-group-primary-solid-bg)', scope: 'host' },
|
|
|
{ value: 'var(--tag-group-secondary-solid-bg)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--tag-group-neutral-solid-bg)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--tag-group-affirm-solid-bg)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--tag-group-fulfill-solid-bg)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--tag-group-risk-solid-bg)', scope: 'color:risk' },
|
|
|
{ value: 'var(--tag-group-threat-solid-bg)', scope: 'color:threat' },
|
|
|
{ value: 'var(--tag-group-loss-solid-bg)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-solid-hover': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--tag-group-hover-primary-solid-bg)', scope: 'host' },
|
|
|
{ value: 'var(--tag-group-hover-secondary-solid-bg)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--tag-group-hover-neutral-solid-bg)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--tag-group-hover-affirm-solid-bg)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--tag-group-hover-fulfill-solid-bg)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--tag-group-hover-risk-solid-bg)', scope: 'color:risk' },
|
|
|
{ value: 'var(--tag-group-hover-threat-solid-bg)', scope: 'color:threat' },
|
|
|
{ value: 'var(--tag-group-hover-loss-solid-bg)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-surface': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--tag-group-primary-soft-bg)', scope: 'host' },
|
|
|
{ value: 'var(--tag-group-secondary-soft-bg)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--tag-group-neutral-soft-bg)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--tag-group-affirm-soft-bg)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--tag-group-fulfill-soft-bg)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--tag-group-risk-soft-bg)', scope: 'color:risk' },
|
|
|
{ value: 'var(--tag-group-threat-soft-bg)', scope: 'color:threat' },
|
|
|
{ value: 'var(--tag-group-loss-soft-bg)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-surface-hover': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--tag-group-hover-primary-soft-bg)', scope: 'host' },
|
|
|
{ value: 'var(--tag-group-hover-secondary-soft-bg)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--tag-group-hover-neutral-soft-bg)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--tag-group-hover-affirm-soft-bg)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--tag-group-hover-fulfill-soft-bg)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--tag-group-hover-risk-soft-bg)', scope: 'color:risk' },
|
|
|
{ value: 'var(--tag-group-hover-threat-soft-bg)', scope: 'color:threat' },
|
|
|
{ value: 'var(--tag-group-hover-loss-soft-bg)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-text': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--tag-group-primary-soft-fg)', scope: 'host' },
|
|
|
{ value: 'var(--tag-group-secondary-soft-fg)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--tag-group-neutral-soft-fg)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--tag-group-affirm-soft-fg)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--tag-group-fulfill-soft-fg)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--tag-group-risk-soft-fg)', scope: 'color:risk' },
|
|
|
{ value: 'var(--tag-group-threat-soft-fg)', scope: 'color:threat' },
|
|
|
{ value: 'var(--tag-group-loss-soft-fg)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--tag-group-primary-outline-border)', scope: 'host' },
|
|
|
{ value: 'var(--tag-group-secondary-outline-border)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--tag-group-neutral-outline-border)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--tag-group-affirm-outline-border)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--tag-group-fulfill-outline-border)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--tag-group-risk-outline-border)', scope: 'color:risk' },
|
|
|
{ value: 'var(--tag-group-threat-outline-border)', scope: 'color:threat' },
|
|
|
{ value: 'var(--tag-group-loss-outline-border)', scope: 'color:loss' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
editable: {
|
|
|
'gap-xs': 'var(--space-0-5)',
|
|
|
'gap-sm': 'var(--space-1)',
|
|
|
'gap-md': 'var(--space-1-5)',
|
|
|
'gap-lg': 'var(--space-2)',
|
|
|
'gap-xl': 'var(--space-2-5)',
|
|
|
'control-height-xs': 'var(--size-xs-control-height)',
|
|
|
'control-height-sm': 'var(--size-sm-control-height)',
|
|
|
'control-height-md': 'var(--size-md-control-height)',
|
|
|
'control-height-lg': 'var(--size-lg-control-height)',
|
|
|
'control-height-xl': 'var(--size-xl-control-height)',
|
|
|
'control-padding-inline-xs': 'var(--space-1-5)',
|
|
|
'control-padding-inline-sm': 'var(--space-2)',
|
|
|
'control-padding-inline-md': 'var(--space-2-5)',
|
|
|
'control-padding-inline-lg': 'var(--space-3)',
|
|
|
'control-padding-inline-xl': 'var(--space-3-5)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'area-min-inline-size': '12ch',
|
|
|
'control-radius': 'var(--radius-md)',
|
|
|
'control-border-width': 'var(--border-width)',
|
|
|
'control-border': 'var(--color-border-subtle)',
|
|
|
'hover-control-border': 'var(--color-border-default)',
|
|
|
'control-bg': 'var(--color-surface-default)',
|
|
|
'control-shadow': 'var(--shadow-subtle)',
|
|
|
'placeholder-fg': 'var(--color-content-muted)',
|
|
|
'trigger-gap': 'var(--space-1)',
|
|
|
'trigger-padding-inline': 'var(--space-2-5)',
|
|
|
'trigger-radius': 'var(--radius-md)',
|
|
|
'trigger-border-width': 'var(--border-width)',
|
|
|
'trigger-border': 'var(--color-border-subtle)',
|
|
|
'hover-trigger-border': 'var(--color-border-default)',
|
|
|
'trigger-bg': 'var(--color-surface-raised)',
|
|
|
'trigger-fg': 'var(--color-content-secondary)',
|
|
|
'hover-trigger-fg': 'var(--color-content-primary)',
|
|
|
'submit-fg': 'var(--color-content-on-solid)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'risk-solid': 'var(--color-risk-solid)',
|
|
|
'threat-solid': 'var(--color-threat-solid)',
|
|
|
// Restored after accidental sed-removal during switch migration.
|
|
|
'primary-border': 'var(--color-primary-border)',
|
|
|
'secondary-border': 'var(--color-secondary-border)',
|
|
|
'neutral-border': 'var(--color-neutral-border)',
|
|
|
'affirm-border': 'var(--color-affirm-border)',
|
|
|
'risk-border': 'var(--color-risk-border)',
|
|
|
'threat-border': 'var(--color-threat-border)',
|
|
|
// Private palette (TSC v2.1 · canonical `_palette-*` slots for the THM-2
|
|
|
// shared-layer forward — color="teal" + the 33 scales; renamed from
|
|
|
// `_accent-*` 2026-07-13). solid + border + solid-hover consumed.
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--editable-primary-solid)', scope: 'host' },
|
|
|
{ value: 'var(--editable-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--editable-neutral-solid)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--editable-affirm-solid)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--editable-risk-solid)', scope: 'color:risk' },
|
|
|
{ value: 'var(--editable-threat-solid)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--editable-primary-border)', scope: 'host' },
|
|
|
{ value: 'var(--editable-secondary-border)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--editable-neutral-border)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--editable-affirm-border)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--editable-risk-border)', scope: 'color:risk' },
|
|
|
{ value: 'var(--editable-threat-border)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-solid-hover': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--color-primary-solid-hover)', scope: 'host' },
|
|
|
{ value: 'var(--color-secondary-solid-hover)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--color-neutral-solid-hover)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--color-affirm-solid-hover)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--color-risk-solid-hover)', scope: 'color:risk' },
|
|
|
{ value: 'var(--color-threat-solid-hover)', scope: 'color:threat' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
'tags-input': {
|
|
|
'gap-xs': 'var(--space-1)',
|
|
|
'gap-sm': 'var(--space-1-5)',
|
|
|
'gap-md': 'var(--space-2)',
|
|
|
'gap-lg': 'var(--space-2-5)',
|
|
|
'gap-xl': 'var(--space-3)',
|
|
|
'control-min-height-xs': 'var(--size-sm-control-height)',
|
|
|
'control-min-height-sm': 'var(--size-md-control-height)',
|
|
|
'control-min-height-md': 'var(--size-lg-control-height)',
|
|
|
'control-min-height-lg': 'calc(var(--size-lg-control-height) + var(--space-2))',
|
|
|
'control-min-height-xl': 'calc(var(--size-xl-control-height) + var(--space-2))',
|
|
|
'control-padding-inline-xs': 'var(--space-1-5)',
|
|
|
'control-padding-inline-sm': 'var(--space-2)',
|
|
|
'control-padding-inline-md': 'var(--space-2-5)',
|
|
|
'control-padding-inline-lg': 'var(--space-3)',
|
|
|
'control-padding-inline-xl': 'var(--space-3-5)',
|
|
|
'control-padding-block-xs': 'var(--space-0-5)',
|
|
|
'control-padding-block-sm': 'var(--space-1)',
|
|
|
'control-padding-block-md': 'var(--space-1-5)',
|
|
|
'control-padding-block-lg': 'var(--space-2)',
|
|
|
'control-padding-block-xl': 'var(--space-2-5)',
|
|
|
'item-height-xs': 'var(--size-xs-control-height)',
|
|
|
'item-height-sm': 'var(--size-sm-control-height)',
|
|
|
'item-height-md': 'var(--size-md-control-height)',
|
|
|
'item-height-lg': 'var(--size-lg-control-height)',
|
|
|
'item-height-xl': 'var(--size-xl-control-height)',
|
|
|
'item-padding-inline-xs': 'var(--space-1-5)',
|
|
|
'item-padding-inline-sm': 'var(--space-2)',
|
|
|
'item-padding-inline-md': 'var(--space-2-5)',
|
|
|
'item-padding-inline-lg': 'var(--space-3)',
|
|
|
'item-padding-inline-xl': 'var(--space-3-5)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'item-gap': 'var(--space-1-5)',
|
|
|
'control-radius': 'var(--radius-lg)',
|
|
|
'control-border-width': 'var(--border-width)',
|
|
|
'control-border': 'var(--color-border-default)',
|
|
|
'hover-control-border': 'var(--color-border-strong)',
|
|
|
'control-bg': 'var(--color-surface-default)',
|
|
|
'control-shadow': 'var(--shadow-subtle)',
|
|
|
'input-min-inline-size': '8ch',
|
|
|
'placeholder-fg': 'var(--color-content-muted)',
|
|
|
'item-radius': 'var(--radius-full)',
|
|
|
'item-border-width': 'var(--border-width)',
|
|
|
'item-border': 'var(--color-border-subtle)',
|
|
|
'item-bg': 'var(--color-surface-overlay)',
|
|
|
'item-fg': 'var(--color-content-primary)',
|
|
|
'action-gap': 'var(--space-1)',
|
|
|
'action-padding-inline': 'var(--space-2-5)',
|
|
|
'action-radius': 'var(--radius-md)',
|
|
|
'action-border-width': 'var(--border-width)',
|
|
|
'action-border': 'transparent',
|
|
|
'hover-action-border': 'var(--color-border-subtle)',
|
|
|
'action-bg': 'transparent',
|
|
|
'action-fg': 'var(--color-content-secondary)',
|
|
|
'hover-action-fg': 'var(--color-content-primary)',
|
|
|
'delete-size': 'var(--size-xs-control-height)',
|
|
|
'delete-radius': 'var(--radius-full)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'primary-text': 'var(--color-primary-text)',
|
|
|
'secondary-text': 'var(--color-secondary-text)',
|
|
|
'neutral-text': 'var(--color-neutral-text)',
|
|
|
'affirm-text': 'var(--color-affirm-text)',
|
|
|
'fulfill-track': 'var(--color-fulfill-track)',
|
|
|
'fulfill-border': 'var(--color-fulfill-border)',
|
|
|
'fulfill-text': 'var(--color-fulfill-text)',
|
|
|
'risk-text': 'var(--color-risk-text)',
|
|
|
'threat-text': 'var(--color-threat-text)',
|
|
|
'loss-track': 'var(--color-loss-track)',
|
|
|
'loss-border': 'var(--color-loss-border)',
|
|
|
'loss-text': 'var(--color-loss-text)',
|
|
|
// Restored after accidental sed-removal during switch migration.
|
|
|
// Consumed by tags-input.css for tag chip color cues per intent.
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-border': 'var(--color-primary-border)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-border': 'var(--color-secondary-border)',
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-border': 'var(--color-neutral-border)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-border': 'var(--color-affirm-border)',
|
|
|
'risk-track': 'var(--color-risk-track)',
|
|
|
'risk-border': 'var(--color-risk-border)',
|
|
|
'threat-track': 'var(--color-threat-track)',
|
|
|
'threat-border': 'var(--color-threat-border)',
|
|
|
// Private palette (TSC v2.1 · canonical `_palette-*` slots so the
|
|
|
// THM-2 shared-layer forward routes `color="teal"` + the 33 scales;
|
|
|
// renamed from `_accent-*` 2026-07-13). 8 colors, 3 slots (track,
|
|
|
// border, text). `solid` was declared pre-TSC but never consumed.
|
|
|
'_palette-track': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--tags-input-primary-track)', scope: 'host' },
|
|
|
{ value: 'var(--tags-input-secondary-track)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--tags-input-neutral-track)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--tags-input-affirm-track)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--tags-input-fulfill-track)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--tags-input-risk-track)', scope: 'color:risk' },
|
|
|
{ value: 'var(--tags-input-threat-track)', scope: 'color:threat' },
|
|
|
{ value: 'var(--tags-input-loss-track)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--tags-input-primary-border)', scope: 'host' },
|
|
|
{ value: 'var(--tags-input-secondary-border)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--tags-input-neutral-border)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--tags-input-affirm-border)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--tags-input-fulfill-border)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--tags-input-risk-border)', scope: 'color:risk' },
|
|
|
{ value: 'var(--tags-input-threat-border)', scope: 'color:threat' },
|
|
|
{ value: 'var(--tags-input-loss-border)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-text': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--tags-input-primary-text)', scope: 'host' },
|
|
|
{ value: 'var(--tags-input-secondary-text)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--tags-input-neutral-text)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--tags-input-affirm-text)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--tags-input-fulfill-text)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--tags-input-risk-text)', scope: 'color:risk' },
|
|
|
{ value: 'var(--tags-input-threat-text)', scope: 'color:threat' },
|
|
|
{ value: 'var(--tags-input-loss-text)', scope: 'color:loss' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
metrics: {
|
|
|
// El ritmo del trend y las tres FORMAS del icono, una por valor del eje
|
|
|
// `data-shape`: el radio es lo único que las distingue, así que cada forma
|
|
|
// lleva el suyo.
|
|
|
'trend-gap': 'var(--space-2)',
|
|
|
'icon-circle-radius': 'var(--radius-full)',
|
|
|
'icon-rounded-radius': 'var(--radius-lg)',
|
|
|
'icon-square-radius': 'var(--radius-md)',
|
|
|
// Metrics is SURFACELESS — the Delta's colour comes from the composed
|
|
|
// <Badge>; the live flash tint uses the canonical `--color-*` tokens
|
|
|
// directly (it grades by EVENT intent, not by the `color` prop). The
|
|
|
// featured icon routes through the shared palette forward below.
|
|
|
// ── Vertical rhythm between label / value / delta / description ──
|
|
|
'gap-xs': 'var(--space-1)',
|
|
|
'gap-sm': 'var(--space-1)',
|
|
|
'gap-md': 'var(--space-1-5)',
|
|
|
'gap-lg': 'var(--space-2)',
|
|
|
'gap-xl': 'var(--space-2)',
|
|
|
'gap-xxl': 'var(--space-3)',
|
|
|
// ── Featured icon (leading container; soft/solid/outline × colour from
|
|
|
// canonical tokens in CSS; padding + glyph size scale with the metric
|
|
|
// size; the shape drives the radius from canonical `--radius-*`). ──
|
|
|
'icon-gap-xs': 'var(--space-2)',
|
|
|
'icon-gap-sm': 'var(--space-3)',
|
|
|
'icon-gap-md': 'var(--space-4)',
|
|
|
'icon-gap-lg': 'var(--space-4)',
|
|
|
'icon-gap-xl': 'var(--space-4)',
|
|
|
'icon-gap-xxl': 'var(--space-5)',
|
|
|
// Explicit featured-icon BOX (off the type scale on purpose — the Untitled
|
|
|
// "featured icon" is a fixed 48px square at lg; the glyph centres inside via
|
|
|
// flex). Decoupled from font changes so it never drifts. Justified off-ladder.
|
|
|
'icon-box-xs': '1.75rem',
|
|
|
'icon-box-sm': '2rem',
|
|
|
'icon-box-md': '2.5rem',
|
|
|
'icon-box-lg': '3rem',
|
|
|
'icon-box-xl': '3.5rem',
|
|
|
'icon-box-xxl': '4.5rem',
|
|
|
'icon-border-width': 'var(--border-width)',
|
|
|
'icon-bg': 'var(--color-surface-muted)',
|
|
|
'icon-fg': 'var(--color-content-secondary)',
|
|
|
// Featured-icon palette forward (THM-2) — the five slots the variant rules
|
|
|
// read, on the `icon` part. Host = primary (the stamped default); shared
|
|
|
// layer routes roles + 33 scales + custom. `plain` stamps nothing and
|
|
|
// keeps the base `icon-bg` / `icon-color` defaults.
|
|
|
'_palette-track': {
|
|
|
parts: ['icon'],
|
|
|
declarations: [{ value: 'var(--color-primary-track)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-solid': {
|
|
|
parts: ['icon'],
|
|
|
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-text': {
|
|
|
parts: ['icon'],
|
|
|
declarations: [{ value: 'var(--color-primary-text)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-contrast': {
|
|
|
parts: ['icon'],
|
|
|
declarations: [{ value: 'var(--color-primary-contrast)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
parts: ['icon'],
|
|
|
declarations: [{ value: 'var(--color-primary-border)', scope: 'host' }]
|
|
|
},
|
|
|
// ── Label ──
|
|
|
'label-font-family': 'var(--style-label-font-family)',
|
|
|
'label-font-size-xs': 'var(--size-xxs-font-size)',
|
|
|
'label-font-size-sm': 'var(--size-xs-font-size)',
|
|
|
'label-font-size-md': 'var(--size-sm-font-size)',
|
|
|
'label-font-size-lg': 'var(--size-md-font-size)',
|
|
|
'label-font-size-xl': 'var(--size-lg-font-size)',
|
|
|
'label-font-size-xxl': 'var(--size-xl-font-size)',
|
|
|
'label-font-weight': 'var(--font-weight-medium)',
|
|
|
'label-fg': 'var(--color-content-secondary)',
|
|
|
'label-line-height': 'var(--leading-ui)',
|
|
|
'label-letter-spacing': 'var(--tracking-normal)',
|
|
|
// ── Value (headline figure, tabular numerals) ──
|
|
|
// REBASED UP the type scale: the value is the only large element, ~2 steps
|
|
|
// above the label, so the small-label → big-number jump (the canonical KPI
|
|
|
// hierarchy) actually happens. NOT 1:1 with the size name — that collapsed
|
|
|
// the hierarchy (md value == label). Still token-driven + theme-overridable.
|
|
|
'value-font-family': 'var(--style-label-font-family)',
|
|
|
'value-font-size-xs': 'var(--size-md-font-size)',
|
|
|
'value-font-size-sm': 'var(--size-lg-font-size)',
|
|
|
'value-font-size-md': 'var(--size-xl-font-size)',
|
|
|
'value-font-size-lg': 'var(--size-xxl-font-size)',
|
|
|
'value-font-size-xl': 'var(--font-size-xxxl)',
|
|
|
'value-font-size-xxl': 'var(--font-size-xxxl)',
|
|
|
'value-font-weight': 'var(--font-weight-semibold)',
|
|
|
'value-fg': 'var(--color-content-primary)',
|
|
|
'value-line-height': 'var(--leading-tight)',
|
|
|
'value-letter-spacing': 'var(--tracking-tight)',
|
|
|
'value-font-feature': 'var(--font-feature-tabular)',
|
|
|
// ── Description ──
|
|
|
'description-font-family': 'var(--style-label-font-family)',
|
|
|
'description-font-size-xs': 'var(--size-xxs-font-size)',
|
|
|
'description-font-size-sm': 'var(--size-xs-font-size)',
|
|
|
'description-font-size-md': 'var(--size-sm-font-size)',
|
|
|
'description-font-size-lg': 'var(--size-md-font-size)',
|
|
|
'description-font-size-xl': 'var(--size-lg-font-size)',
|
|
|
'description-font-size-xxl': 'var(--size-xl-font-size)',
|
|
|
'description-fg': 'var(--color-content-secondary)',
|
|
|
'description-line-height': 'var(--leading-normal)',
|
|
|
// ── Trend chart (sparkline) slot ──
|
|
|
'chart-margin-block-start': 'var(--space-3)',
|
|
|
'chart-right-width': '7rem',
|
|
|
// ── Progress / gauge to goal (compose <Meter>) ──
|
|
|
'progress-margin-block-start': 'var(--space-3)',
|
|
|
'progress-gap': 'var(--space-1-5)',
|
|
|
'gauge-value-font-size': 'var(--size-sm-font-size)',
|
|
|
'gauge-value-font-weight': 'var(--font-weight-semibold)',
|
|
|
'gauge-value-fg': 'var(--color-content-primary)',
|
|
|
// ── Actions row (View report link / dropdown) ──
|
|
|
'actions-gap': 'var(--space-3)',
|
|
|
'actions-margin-block-start': 'var(--space-3)',
|
|
|
'actions-padding-block-start': 'var(--space-3)',
|
|
|
'actions-border-fg': 'var(--color-border-subtle)',
|
|
|
// ── Live-update flash on the value (data-event-family='signal') ──
|
|
|
'flash-duration': 'var(--duration-slow)',
|
|
|
'flash-ease': 'var(--ease-out)',
|
|
|
'flash-radius': 'var(--radius-sm)',
|
|
|
'flash-inset': 'var(--space-1)'
|
|
|
},
|
|
|
stepper: {
|
|
|
'gap-sm': 'var(--space-3)',
|
|
|
'gap-md': 'var(--space-4)',
|
|
|
'gap-lg': 'var(--space-5)',
|
|
|
'indicator-size-sm': 'var(--size-sm-control-height)',
|
|
|
'indicator-size-md': 'var(--size-md-control-height)',
|
|
|
'indicator-size-lg': 'var(--size-lg-control-height)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'list-gap': 'var(--space-1)',
|
|
|
'item-gap': 'var(--space-2)',
|
|
|
'trigger-gap': 'var(--space-2)',
|
|
|
'trigger-padding-inline': 'var(--space-2)',
|
|
|
'trigger-padding-block': 'var(--space-1)',
|
|
|
'trigger-radius': 'var(--radius-md)',
|
|
|
'trigger-border-width': 'var(--border-width)',
|
|
|
'trigger-fg': 'var(--color-content-secondary)',
|
|
|
'hover-trigger-fg': 'var(--color-content-primary)',
|
|
|
'indicator-radius': 'var(--radius-full)',
|
|
|
'indicator-border-width': 'var(--border-width)',
|
|
|
'indicator-border': 'var(--color-border-default)',
|
|
|
'indicator-bg': 'var(--color-surface-raised)',
|
|
|
'indicator-fg': 'var(--color-content-secondary)',
|
|
|
'indicator-font-weight': 'var(--font-weight-semibold)',
|
|
|
'complete-fg': 'var(--color-content-on-solid)',
|
|
|
'separator-min-size': '3rem',
|
|
|
'separator-thickness': 'var(--border-width-medium)',
|
|
|
'separator-radius': 'var(--radius-full)',
|
|
|
'separator-bg': 'var(--color-neutral-separator)',
|
|
|
'content-padding': 'var(--space-4)',
|
|
|
'content-radius': 'var(--radius-lg)',
|
|
|
'content-border-width': 'var(--border-width)',
|
|
|
'content-border': 'var(--color-border-subtle)',
|
|
|
'content-bg': 'var(--color-surface-raised)',
|
|
|
'content-shadow': 'var(--shadow-subtle)',
|
|
|
'nav-height': 'var(--size-md-control-height)',
|
|
|
'nav-padding-inline': 'var(--space-3)',
|
|
|
'nav-radius': 'var(--radius-md)',
|
|
|
'nav-border-width': 'var(--border-width)',
|
|
|
'nav-border': 'var(--color-border-subtle)',
|
|
|
'nav-bg': 'var(--color-surface-raised)',
|
|
|
'nav-fg': 'var(--color-content-primary)',
|
|
|
'nav-hover-brightness': '1.04',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-text': 'var(--color-primary-text)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-text': 'var(--color-secondary-text)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-text': 'var(--color-neutral-text)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-text': 'var(--color-affirm-text)',
|
|
|
'fulfill-solid': 'var(--color-fulfill-solid)',
|
|
|
'fulfill-track': 'var(--color-fulfill-track)',
|
|
|
'fulfill-border': 'var(--color-fulfill-border)',
|
|
|
'fulfill-text': 'var(--color-fulfill-text)',
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-border': 'var(--color-primary-border)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-border': 'var(--color-secondary-border)',
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-border': 'var(--color-neutral-border)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-border': 'var(--color-affirm-border)',
|
|
|
// Private palette (TSC v2.1 · canonical `_palette-*` slots for the
|
|
|
// THM-2 shared-layer forward — color="teal" + the 33 scales; renamed
|
|
|
// from `_accent-*` 2026-07-13). Per-color cascade in config.
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--stepper-primary-solid)', scope: 'host' },
|
|
|
{ value: 'var(--stepper-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--stepper-neutral-solid)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--stepper-affirm-solid)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--stepper-fulfill-solid)', scope: 'color:fulfill' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-track': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--stepper-primary-track)', scope: 'host' },
|
|
|
{ value: 'var(--stepper-secondary-track)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--stepper-neutral-track)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--stepper-affirm-track)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--stepper-fulfill-track)', scope: 'color:fulfill' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--stepper-primary-border)', scope: 'host' },
|
|
|
{ value: 'var(--stepper-secondary-border)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--stepper-neutral-border)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--stepper-affirm-border)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--stepper-fulfill-border)', scope: 'color:fulfill' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-text': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--stepper-primary-text)', scope: 'host' },
|
|
|
{ value: 'var(--stepper-secondary-text)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--stepper-neutral-text)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--stepper-affirm-text)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--stepper-fulfill-text)', scope: 'color:fulfill' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
timeline: {
|
|
|
// Status-accent palette forward (THM-2): host = primary; the shared layer
|
|
|
// routes roles + 33 scales + custom into `--_timeline-palette-*`, which the
|
|
|
// item's `--_timeline-hue-*` reads (data-intent still overrides per item).
|
|
|
// Was a per-role cascade over `--_timeline-accent-*` in timeline.css.
|
|
|
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
|
|
|
'_palette-track': { value: 'var(--color-primary-track)', scope: 'host' },
|
|
|
'_palette-border': { value: 'var(--color-primary-border)', scope: 'host' },
|
|
|
'_palette-contrast': { value: 'var(--color-primary-contrast)', scope: 'host' },
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' },
|
|
|
// ── Rhythm (size-scaled) ──
|
|
|
'gap-sm': 'var(--space-3)',
|
|
|
'gap-md': 'var(--space-4)',
|
|
|
'gap-lg': 'var(--space-5)',
|
|
|
'item-gap-sm': 'var(--space-5)',
|
|
|
'item-gap-md': 'var(--space-6)',
|
|
|
'item-gap-lg': 'var(--space-7)',
|
|
|
'marker-size-sm': 'var(--size-sm-control-height)',
|
|
|
'marker-size-md': 'var(--size-md-control-height)',
|
|
|
'marker-size-lg': 'var(--size-lg-control-height)',
|
|
|
'marker-icon-size-sm': 'var(--size-sm-icon-size)',
|
|
|
'marker-icon-size-md': 'var(--size-md-icon-size)',
|
|
|
'marker-icon-size-lg': 'var(--size-lg-icon-size)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
// Time meta sits one step below the item's font size (field-label rhythm).
|
|
|
'time-font-size-sm': 'var(--size-xs-font-size)',
|
|
|
'time-font-size-md': 'var(--size-sm-font-size)',
|
|
|
'time-font-size-lg': 'var(--size-md-font-size)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
// ── Marker (idle / default treatment) ──
|
|
|
'marker-radius': 'var(--radius-full)',
|
|
|
'marker-border-width': 'var(--border-width-medium)',
|
|
|
'marker-border': 'var(--color-border-default)',
|
|
|
'marker-bg': 'var(--color-surface-default)',
|
|
|
'marker-fg': 'var(--color-content-secondary)',
|
|
|
'marker-font-weight': 'var(--font-weight-semibold)',
|
|
|
'marker-ring-width': 'var(--space-1)',
|
|
|
// ── Connector ──
|
|
|
'connector-thickness': 'var(--border-width-medium)',
|
|
|
'connector-fg': 'var(--color-border-subtle)',
|
|
|
'connector-min-size': 'var(--space-6)',
|
|
|
// Comfortable width for a horizontal entry so its text doesn't collapse
|
|
|
// when the rail overflows (the timeline scrolls instead of shrinking).
|
|
|
'item-inline': '10rem',
|
|
|
// Fixed region heights for horizontal `align=alternate`: the content and
|
|
|
// time rows are pinned so the marker lands at a constant offset and the
|
|
|
// rail stays straight across entries. Keep horizontal-alternate content
|
|
|
// concise (title + one description line).
|
|
|
'alt-content-block': '3.25rem',
|
|
|
'alt-time-block': '1.5rem',
|
|
|
// ── Content / time / title / description ──
|
|
|
'content-gap': 'var(--space-1)',
|
|
|
'time-font-weight': 'var(--font-weight-medium)',
|
|
|
'time-fg': 'var(--color-content-secondary)',
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'title-fg': 'var(--color-content-primary)',
|
|
|
'description-fg': 'var(--color-content-secondary)',
|
|
|
// ── Live-feed reveal ──
|
|
|
'reveal-duration': 'var(--duration-moderate)',
|
|
|
'reveal-ease': 'var(--ease-out)',
|
|
|
'reveal-distance': 'var(--space-3)',
|
|
|
// ── Transitions ──
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)'
|
|
|
},
|
|
|
slider: {
|
|
|
'track-size-xs': '3px',
|
|
|
'track-size-sm': '4px',
|
|
|
'track-size-md': '6px',
|
|
|
'track-size-lg': '8px',
|
|
|
'track-size-xl': '10px',
|
|
|
'thumb-size-xs': '12px',
|
|
|
'thumb-size-sm': '16px',
|
|
|
'thumb-size-md': '20px',
|
|
|
'thumb-size-lg': '24px',
|
|
|
'thumb-size-xl': '28px',
|
|
|
'hit-size-xs': '24px',
|
|
|
'hit-size-sm': '28px',
|
|
|
'hit-size-md': '32px',
|
|
|
'hit-size-lg': '36px',
|
|
|
'hit-size-xl': '40px',
|
|
|
'tick-size-xs': '3px',
|
|
|
'tick-size-sm': '4px',
|
|
|
'tick-size-md': '5px',
|
|
|
'tick-size-lg': '6px',
|
|
|
'tick-size-xl': '7px',
|
|
|
'min-inline-size': '12rem',
|
|
|
'min-block-size': '10rem',
|
|
|
'track-radius': 'var(--radius-full)',
|
|
|
// The unfilled groove. `--color-neutral-track` equals the surface in the
|
|
|
// base theme (invisible against panels), so the track uses the same
|
|
|
// visible-track mix as Progress: a light tint of content-primary on the
|
|
|
// surface. Reads on white AND dark, and follows the theme.
|
|
|
'track-bg':
|
|
|
'color-mix(in srgb, var(--color-content-primary) 12%, var(--color-surface-default))',
|
|
|
'range-bg': 'var(--color-primary-solid)',
|
|
|
// Secondary track (buffer / preloaded region): sits BETWEEN the track and
|
|
|
// the range in weight — solid enough to read on the 12% track mix, muted
|
|
|
// enough that the value range stays the dominant fill on top of it.
|
|
|
'secondary-bg': 'color-mix(in srgb, var(--color-primary-solid) 28%, transparent)',
|
|
|
'thumb-radius': 'var(--radius-full)',
|
|
|
'thumb-border-width': 'var(--border-width)',
|
|
|
'thumb-border': 'var(--color-border-default)',
|
|
|
'thumb-bg': 'var(--color-surface-default)',
|
|
|
'thumb-shadow': 'var(--shadow-subtle)',
|
|
|
// Pickup lift WHILE grabbed (pointer :active): the thumb scales toward the
|
|
|
// viewer + rides a higher shadow — the draggable-surface lift doctrine
|
|
|
// (THEMING §14), scaled up from the surface `--motion-scale-lift` (1.02)
|
|
|
// because a ~20px thumb needs a larger multiplier to read as a lift.
|
|
|
'active-thumb-scale': '1.15',
|
|
|
'active-thumb-shadow': 'var(--shadow-3)',
|
|
|
'tick-radius': 'var(--radius-full)',
|
|
|
'tick-bg': 'var(--color-neutral-border)',
|
|
|
'active-tick-bg': 'var(--color-primary-solid)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
'media-player': {
|
|
|
// Projector accent — a component-owned colour (not a role), theme-stable
|
|
|
// so the scrubber reads over arbitrary video in light AND dark. Overridable
|
|
|
// per theme via `--media-player-accent`. The scrubber consumes it through
|
|
|
// the Slider's own `--slider-range-bg` / `--slider-thumb-bg` tokens, so it
|
|
|
// stays a system Slider — only its colour is re-tinted.
|
|
|
accent: 'var(--scale-amber-9)',
|
|
|
'accent-strong': 'var(--scale-amber-10)',
|
|
|
|
|
|
// ── Chrome ──
|
|
|
// The `--_mp-*` private layer this recipe used to carry was an ABBREVIATED
|
|
|
// prefix (theming §6 r5 forbids it) AND a pure alias of its own source, so
|
|
|
// it is not renamed: it is gone. Every knob below is public.
|
|
|
bg: 'var(--color-surface-default)',
|
|
|
radius: 'var(--radius-lg)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'fg-muted': 'var(--color-content-secondary)',
|
|
|
// The control bar's glass and its hairline. Both ride over arbitrary
|
|
|
// video, which is why they are mixes of a surface rather than a flat role.
|
|
|
glass: 'color-mix(in srgb, var(--color-surface-overlay) 82%, transparent)',
|
|
|
border: 'color-mix(in srgb, var(--color-content-primary) 10%, transparent)',
|
|
|
// ONE scrim colour; the recipe paints TWO gradients from it (bottom for
|
|
|
// the control strip, top for the title). Two tokens would let a theme
|
|
|
// break the pair without meaning to.
|
|
|
scrim: 'color-mix(in srgb, var(--color-surface-default) 75%, transparent)',
|
|
|
'scrim-height': '42%',
|
|
|
// The unfilled rail behind both scrubbers, and the buffered sub-track.
|
|
|
track: 'color-mix(in srgb, var(--color-content-primary) 22%, transparent)',
|
|
|
'buffered-bg': 'color-mix(in srgb, var(--color-content-primary) 42%, transparent)',
|
|
|
|
|
|
// ── Captions ──
|
|
|
// Denser than the bar's glass on purpose: this one carries prose over
|
|
|
// moving frames rather than icons over a scrim.
|
|
|
'captions-bg': 'color-mix(in srgb, var(--color-surface-overlay) 94%, transparent)',
|
|
|
'captions-fg': 'var(--color-content-primary)',
|
|
|
'captions-radius': 'var(--radius-sm)',
|
|
|
'captions-gap': 'var(--space-1)',
|
|
|
'captions-padding-block': 'var(--space-1)',
|
|
|
'captions-padding-inline': 'var(--space-2)',
|
|
|
|
|
|
// ── Title overlay ──
|
|
|
'title-font-size': 'var(--size-md-font-size)',
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'title-padding-block': 'var(--space-3)',
|
|
|
'title-padding-inline': 'var(--space-4)',
|
|
|
|
|
|
// ── Buffering spinner ──
|
|
|
'buffering-size': 'var(--space-8)',
|
|
|
'buffering-border-width': '3px',
|
|
|
|
|
|
// ── Per-size coordinates (sm | md | lg | xl) - all 1:1 with the bundle ──
|
|
|
'gap-sm': 'var(--size-sm-gap)',
|
|
|
'gap-md': 'var(--size-md-gap)',
|
|
|
'gap-lg': 'var(--size-lg-gap)',
|
|
|
'gap-xl': 'var(--size-xl-gap)',
|
|
|
'padding-block-sm': 'var(--size-sm-padding-block)',
|
|
|
'padding-block-md': 'var(--size-md-padding-block)',
|
|
|
'padding-block-lg': 'var(--size-lg-padding-block)',
|
|
|
'padding-block-xl': 'var(--size-xl-padding-block)',
|
|
|
'padding-inline-sm': 'var(--size-sm-padding-inline)',
|
|
|
'padding-inline-md': 'var(--size-md-padding-inline)',
|
|
|
'padding-inline-lg': 'var(--size-lg-padding-inline)',
|
|
|
'padding-inline-xl': 'var(--size-xl-padding-inline)',
|
|
|
'time-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'time-font-size-md': 'var(--size-md-font-size)',
|
|
|
'time-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'time-font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'control-height-sm': 'var(--size-sm-control-height)',
|
|
|
'control-height-md': 'var(--size-md-control-height)',
|
|
|
'control-height-lg': 'var(--size-lg-control-height)',
|
|
|
'control-height-xl': 'var(--size-xl-control-height)',
|
|
|
|
|
|
// The RESOLVED names - the only ones the recipe reads. `data-size` is on
|
|
|
// the provider, so `host` scope is enough.
|
|
|
gap: {
|
|
|
declarations: [
|
|
|
{ value: 'var(--media-player-gap-md)', scope: 'host' },
|
|
|
{ value: 'var(--media-player-gap-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--media-player-gap-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--media-player-gap-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'padding-block': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--media-player-padding-block-md)', scope: 'host' },
|
|
|
{ value: 'var(--media-player-padding-block-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--media-player-padding-block-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--media-player-padding-block-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'padding-inline': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--media-player-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--media-player-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--media-player-padding-inline-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--media-player-padding-inline-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'time-font-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--media-player-time-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--media-player-time-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--media-player-time-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--media-player-time-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'control-height': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--media-player-control-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--media-player-control-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--media-player-control-height-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--media-player-control-height-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// ── Scrubbers ──
|
|
|
// Named because the TIME scrubber floors itself against the volume one:
|
|
|
// the main control of a bar must never render narrower than the volume
|
|
|
// beside it, and two literals would drift apart the first time one moves.
|
|
|
'volume-width': 'calc(var(--space-8) * 2)',
|
|
|
'volume-vertical-length': 'calc(var(--space-8) * 3)',
|
|
|
'time-slider-min-width': 'var(--space-8)',
|
|
|
'rate-button-min-width': '4.6em',
|
|
|
'buffering-radius': 'var(--radius-full)',
|
|
|
'captions-line-height': 'var(--font-line-height-md)',
|
|
|
'time-font-family': 'var(--font-family-mono)',
|
|
|
'time-padding-inline': 'var(--space-2)',
|
|
|
// The audio shape drops the overlay: the title is in the flow, so it wears
|
|
|
// its own tighter padding instead of the video overlay's.
|
|
|
'audio-title-padding-block': 'var(--space-2)',
|
|
|
'audio-title-padding-inline': 'var(--space-3)',
|
|
|
// The floating volume panel: block padding clears the THUMB's overhang, so
|
|
|
// the two axes differ on purpose.
|
|
|
'volume-float-padding-block': 'var(--space-6)',
|
|
|
'volume-float-padding-inline': 'var(--space-2)'
|
|
|
},
|
|
|
'audio-player': {
|
|
|
// The audio variants' public knobs (F5, PLAN-audio-player-v2.md). Artwork
|
|
|
// sizes are design values of each identity: `card` is the hero cap, `row`
|
|
|
// matches a list entry (SoundCloud-track scale), `bar` a persistent app bar
|
|
|
// (Spotify scale) — public tokens, so a theme retunes them like the
|
|
|
// slider's `track-size-*`.
|
|
|
gap: 'var(--space-3)',
|
|
|
// La pieza tipográfica que ESTE reproductor añade sobre el chasis: el
|
|
|
// título en peso fuerte y la insignia LIVE en versalitas espaciadas.
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'live-font-weight': 'var(--font-weight-semibold)',
|
|
|
'live-letter-spacing': '0.08em',
|
|
|
'artwork-radius': 'var(--radius-md)',
|
|
|
// `card` is a CAP (the artwork is `inline-size: 100%` up to this), and it
|
|
|
// is deliberately expressed on the space scale rather than as a flat rem:
|
|
|
// `--space-8` already carries density × scaling composed, so the hero
|
|
|
// artwork breathes with the foundation while the two compact sizes below
|
|
|
// stay fixed — a list row and an app bar must NOT grow with density, or
|
|
|
// they stop matching the rows and bars around them. 256px at 1×/1×.
|
|
|
'artwork-size-card': 'calc(var(--space-8) * 8)',
|
|
|
'artwork-size-row': '3.5rem',
|
|
|
'artwork-size-bar': '2.5rem'
|
|
|
},
|
|
|
waveform: {
|
|
|
// Waveform (D-WF, PLAN-waveform.md) — the audio-signal painting. The
|
|
|
// interaction chrome is the embedded canonical Slider, re-tinted by the
|
|
|
// recipe (track/range transparent, thumb = thin playhead); these tokens
|
|
|
// own only what the Slider does not have: the painting itself.
|
|
|
'height-xs': '1.5rem',
|
|
|
'height-sm': '2rem',
|
|
|
'height-md': '2.5rem',
|
|
|
'height-lg': '3.5rem',
|
|
|
'height-xl': '4.5rem',
|
|
|
// Bar thickness in viewBox units (unitless stroke-width): the svg
|
|
|
// stretches with `preserveAspectRatio="none"`, so bars keep a constant
|
|
|
// bar/gap duty cycle at any rendered width — 0.6 of a bucket slot at
|
|
|
// the wave's 100-unit width reads as the SoundCloud density.
|
|
|
'bar-width': '0.6',
|
|
|
// Unplayed signal: quiet tint of content over any surface; played takes
|
|
|
// the same role colour the Slider's range uses, so both stay theme-led.
|
|
|
fg: 'color-mix(in srgb, var(--color-content-primary) 28%, transparent)',
|
|
|
'played-fg': 'var(--color-primary-solid)',
|
|
|
// Buffered / preloaded: the middle rung. It is the SAME wave path clipped
|
|
|
// to the buffered edge, so it only ever shows ahead of the playhead (the
|
|
|
// played copy paints after and wins).
|
|
|
//
|
|
|
// It carries the PLAYED hue, washed — "loaded, on its way to heard". A
|
|
|
// lighter NEUTRAL was tried first and rejected ON SIGHT: separated from the
|
|
|
// unplayed wave by lightness alone, it read as more of the same grey. The
|
|
|
// HUE is what makes it a third state.
|
|
|
//
|
|
|
// Composited against the base theme: rgb(148,112,178) light /
|
|
|
// rgb(130,95,161) dark. Note the raw rgb distance to the unplayed wave is
|
|
|
// slightly SMALLER than the neutral's (89/98 vs 102/104) — that metric is
|
|
|
// lightness-dominated and blind to chroma, which is exactly why it endorsed
|
|
|
// a colour the eye rejected. The number to watch instead is the distance to
|
|
|
// the PLAYED accent (40/42): wash further if the two ever blur.
|
|
|
//
|
|
|
// The 55/20 pair sums to 75, so color-mix normalises the ratio to 73:27 AND
|
|
|
// takes the result to alpha .75 — the wash is that alpha. Retune freely:
|
|
|
// this is a public token, the designer's knob.
|
|
|
'buffered-fg':
|
|
|
'color-mix(in srgb, var(--color-primary-solid) 55%, var(--color-content-primary) 20%)',
|
|
|
// The embedded Slider's thumb restyled as a thin full-height playhead —
|
|
|
// a shape its square `--slider-thumb-size-*` tokens cannot express.
|
|
|
'playhead-width': '2px',
|
|
|
// …and its colour, the PAIR `playhead-width` was missing. Every other
|
|
|
// line of the re-tint already reads a token of this component's own
|
|
|
// (`--slider-secondary-bg: var(--waveform-buffered-fg)`) or an identity
|
|
|
// (`transparent` / `none`); the thumb was the one passing a RAW primitive
|
|
|
// (`--color-content-primary`) into a composed component, so the only way
|
|
|
// to repaint the playhead was to move every Slider in the app. Same shape
|
|
|
// as media-player's re-tint, which forwards `--media-player-*` throughout.
|
|
|
'playhead-bg': 'var(--color-content-primary)'
|
|
|
},
|
|
|
background: {
|
|
|
// ── Patterns — the knobs `Backdrop` kept as inline fallbacks, promoted to
|
|
|
// theme data so a product can retune the decoration without a recipe fork.
|
|
|
'pattern-rule': 'var(--color-border-subtle)',
|
|
|
'pattern-cell': '2.5rem',
|
|
|
'pattern-dot-size': '1.5px',
|
|
|
'pattern-glow-size': '60% 55%',
|
|
|
'pattern-glow-at': '50% 0%',
|
|
|
'pattern-glow-strength': '28%',
|
|
|
// A mesh is a veil at scrim weight — the same 0.45 Backdrop shipped, now
|
|
|
// tied to the semantic scale instead of a literal (R-4.2).
|
|
|
// The mesh pattern IS a system gradient — the seam: the VALUE belongs to
|
|
|
// the gradient catalogue, the KNOB belongs to this component, so a theme
|
|
|
// can give the mesh another image without moving `--gradient-aurora`
|
|
|
// everywhere it is used. (Measured 2026-08-23: it paints as a longhand
|
|
|
// `background-image` here — this token's serialisation starts with a
|
|
|
// radial-gradient, so the mesh-through-`background-image` trap does not
|
|
|
// apply to it.)
|
|
|
'pattern-mesh-image': 'var(--gradient-aurora)',
|
|
|
'pattern-mesh-opacity': 'var(--opacity-scrim)',
|
|
|
'pattern-noise-opacity': 'var(--opacity-ghost)',
|
|
|
'pattern-vignette-strength': '38%',
|
|
|
'pattern-lines-width': '1px',
|
|
|
'pattern-lines-gap': '0.5rem',
|
|
|
'pattern-lines-angle': '45deg',
|
|
|
'pattern-rings-width': '1px',
|
|
|
'pattern-rings-gap': '4rem',
|
|
|
// ── Fade (the edge mask) ────────────────────────────────────────────
|
|
|
'fade-size': '75% 75%',
|
|
|
'fade-at': '50% 35%',
|
|
|
// ── Scrim — the FLOOR of the ink in force, and the veil's own weights ──
|
|
|
//
|
|
|
// The ink: a scrim exists so the copy above it stays legible, so its
|
|
|
// colour is the FLOOR of whatever ink `on` put in force (D12, §39). It
|
|
|
// used to be `--color-overlay`, which is `surface.backdrop` — the modal
|
|
|
// dim (MD3/Radix/Vaul), tuned PER MODE because a light page needs less
|
|
|
// attenuation than a dark one. Borrowed as a legibility tool that brought
|
|
|
// two defects: the same photograph read 1.48:1 in light and 2.10:1 in
|
|
|
// dark (a photograph does not change with the mode), and its own alpha
|
|
|
// (0.42 / 0.66) capped the whole scale at 2.66:1 — no weight could reach
|
|
|
// AA. Now the ink is opaque and picked by context, so weight IS alpha.
|
|
|
'scrim-fg': 'var(--color-surface-default)',
|
|
|
'scrim-fg-over-dark': 'var(--color-content-on-solid-contrast)',
|
|
|
'scrim-fg-over-light': 'var(--color-content-on-solid)',
|
|
|
// The weights: five steps by the WORK each one does, measured with the
|
|
|
// framework's own maths (`$color` apcaLc + wcagContrastRatio) over the
|
|
|
// worst artwork of each context. `md` is the default and holds the 0.189
|
|
|
// the hero shipped — in light mode, identical to the digit, because the
|
|
|
// old backdrop's hue WAS this floor. `xl` is the only step that promises
|
|
|
// legibility over ANY photograph: it clears BOTH framework floors in BOTH
|
|
|
// contexts (on-solid's APCA ≥ 60 ∧ WCAG ≥ 3, and §40's AA 4.5 for text) —
|
|
|
// measured `on='dark'` 6.45:1 / Lc 85, `on='light'` 8.29:1 / Lc 61.
|
|
|
// The frost behind the scrim, one step per `blur` value. Same seam as the
|
|
|
// mesh above: the scale is the system's (`--blur-*`), the knob is the
|
|
|
// component's — a theme can soften the scrim's frost without touching
|
|
|
// every blur in the app. `blur` is a METRIC family of the `scaling` axis
|
|
|
// (theming §23), so these must stay tokens, never literals.
|
|
|
'scrim-blur-sm': 'var(--blur-sm)',
|
|
|
'scrim-blur-md': 'var(--blur-md)',
|
|
|
'scrim-blur-lg': 'var(--blur-lg)',
|
|
|
'scrim-blur-xl': 'var(--blur-xl)',
|
|
|
'scrim-blur-xxl': 'var(--blur-xxl)',
|
|
|
'scrim-strength-xs': '0.08',
|
|
|
'scrim-strength-sm': '0.13',
|
|
|
'scrim-strength-md': '0.19',
|
|
|
'scrim-strength-lg': '0.4',
|
|
|
'scrim-strength-xl': '0.7',
|
|
|
// ── Gradient drift (the `animate` layer) ────────────────────────────
|
|
|
'gradient-drift-duration': '24s',
|
|
|
// ── The pause control (WCAG 2.2.2) ──────────────────────────────────
|
|
|
// Composed from primitives that are already themeable: the space scale
|
|
|
// for the inset, and the `--z-index-*` ladder for the plane. The control
|
|
|
// only has to clear the host's own content, not the app's overlays — a
|
|
|
// dialog or a popover opening over this surface must still cover it,
|
|
|
// which is why this sits far below `--z-index-affix` (150).
|
|
|
'pause-offset': 'var(--space-3)',
|
|
|
'pause-z': 'var(--z-index-raised)',
|
|
|
// ── Parallax + pointer ──────────────────────────────────────────────
|
|
|
// The travel a layer covers at `speed: 1` across the whole scrollport.
|
|
|
// Config data, not a magic number in the recipe: a theme that wants a
|
|
|
// calmer page turns this down once and every background follows.
|
|
|
'parallax-travel': '4rem',
|
|
|
'spotlight-size': '40% 40%',
|
|
|
'spotlight-strength': '18%',
|
|
|
// The light's own ink. A separate token because a spotlight over LIGHT
|
|
|
// artwork needs a dark light — the scrim has the same escape (`scrim-color`)
|
|
|
// for the same reason.
|
|
|
'spotlight-fg': 'var(--color-content-on-solid)'
|
|
|
},
|
|
|
surface: {
|
|
|
// Surface — the themeable canvas (Box + treatment): the palette-tint
|
|
|
// mechanism of Card WITHOUT the chrome (no header slots, no border by
|
|
|
// default, no interactivity). Rides THM-2 (color axis) + the gradient
|
|
|
// finish (§39). Default = neutral, like Card.
|
|
|
radius: 'var(--radius-lg)',
|
|
|
// Layer-4 per-color slots — 3 slots × 8 colors (track/solid/contrast:
|
|
|
// exactly what soft/solid consume; border/text pruned by the orphan
|
|
|
// guard — Surface has no outline/text variant BY DECISION).
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-contrast': 'var(--color-neutral-contrast)',
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-contrast': 'var(--color-primary-contrast)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-contrast': 'var(--color-secondary-contrast)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-contrast': 'var(--color-affirm-contrast)',
|
|
|
'fulfill-track': 'var(--color-fulfill-track)',
|
|
|
'fulfill-solid': 'var(--color-fulfill-solid)',
|
|
|
'fulfill-contrast': 'var(--color-fulfill-contrast)',
|
|
|
'risk-track': 'var(--color-risk-track)',
|
|
|
'risk-solid': 'var(--color-risk-solid)',
|
|
|
'risk-contrast': 'var(--color-risk-contrast)',
|
|
|
'threat-track': 'var(--color-threat-track)',
|
|
|
'threat-solid': 'var(--color-threat-solid)',
|
|
|
'threat-contrast': 'var(--color-threat-contrast)',
|
|
|
'loss-track': 'var(--color-loss-track)',
|
|
|
'loss-solid': 'var(--color-loss-solid)',
|
|
|
'loss-contrast': 'var(--color-loss-contrast)',
|
|
|
// Private palette (canonical `_palette-*` slots → the THM-2 forward
|
|
|
// routes color="teal" + the 33 scales). 8 colors, 3 slots.
|
|
|
'_palette-track': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--surface-neutral-track)', scope: 'host' },
|
|
|
{ value: 'var(--surface-primary-track)', scope: 'color:primary' },
|
|
|
{ value: 'var(--surface-secondary-track)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--surface-affirm-track)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--surface-fulfill-track)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--surface-risk-track)', scope: 'color:risk' },
|
|
|
{ value: 'var(--surface-threat-track)', scope: 'color:threat' },
|
|
|
{ value: 'var(--surface-loss-track)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--surface-neutral-solid)', scope: 'host' },
|
|
|
{ value: 'var(--surface-primary-solid)', scope: 'color:primary' },
|
|
|
{ value: 'var(--surface-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--surface-affirm-solid)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--surface-fulfill-solid)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--surface-risk-solid)', scope: 'color:risk' },
|
|
|
{ value: 'var(--surface-threat-solid)', scope: 'color:threat' },
|
|
|
{ value: 'var(--surface-loss-solid)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-contrast': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--surface-neutral-contrast)', scope: 'host' },
|
|
|
{ value: 'var(--surface-primary-contrast)', scope: 'color:primary' },
|
|
|
{ value: 'var(--surface-secondary-contrast)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--surface-affirm-contrast)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--surface-fulfill-contrast)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--surface-risk-contrast)', scope: 'color:risk' },
|
|
|
{ value: 'var(--surface-threat-contrast)', scope: 'color:threat' },
|
|
|
{ value: 'var(--surface-loss-contrast)', scope: 'color:loss' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
switch: {
|
|
|
'size-xs-track-width': '26px',
|
|
|
'size-xs-track-height': '15px',
|
|
|
'size-xs-thumb-size': '11px',
|
|
|
'size-xs-thumb-translate': '12px',
|
|
|
'size-sm-track-width': '32px',
|
|
|
'size-sm-track-height': '18px',
|
|
|
'size-sm-thumb-size': '14px',
|
|
|
'size-sm-thumb-translate': '14px',
|
|
|
'size-md-track-width': '38px',
|
|
|
'size-md-track-height': '22px',
|
|
|
'size-md-thumb-size': '18px',
|
|
|
'size-md-thumb-translate': '16px',
|
|
|
'size-lg-track-width': '46px',
|
|
|
'size-lg-track-height': '28px',
|
|
|
'size-lg-thumb-size': '24px',
|
|
|
'size-lg-thumb-translate': '18px',
|
|
|
'size-xl-track-width': '54px',
|
|
|
'size-xl-track-height': '34px',
|
|
|
'size-xl-thumb-size': '30px',
|
|
|
'size-xl-thumb-translate': '20px',
|
|
|
'track-radius': 'var(--radius-full)',
|
|
|
'track-border-width': 'var(--border-width)',
|
|
|
'thumb-inset': '1px',
|
|
|
'thumb-radius': 'var(--radius-full)',
|
|
|
'thumb-border-width': 'var(--border-width)',
|
|
|
'thumb-shadow': '0 1px 2px rgb(17 14 10 / 0.12)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'track-bg-off': 'var(--color-surface-overlay)',
|
|
|
'track-border-off': 'var(--color-border-default)',
|
|
|
'hover-track-border-off': 'var(--color-border-strong)',
|
|
|
'disabled-track-bg':
|
|
|
'color-mix( in srgb, var(--color-surface-overlay) 70%, var(--color-surface-default) )',
|
|
|
'disabled-track-border':
|
|
|
'color-mix( in srgb, var(--color-border-default) 70%, var(--color-border-subtle) )',
|
|
|
'disabled-thumb-bg':
|
|
|
'color-mix( in srgb, var(--color-surface-default) 88%, var(--color-surface-raised) )',
|
|
|
'disabled-thumb-border':
|
|
|
'color-mix( in srgb, var(--color-border-subtle) 88%, var(--color-surface-overlay) )',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'thumb-bg': 'var(--color-surface-default)',
|
|
|
'thumb-border':
|
|
|
'color-mix( in srgb, var(--color-border-default) 82%, var(--color-surface-overlay) )',
|
|
|
'thumb-icon-fg': 'var(--color-content-muted)',
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-solid-hover': 'var(--color-primary-solid-hover)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
|
|
|
'risk-solid': 'var(--color-risk-solid)',
|
|
|
'risk-solid-hover': 'var(--color-risk-solid-hover)',
|
|
|
'threat-solid': 'var(--color-threat-solid)',
|
|
|
'threat-solid-hover': 'var(--color-threat-solid-hover)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
|
|
|
// Private palette (TSC v2.1). Host default = neutral, plus
|
|
|
// per-color overrides. Migrated from manual blocks in switch.css.
|
|
|
// Only solid/solid-hover are actively consumed; track/border
|
|
|
// existed in the pre-TSC defaults but were never referenced.
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--switch-neutral-solid)', scope: 'host' },
|
|
|
{ value: 'var(--switch-primary-solid)', scope: 'color:primary' },
|
|
|
{ value: 'var(--switch-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--switch-affirm-solid)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--switch-risk-solid)', scope: 'color:risk' },
|
|
|
{ value: 'var(--switch-threat-solid)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-solid-hover': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--switch-neutral-solid-hover)', scope: 'host' },
|
|
|
{ value: 'var(--switch-primary-solid-hover)', scope: 'color:primary' },
|
|
|
{ value: 'var(--switch-secondary-solid-hover)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--switch-affirm-solid-hover)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--switch-risk-solid-hover)', scope: 'color:risk' },
|
|
|
{ value: 'var(--switch-threat-solid-hover)', scope: 'color:threat' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
'color-swatch': {
|
|
|
// Dedicated swatch scale — a colour chip is a SUBSTANTIAL, tappable sample,
|
|
|
// NOT an inline glyph, so it is bigger than the icon scale (which read
|
|
|
// ridiculously small). Values match the prior ColorPicker swatch (20/24/28
|
|
|
// for sm/md/lg) + ends; follows --scaling like the other metrics.
|
|
|
'size-xs': 'calc(16px * var(--scaling))',
|
|
|
'size-sm': 'calc(20px * var(--scaling))',
|
|
|
'size-md': 'calc(24px * var(--scaling))',
|
|
|
'size-lg': 'calc(28px * var(--scaling))',
|
|
|
'size-xl': 'calc(32px * var(--scaling))',
|
|
|
radius: 'var(--radius-sm)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
'border-fg': 'var(--color-border-subtle)',
|
|
|
// The sampled colour — set per-instance inline by the wrapper; default
|
|
|
// transparent (an empty swatch shows the checker). Public knob.
|
|
|
fill: 'transparent',
|
|
|
// Transparency checker — mode-aware neutral base + a slightly darker square,
|
|
|
// so alpha colours read on a subtle grid (never a hard white).
|
|
|
'checker-cell': '6px',
|
|
|
'checker-bg': 'var(--color-surface-default)',
|
|
|
'checker-square': 'var(--color-content-muted)'
|
|
|
},
|
|
|
tabs: {
|
|
|
'stack-gap': 'var(--space-4)',
|
|
|
'line-stack-gap': 'var(--space-3)',
|
|
|
'provider-radius': 'var(--radius-xl)',
|
|
|
'provider-border-width': 'var(--border-width)',
|
|
|
'provider-border': 'var(--color-border-default)',
|
|
|
'provider-bg': 'var(--color-surface-raised)',
|
|
|
'surface-provider-padding': 'var(--space-3)',
|
|
|
'surface-provider-shadow': 'var(--shadow-subtle)',
|
|
|
'list-gap': 'var(--space-1)',
|
|
|
'list-padding': 'var(--space-1)',
|
|
|
'list-radius': 'var(--radius-lg)',
|
|
|
'list-width': '220px',
|
|
|
'list-bg': 'transparent',
|
|
|
'list-border-width': 'var(--border-width)',
|
|
|
'list-border': 'var(--color-border-default)',
|
|
|
'line-list-padding-inline-end': 'var(--space-2)',
|
|
|
'surface-list-bg': 'color-mix(in srgb, var(--color-surface-default) 82%, transparent)',
|
|
|
'pills-list-bg': 'color-mix(in srgb, var(--color-surface-default) 90%, transparent)',
|
|
|
'trigger-height-xs': 'var(--size-xs-control-height)',
|
|
|
'trigger-height-sm': 'var(--size-sm-control-height)',
|
|
|
'trigger-height-md': 'var(--size-md-control-height)',
|
|
|
'trigger-height-lg': 'var(--size-lg-control-height)',
|
|
|
'trigger-padding-inline-xs': 'var(--space-2-5)',
|
|
|
'trigger-padding-inline-sm': 'var(--space-3)',
|
|
|
'trigger-padding-inline-md': 'var(--space-3-5)',
|
|
|
'trigger-padding-inline-lg': 'var(--space-4)',
|
|
|
'trigger-gap-xs': 'var(--space-1)',
|
|
|
'trigger-gap-sm': 'var(--space-1-5)',
|
|
|
'trigger-gap-md': 'var(--space-2)',
|
|
|
'trigger-gap-lg': 'var(--space-2)',
|
|
|
'trigger-radius': 'var(--radius-md)',
|
|
|
'trigger-border-width': 'var(--border-width)',
|
|
|
'trigger-font-family': 'var(--style-label-font-family)',
|
|
|
'trigger-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'trigger-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'trigger-font-size-md': 'var(--size-md-font-size)',
|
|
|
'trigger-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'trigger-font-weight': 'var(--font-weight-medium)',
|
|
|
'trigger-line-height': 'var(--leading-ui)',
|
|
|
'trigger-letter-spacing': 'var(--tracking-normal)',
|
|
|
'trigger-fg': 'var(--color-content-secondary)',
|
|
|
'hover-trigger-fg': 'var(--color-content-primary)',
|
|
|
'active-trigger-fg': 'var(--color-content-primary)',
|
|
|
'trigger-bg': 'transparent',
|
|
|
'active-trigger-bg': 'var(--color-surface-overlay)',
|
|
|
'trigger-border': 'transparent',
|
|
|
'active-trigger-border': 'var(--color-primary-border)',
|
|
|
'trigger-transition-duration': 'var(--duration-fast)',
|
|
|
'trigger-transition-ease': 'var(--ease-default)',
|
|
|
'trigger-focus-ring-offset': 'var(--focus-ring-offset)',
|
|
|
'trigger-focus-ring-width': 'var(--focus-ring-width)',
|
|
|
'trigger-focus-ring-color': 'var(--focus-ring-color)',
|
|
|
'active-surface-trigger-border':
|
|
|
'color-mix( in srgb, var(--tabs-active-trigger-border) 58%, transparent )',
|
|
|
'pills-trigger-radius': 'var(--radius-full)',
|
|
|
'active-pills-trigger-border':
|
|
|
'color-mix( in srgb, var(--tabs-active-trigger-border) 58%, transparent )',
|
|
|
'active-pills-trigger-shadow': 'var(--shadow-subtle)',
|
|
|
'segmented-list-bg': 'var(--color-surface-muted)',
|
|
|
'segmented-list-radius': 'var(--radius-md)',
|
|
|
'segmented-trigger-radius':
|
|
|
'max(0px, calc(var(--tabs-segmented-list-radius) - var(--tabs-list-padding)))',
|
|
|
'active-segmented-trigger-bg': 'var(--color-primary-solid)',
|
|
|
'active-segmented-trigger-fg': 'var(--color-content-on-solid)',
|
|
|
'active-segmented-trigger-shadow': 'var(--shadow-subtle)',
|
|
|
'content-padding-xs': 'var(--space-3)',
|
|
|
'content-padding-sm': 'var(--space-4)',
|
|
|
'content-padding-md': 'var(--space-5)',
|
|
|
'content-padding-lg': 'var(--space-6)',
|
|
|
'content-radius': 'var(--radius-lg)',
|
|
|
'content-bg': 'transparent',
|
|
|
'content-border-width': 'var(--border-width)',
|
|
|
'content-border': 'transparent',
|
|
|
'content-fg': 'var(--color-content-primary)',
|
|
|
'surface-content-border': 'color-mix( in srgb, var(--tabs-provider-border) 84%, transparent )',
|
|
|
'surface-content-bg': 'color-mix( in srgb, var(--color-surface-overlay) 92%, transparent )',
|
|
|
'pills-content-padding-block-start': 'var(--space-2)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'indicator-transition-duration': 'var(--duration-normal)',
|
|
|
'indicator-transition-ease': 'cubic-bezier(0.32, 0.72, 0, 1)',
|
|
|
'indicator-line-thickness': '2px',
|
|
|
'scrollable-fade': '16px'
|
|
|
},
|
|
|
|
|
|
's-text': {
|
|
|
'hint-fg': 'var(--color-content-secondary)',
|
|
|
'hint-opacity': '0.8',
|
|
|
'hint-margin-top': 'var(--space-1)',
|
|
|
// Content-ink tint (Fase 4, mirrors <Text>) — `color` beyond the ink axis
|
|
|
// routes through the shared palette forward. Colored-only: the ink axis
|
|
|
// (primary…on-solid) never stamps `data-color`, so this host is the
|
|
|
// forward fallback only.
|
|
|
'_palette-text': { value: 'var(--color-primary-text)', scope: 'host' }
|
|
|
},
|
|
|
's-text-virtual-list': {
|
|
|
'line-height': 'var(--leading-prose)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
bg: 'var(--color-surface-default)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
'border-fg': 'var(--color-border-subtle)',
|
|
|
radius: 'var(--radius-md)',
|
|
|
'row-padding-inline': 'var(--space-3)',
|
|
|
'row-border-width': 'var(--border-width)',
|
|
|
'row-border-fg': 'var(--color-border-subtle)'
|
|
|
},
|
|
|
toast: {
|
|
|
'toaster-z': 'var(--z-index-overlay-toast)',
|
|
|
'toaster-inset': 'var(--space-4)',
|
|
|
'toaster-gap': 'var(--space-3)',
|
|
|
'toaster-width': '420px',
|
|
|
'provider-min-width': '280px',
|
|
|
'provider-max-width': '420px',
|
|
|
'provider-gap': 'var(--space-3)',
|
|
|
'provider-stack-gap': 'var(--space-1)',
|
|
|
'provider-padding-inline': 'var(--space-4)',
|
|
|
'provider-padding-block': 'var(--space-3)',
|
|
|
'provider-border-width': 'var(--border-width)',
|
|
|
'provider-radius': 'var(--radius-lg)',
|
|
|
'provider-shadow': 'var(--depth-overlay-shadow), var(--depth-overlay-halo)',
|
|
|
'accent-width': 'var(--border-width-thick)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
'closed-offset': '10px',
|
|
|
'closed-scale': '0.98',
|
|
|
'status-size': '32px',
|
|
|
'status-radius': 'var(--radius-full)',
|
|
|
'title-font-family': 'var(--style-label-font-family)',
|
|
|
'title-font-size': 'var(--size-sm-font-size)',
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'title-line-height': 'var(--leading-ui)',
|
|
|
'title-fg': 'var(--color-content-primary)',
|
|
|
'message-fg': 'var(--color-content-primary)',
|
|
|
'description-font-family': 'var(--style-label-font-family)',
|
|
|
'description-font-size': 'var(--size-xs-font-size)',
|
|
|
'description-line-height': 'var(--leading-prose)',
|
|
|
'description-fg': 'var(--color-content-secondary)',
|
|
|
'action-gap': 'var(--space-1)',
|
|
|
'action-padding-inline': 'var(--space-2)',
|
|
|
'action-padding-block': 'var(--space-1)',
|
|
|
'action-margin-top': 'var(--space-1)',
|
|
|
'action-border-width': 'var(--border-width)',
|
|
|
'action-radius': 'var(--radius-md)',
|
|
|
'action-font-family': 'var(--style-label-font-family)',
|
|
|
'action-font-size': 'var(--size-xs-font-size)',
|
|
|
'action-font-weight': 'var(--font-weight-medium)',
|
|
|
'action-line-height': 'var(--leading-ui)',
|
|
|
'action-focus-ring-offset': 'var(--focus-ring-offset)',
|
|
|
'action-focus-ring-width': 'var(--focus-ring-width)',
|
|
|
'action-focus-ring-color': 'var(--focus-ring-color)',
|
|
|
// `close-*` tokens removed: Toast.Close composes the canonical
|
|
|
// <IconButton rounded="full">, so its chrome lives in button.css.
|
|
|
'default-surface':
|
|
|
'color-mix( in srgb, var(--color-surface-overlay) 92%, var(--color-neutral-track) )',
|
|
|
'default-border':
|
|
|
'color-mix( in srgb, var(--color-neutral-border) 72%, var(--color-border-default) )',
|
|
|
'default-accent': 'var(--color-neutral-border)',
|
|
|
'default-status-bg': 'var(--color-neutral-track)',
|
|
|
'default-status-fg': 'var(--color-neutral-text)',
|
|
|
'default-action-fg': 'var(--color-neutral-text)',
|
|
|
'default-action-hover-bg': 'var(--color-neutral-track)',
|
|
|
'affirm-surface':
|
|
|
'color-mix( in srgb, var(--color-surface-overlay) 90%, var(--color-affirm-track) )',
|
|
|
'affirm-border':
|
|
|
'color-mix( in srgb, var(--color-affirm-border) 70%, var(--color-border-default) )',
|
|
|
'affirm-accent': 'var(--color-affirm-border)',
|
|
|
'affirm-status-bg': 'var(--color-affirm-track)',
|
|
|
'affirm-status-fg': 'var(--color-affirm-text)',
|
|
|
'affirm-action-fg': 'var(--color-affirm-text)',
|
|
|
'affirm-action-hover-bg': 'var(--color-affirm-track)',
|
|
|
'fulfill-surface':
|
|
|
'color-mix( in srgb, var(--color-surface-overlay) 90%, var(--color-fulfill-track) )',
|
|
|
'fulfill-border':
|
|
|
'color-mix( in srgb, var(--color-fulfill-border) 70%, var(--color-border-default) )',
|
|
|
'fulfill-accent': 'var(--color-fulfill-border)',
|
|
|
'fulfill-status-bg': 'var(--color-fulfill-track)',
|
|
|
'fulfill-status-fg': 'var(--color-fulfill-text)',
|
|
|
'fulfill-action-fg': 'var(--color-fulfill-text)',
|
|
|
'fulfill-action-hover-bg': 'var(--color-fulfill-track)',
|
|
|
'risk-surface':
|
|
|
'color-mix( in srgb, var(--color-surface-overlay) 90%, var(--color-risk-track) )',
|
|
|
'risk-border':
|
|
|
'color-mix( in srgb, var(--color-risk-border) 72%, var(--color-border-default) )',
|
|
|
'risk-accent': 'var(--color-risk-border)',
|
|
|
'risk-status-bg': 'var(--color-risk-track)',
|
|
|
'risk-status-fg': 'var(--color-risk-text)',
|
|
|
'risk-action-fg': 'var(--color-risk-text)',
|
|
|
'risk-action-hover-bg': 'var(--color-risk-track)',
|
|
|
'threat-surface':
|
|
|
'color-mix( in srgb, var(--color-surface-overlay) 90%, var(--color-threat-track) )',
|
|
|
'threat-border':
|
|
|
'color-mix( in srgb, var(--color-threat-border) 72%, var(--color-border-default) )',
|
|
|
'threat-accent': 'var(--color-threat-border)',
|
|
|
'threat-status-bg': 'var(--color-threat-track)',
|
|
|
'threat-status-fg': 'var(--color-threat-text)',
|
|
|
'threat-action-fg': 'var(--color-threat-text)',
|
|
|
'threat-action-hover-bg': 'var(--color-threat-track)',
|
|
|
'loss-surface':
|
|
|
'color-mix( in srgb, var(--color-surface-overlay) 90%, var(--color-loss-track) )',
|
|
|
'loss-border':
|
|
|
'color-mix( in srgb, var(--color-loss-border) 72%, var(--color-border-default) )',
|
|
|
'loss-accent': 'var(--color-loss-border)',
|
|
|
'loss-status-bg': 'var(--color-loss-track)',
|
|
|
'loss-status-fg': 'var(--color-loss-text)',
|
|
|
'loss-action-fg': 'var(--color-loss-text)',
|
|
|
'loss-action-hover-bg': 'var(--color-loss-track)'
|
|
|
},
|
|
|
toggle: {
|
|
|
// SIZE-BUNDLE PILOT (Fase D adoption pattern): where the recipe sits ON
|
|
|
// the canonical coordinate, it consumes `--size-{k}-*` (retuning the
|
|
|
// bundle at theme level now moves this recipe); where it DELIBERATELY
|
|
|
// deviates (px / gap tuned tighter than the bundle's padding, leading
|
|
|
// anchored to `--leading-ui`), it keeps its own value — the deviation is
|
|
|
// now visible instead of buried in a parallel re-declaration.
|
|
|
'height-xs': 'var(--size-xs-control-height)',
|
|
|
'height-sm': 'var(--size-sm-control-height)',
|
|
|
'height-md': 'var(--size-md-control-height)',
|
|
|
'height-lg': 'var(--size-lg-control-height)',
|
|
|
'height-xl': 'var(--size-xl-control-height)',
|
|
|
'padding-inline-xs': 'var(--space-2)',
|
|
|
'padding-inline-sm': 'var(--space-2-5)',
|
|
|
'padding-inline-md': 'var(--space-3-5)',
|
|
|
'padding-inline-lg': 'var(--space-4-5)',
|
|
|
'padding-inline-xl': 'var(--space-5)',
|
|
|
'gap-xs': 'var(--space-1)',
|
|
|
'gap-sm': 'var(--space-1-5)',
|
|
|
'gap-md': 'var(--space-1-5)',
|
|
|
'gap-lg': 'var(--space-2)',
|
|
|
'gap-xl': 'var(--space-2-5)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
'letter-spacing': 'var(--tracking-normal)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'font-weight-xs': 'var(--font-weight-medium)',
|
|
|
'font-weight-sm': 'var(--font-weight-medium)',
|
|
|
'font-weight-md': 'var(--font-weight-medium)',
|
|
|
'font-weight-lg': 'var(--font-weight-medium)',
|
|
|
'font-weight-xl': 'var(--font-weight-medium)',
|
|
|
// Radius is NOT a per-size token: it is DECOUPLED from `size` (Radix model).
|
|
|
// toggle.css resolves the resting radius from `--radius-default` (control
|
|
|
// archetype default) and the `rounded` override from the global `--radius-*`
|
|
|
// scale. No `--toggle-radius-*` aliases — see toggle.css "Rounded override".
|
|
|
'border-width': 'var(--border-width)',
|
|
|
shadow: 'none',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)',
|
|
|
// Per-color forwarders consumed by the palette-* cascades below.
|
|
|
// 8 slots per color (track / element / hover / border / solid /
|
|
|
// solid-hover / text / contrast) matching the 8 palette-* tokens
|
|
|
// the cascades target. Missing any forwarder makes the
|
|
|
// corresponding palette-{slot} resolve to empty for that color,
|
|
|
// which downstream collapses any derived token consuming it
|
|
|
// (e.g. ghost-on-bg uses palette-track).
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-element': 'var(--color-primary-element)',
|
|
|
'primary-hover': 'var(--color-primary-hover)',
|
|
|
'primary-border': 'var(--color-primary-border)',
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-solid-hover': 'var(--color-primary-solid-hover)',
|
|
|
'primary-text': 'var(--color-primary-text)',
|
|
|
'primary-contrast': 'var(--color-primary-contrast)',
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-element': 'var(--color-neutral-element)',
|
|
|
'neutral-hover': 'var(--color-neutral-hover)',
|
|
|
'neutral-border': 'var(--color-neutral-border)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-solid-hover': 'var(--color-neutral-solid-hover)',
|
|
|
'neutral-text': 'var(--color-neutral-text)',
|
|
|
'neutral-contrast': 'var(--color-neutral-contrast)',
|
|
|
'risk-track': 'var(--color-risk-track)',
|
|
|
'risk-element': 'var(--color-risk-element)',
|
|
|
'risk-hover': 'var(--color-risk-hover)',
|
|
|
'risk-border': 'var(--color-risk-border)',
|
|
|
'risk-solid': 'var(--color-risk-solid)',
|
|
|
'risk-solid-hover': 'var(--color-risk-solid-hover)',
|
|
|
'risk-text': 'var(--color-risk-text)',
|
|
|
'risk-contrast': 'var(--color-risk-contrast)',
|
|
|
'threat-track': 'var(--color-threat-track)',
|
|
|
'threat-element': 'var(--color-threat-element)',
|
|
|
'threat-hover': 'var(--color-threat-hover)',
|
|
|
'threat-border': 'var(--color-threat-border)',
|
|
|
'threat-solid': 'var(--color-threat-solid)',
|
|
|
'threat-solid-hover': 'var(--color-threat-solid-hover)',
|
|
|
'threat-text': 'var(--color-threat-text)',
|
|
|
'threat-contrast': 'var(--color-threat-contrast)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-element': 'var(--color-secondary-element)',
|
|
|
'secondary-hover': 'var(--color-secondary-hover)',
|
|
|
'secondary-border': 'var(--color-secondary-border)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-solid-hover': 'var(--color-secondary-solid-hover)',
|
|
|
'secondary-text': 'var(--color-secondary-text)',
|
|
|
'secondary-contrast': 'var(--color-secondary-contrast)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-element': 'var(--color-affirm-element)',
|
|
|
'affirm-hover': 'var(--color-affirm-hover)',
|
|
|
'affirm-border': 'var(--color-affirm-border)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-solid-hover': 'var(--color-affirm-solid-hover)',
|
|
|
'affirm-text': 'var(--color-affirm-text)',
|
|
|
'affirm-contrast': 'var(--color-affirm-contrast)',
|
|
|
// Palette tokens: 8 custom properties, each declared 7 times.
|
|
|
// One default declaration at scope 'host' (the neutral color), plus
|
|
|
// one override per supported color value (scope 'color:X'). The
|
|
|
// generator emits them as a `[data-toggle]` block (default) plus
|
|
|
// six `[data-toggle][data-color='X']` blocks (overrides). The CSS
|
|
|
// cascade picks the most specific match per element.
|
|
|
//
|
|
|
// This replaces the per-color blocks that used to live in
|
|
|
// `toggle.css` — the config now owns the entire palette cascade.
|
|
|
'palette-track': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--toggle-neutral-track)', scope: 'host' },
|
|
|
{ value: 'var(--toggle-primary-track)', scope: 'color:primary' },
|
|
|
{ value: 'var(--toggle-secondary-track)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--toggle-affirm-track)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--toggle-risk-track)', scope: 'color:risk' },
|
|
|
{ value: 'var(--toggle-threat-track)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
'palette-element': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--toggle-neutral-element)', scope: 'host' },
|
|
|
{ value: 'var(--toggle-primary-element)', scope: 'color:primary' },
|
|
|
{ value: 'var(--toggle-secondary-element)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--toggle-affirm-element)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--toggle-risk-element)', scope: 'color:risk' },
|
|
|
{ value: 'var(--toggle-threat-element)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
'palette-hover': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--toggle-neutral-hover)', scope: 'host' },
|
|
|
{ value: 'var(--toggle-primary-hover)', scope: 'color:primary' },
|
|
|
{ value: 'var(--toggle-secondary-hover)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--toggle-affirm-hover)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--toggle-risk-hover)', scope: 'color:risk' },
|
|
|
{ value: 'var(--toggle-threat-hover)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
'palette-border': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--toggle-neutral-border)', scope: 'host' },
|
|
|
{ value: 'var(--toggle-primary-border)', scope: 'color:primary' },
|
|
|
{ value: 'var(--toggle-secondary-border)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--toggle-affirm-border)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--toggle-risk-border)', scope: 'color:risk' },
|
|
|
{ value: 'var(--toggle-threat-border)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
'palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--toggle-neutral-solid)', scope: 'host' },
|
|
|
{ value: 'var(--toggle-primary-solid)', scope: 'color:primary' },
|
|
|
{ value: 'var(--toggle-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--toggle-affirm-solid)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--toggle-risk-solid)', scope: 'color:risk' },
|
|
|
{ value: 'var(--toggle-threat-solid)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
'palette-solid-hover': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--toggle-neutral-solid-hover)', scope: 'host' },
|
|
|
{ value: 'var(--toggle-primary-solid-hover)', scope: 'color:primary' },
|
|
|
{ value: 'var(--toggle-secondary-solid-hover)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--toggle-affirm-solid-hover)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--toggle-risk-solid-hover)', scope: 'color:risk' },
|
|
|
{ value: 'var(--toggle-threat-solid-hover)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
'palette-text': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--toggle-neutral-text)', scope: 'host' },
|
|
|
{ value: 'var(--toggle-primary-text)', scope: 'color:primary' },
|
|
|
{ value: 'var(--toggle-secondary-text)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--toggle-affirm-text)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--toggle-risk-text)', scope: 'color:risk' },
|
|
|
{ value: 'var(--toggle-threat-text)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
'palette-contrast': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--toggle-neutral-contrast)', scope: 'host' },
|
|
|
{ value: 'var(--toggle-primary-contrast)', scope: 'color:primary' },
|
|
|
{ value: 'var(--toggle-secondary-contrast)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--toggle-affirm-contrast)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--toggle-risk-contrast)', scope: 'color:risk' },
|
|
|
{ value: 'var(--toggle-threat-contrast)', scope: 'color:threat' }
|
|
|
]
|
|
|
},
|
|
|
// Variant tokens that depend on the runtime `--toggle-palette-*`
|
|
|
// values are scoped to `host` so the var() reference resolves
|
|
|
// against the toggle element (where the palette override lives),
|
|
|
// not against `:root` (which would freeze them at neutral).
|
|
|
// The generator infers the `depends` set automatically from the
|
|
|
// `var(--toggle-palette-*)` references — no manual `depends`
|
|
|
// arrays needed. See Token Scope Contract in `lib/config-types.ts`.
|
|
|
'solid-bg': 'var(--color-surface-raised)',
|
|
|
'solid-fg': 'var(--color-content-primary)',
|
|
|
'solid-border': 'var(--color-border-default)',
|
|
|
'solid-hover-bg': {
|
|
|
value: 'color-mix( in srgb, var(--color-surface-overlay) 68%, var(--toggle-palette-track) )',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'solid-hover-border': {
|
|
|
value: 'color-mix( in srgb, var(--toggle-palette-border) 45%, var(--color-border-strong) )',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'solid-on-bg': { value: 'var(--toggle-palette-solid)', scope: 'host' },
|
|
|
'solid-on-fg': { value: 'var(--toggle-palette-contrast)', scope: 'host' },
|
|
|
'solid-on-border': { value: 'var(--toggle-palette-solid)', scope: 'host' },
|
|
|
'solid-on-hover-bg': { value: 'var(--toggle-palette-solid-hover)', scope: 'host' },
|
|
|
'solid-on-hover-border': { value: 'var(--toggle-palette-solid-hover)', scope: 'host' },
|
|
|
'outline-bg': 'transparent',
|
|
|
'outline-fg': { value: 'var(--toggle-palette-text)', scope: 'host' },
|
|
|
'outline-border': {
|
|
|
value: 'color-mix( in srgb, var(--toggle-palette-border) 88%, var(--color-border-default) )',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'outline-hover-bg': { value: 'var(--toggle-palette-track)', scope: 'host' },
|
|
|
'outline-hover-border': {
|
|
|
value: 'color-mix( in srgb, var(--toggle-palette-border) 100%, var(--color-border-default) )',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'outline-on-bg': {
|
|
|
value:
|
|
|
'color-mix( in srgb, var(--toggle-palette-element) 100%, var(--color-surface-raised) )',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'outline-on-fg': { value: 'var(--toggle-palette-text)', scope: 'host' },
|
|
|
'outline-on-border': { value: 'var(--toggle-palette-border)', scope: 'host' },
|
|
|
'outline-on-hover-bg': { value: 'var(--toggle-palette-hover)', scope: 'host' },
|
|
|
'outline-on-hover-border': { value: 'var(--toggle-palette-border)', scope: 'host' },
|
|
|
'ghost-bg': 'transparent',
|
|
|
'ghost-fg': { value: 'var(--toggle-palette-text)', scope: 'host' },
|
|
|
'ghost-border': 'transparent',
|
|
|
'ghost-hover-bg': {
|
|
|
value: 'color-mix(in srgb, var(--toggle-palette-track) 72%, transparent)',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'ghost-hover-border': 'transparent',
|
|
|
'ghost-on-bg': { value: 'var(--toggle-palette-track)', scope: 'host' },
|
|
|
'ghost-on-fg': { value: 'var(--toggle-palette-text)', scope: 'host' },
|
|
|
'ghost-on-border': {
|
|
|
value: 'color-mix( in srgb, var(--toggle-palette-border) 72%, transparent )',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'ghost-on-hover-bg': { value: 'var(--toggle-palette-element)', scope: 'host' },
|
|
|
'ghost-on-hover-border': {
|
|
|
value: 'color-mix( in srgb, var(--toggle-palette-border) 88%, transparent )',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'disabled-opacity': 'var(--opacity-disabled)'
|
|
|
},
|
|
|
tooltip: {
|
|
|
'content-z': 'var(--z-index-overlay-tooltip)',
|
|
|
'max-width': '22rem',
|
|
|
'padding-block-sm': 'var(--space-1)',
|
|
|
'padding-inline-sm': 'var(--space-2)',
|
|
|
'padding-block-md': 'var(--space-1-5)',
|
|
|
'padding-inline-md': 'var(--space-2-5)',
|
|
|
'padding-block-lg': 'var(--space-2)',
|
|
|
'padding-inline-lg': 'var(--space-3)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'font-weight': 'var(--font-weight-regular)',
|
|
|
'line-height': 'var(--leading-text)',
|
|
|
radius: 'var(--radius-md)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
bg: 'var(--color-surface-overlay)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
border: 'color-mix( in srgb, var(--color-border-default) 84%, var(--tooltip-bg) )',
|
|
|
shadow: 'var(--depth-overlay-shadow), var(--depth-overlay-halo)',
|
|
|
// La variante `ghost`: un velo translúcido sobre lo que haya detrás. Su
|
|
|
// superficie y su desenfoque son suyos — la sólida usa `bg`.
|
|
|
// NO `ghost-bg`: la superficie del panel la pinta el PLANO `overlay`
|
|
|
// (misma especificidad, más tarde en la cascada), así que un token de fondo
|
|
|
// aquí mentiría — medido 2026-08-23, igual que `bg`/`border`/`shadow`.
|
|
|
'ghost-blur': 'var(--blur-md)',
|
|
|
// La flecha de la variante `outline`: se rellena con la superficie y se
|
|
|
// perfila, porque el panel es transparente y el caret no puede serlo.
|
|
|
'outline-arrow-bg': 'var(--color-surface-default)',
|
|
|
'outline-arrow-border': 'var(--color-border-strong)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Layout primitives. Box, Flex, Grid, Float and Group expose their CSS
|
|
|
// variables as a pass-through channel: callers push per-instance values
|
|
|
// inline via `pushStyleVar`, and the recipe simply mirrors the CSS
|
|
|
// fallbacks so the contract sees each var as declared. `revert-layer`
|
|
|
// means "fall back to the underlying tag's natural rule" — the right
|
|
|
// default for unset Box props.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
box: {
|
|
|
display: 'revert-layer',
|
|
|
position: 'revert-layer',
|
|
|
width: 'revert-layer',
|
|
|
'min-width': '0',
|
|
|
'max-width': 'revert-layer',
|
|
|
height: 'revert-layer',
|
|
|
'min-height': 'revert-layer',
|
|
|
'max-height': 'revert-layer',
|
|
|
padding: 'revert-layer',
|
|
|
'padding-block': 'revert-layer',
|
|
|
'padding-inline': 'revert-layer',
|
|
|
'padding-top': 'revert-layer',
|
|
|
'padding-right': 'revert-layer',
|
|
|
'padding-bottom': 'revert-layer',
|
|
|
'padding-left': 'revert-layer',
|
|
|
margin: 'revert-layer',
|
|
|
'margin-block': 'revert-layer',
|
|
|
'margin-inline': 'revert-layer',
|
|
|
'margin-top': 'revert-layer',
|
|
|
'margin-right': 'revert-layer',
|
|
|
'margin-bottom': 'revert-layer',
|
|
|
'margin-left': 'revert-layer',
|
|
|
gap: 'revert-layer',
|
|
|
top: 'revert-layer',
|
|
|
right: 'revert-layer',
|
|
|
bottom: 'revert-layer',
|
|
|
left: 'revert-layer',
|
|
|
inset: 'revert-layer',
|
|
|
'inset-block': 'revert-layer',
|
|
|
'inset-inline': 'revert-layer',
|
|
|
overflow: 'revert-layer',
|
|
|
'overflow-x': 'revert-layer',
|
|
|
'overflow-y': 'revert-layer',
|
|
|
// No `flex` token: the shorthand is expanded by the wrapper into the three
|
|
|
// longhands below (a shorthand in the recipe is erased by them — see
|
|
|
// `box.css`), so `--box-flex` would be a name nobody reads.
|
|
|
grow: 'revert-layer',
|
|
|
shrink: 'revert-layer',
|
|
|
basis: 'revert-layer',
|
|
|
order: 'revert-layer',
|
|
|
'align-self': 'revert-layer',
|
|
|
'justify-self': 'revert-layer',
|
|
|
'place-self': 'revert-layer',
|
|
|
'grid-column': 'revert-layer',
|
|
|
'grid-row': 'revert-layer',
|
|
|
'grid-area': 'revert-layer'
|
|
|
},
|
|
|
flex: {
|
|
|
direction: 'row',
|
|
|
align: 'stretch',
|
|
|
justify: 'flex-start',
|
|
|
wrap: 'nowrap',
|
|
|
'align-content': 'normal',
|
|
|
'row-gap': 'var(--box-gap, revert-layer)',
|
|
|
'column-gap': 'var(--box-gap, revert-layer)'
|
|
|
},
|
|
|
grid: {
|
|
|
'template-columns': 'none',
|
|
|
'template-rows': 'none',
|
|
|
'auto-columns': 'auto',
|
|
|
'auto-rows': 'auto',
|
|
|
'auto-flow': 'row',
|
|
|
align: 'stretch',
|
|
|
justify: 'start',
|
|
|
'align-content': 'normal',
|
|
|
'place-items': 'revert-layer',
|
|
|
'place-content': 'revert-layer',
|
|
|
gap: 'revert-layer',
|
|
|
'row-gap': 'var(--grid-gap, revert-layer)',
|
|
|
'column-gap': 'var(--grid-gap, revert-layer)'
|
|
|
},
|
|
|
float: {
|
|
|
gap: 'var(--space-3)'
|
|
|
},
|
|
|
group: {
|
|
|
'attached-overlap': '1px'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Banner — announcement strip. Public surface area is intentionally
|
|
|
// small (gap + radius); the rest of the chrome resolves from
|
|
|
// data-intent + data-variant.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
banner: {
|
|
|
gap: 'var(--space-3)',
|
|
|
radius: '0',
|
|
|
// Densidad por talla: la franja es un anuncio a todo el ancho, y su respiro
|
|
|
// es lo primero que un tema toca.
|
|
|
'padding-block-sm': 'var(--space-2)',
|
|
|
'padding-block-md': 'var(--space-3)',
|
|
|
'padding-block-lg': 'var(--space-4)',
|
|
|
'padding-inline-sm': 'var(--space-3)',
|
|
|
'padding-inline-md': 'var(--space-4)',
|
|
|
'padding-inline-lg': 'var(--space-5)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'line-height': 'var(--size-sm-font-line-height)',
|
|
|
// El trazo del anillo de la variante `outline` — la única que dibuja borde.
|
|
|
'border-width': 'var(--border-width)',
|
|
|
// Per-intent palette forwarders → Banner is retintable via
|
|
|
// `recipes.banner.{intent}-{slot}` like every other color component
|
|
|
// (button/badge/toggle). banner.css reads these instead of the raw
|
|
|
// foundation `--color-{intent}-*` tokens, so a per-component recipe
|
|
|
// override actually reaches it. (audit P1-5)
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'primary-contrast': 'var(--color-primary-contrast)',
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-border': 'var(--color-primary-border)',
|
|
|
'primary-text': 'var(--color-primary-text)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'secondary-contrast': 'var(--color-secondary-contrast)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-border': 'var(--color-secondary-border)',
|
|
|
'secondary-text': 'var(--color-secondary-text)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'neutral-contrast': 'var(--color-neutral-contrast)',
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-border': 'var(--color-neutral-border)',
|
|
|
'neutral-text': 'var(--color-neutral-text)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'affirm-contrast': 'var(--color-affirm-contrast)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-border': 'var(--color-affirm-border)',
|
|
|
'affirm-text': 'var(--color-affirm-text)',
|
|
|
'fulfill-solid': 'var(--color-fulfill-solid)',
|
|
|
'fulfill-contrast': 'var(--color-fulfill-contrast)',
|
|
|
'fulfill-track': 'var(--color-fulfill-track)',
|
|
|
'fulfill-border': 'var(--color-fulfill-border)',
|
|
|
'fulfill-text': 'var(--color-fulfill-text)',
|
|
|
'risk-solid': 'var(--color-risk-solid)',
|
|
|
'risk-contrast': 'var(--color-risk-contrast)',
|
|
|
'risk-track': 'var(--color-risk-track)',
|
|
|
'risk-border': 'var(--color-risk-border)',
|
|
|
'risk-text': 'var(--color-risk-text)',
|
|
|
'threat-solid': 'var(--color-threat-solid)',
|
|
|
'threat-contrast': 'var(--color-threat-contrast)',
|
|
|
'threat-track': 'var(--color-threat-track)',
|
|
|
'threat-border': 'var(--color-threat-border)',
|
|
|
'threat-text': 'var(--color-threat-text)',
|
|
|
'loss-solid': 'var(--color-loss-solid)',
|
|
|
'loss-contrast': 'var(--color-loss-contrast)',
|
|
|
'loss-track': 'var(--color-loss-track)',
|
|
|
'loss-border': 'var(--color-loss-border)',
|
|
|
'loss-text': 'var(--color-loss-text)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Sticky — position:sticky affix. Only a stacking z-index is public; the
|
|
|
// offset arrives as the soma-injected `--_sticky-offset` data property.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
sticky: {
|
|
|
'z-index': 'var(--z-index-sticky)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// SkipLink — the bypass affordance. Invisible until focused, then a
|
|
|
// pill against the start corner of the viewport. Every token is a
|
|
|
// role/scale reference: a skip link is the app's own chrome and has to
|
|
|
// follow its theme, not a hard-coded contrast pair.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'skip-link': {
|
|
|
'z-index': 'var(--z-index-skip-link)',
|
|
|
offset: 'var(--space-2)',
|
|
|
'padding-block': 'var(--space-2)',
|
|
|
'padding-inline': 'var(--space-4)',
|
|
|
radius: 'var(--radius-md)',
|
|
|
bg: 'var(--color-primary-solid)',
|
|
|
fg: 'var(--color-primary-contrast)',
|
|
|
shadow: 'var(--shadow-overlay)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Prose — long-form typography. The reading column + the reading
|
|
|
// line-height are public; the em-relative element scale is recipe-internal.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
prose: {
|
|
|
// ── La costura (2026-08-22) ──────────────────────────────────────────
|
|
|
// Prose estiliza HTML CRUDO (h1, code, table…) por `:where([data-prose] el)`,
|
|
|
// así que sus "partes" son elementos. Los valores siguen siendo del canon
|
|
|
// —bundle de talla, ranuras de contenido, escalas de peso y radio— y aquí
|
|
|
// están sus knobs, que es lo que PLAN §2-A pide.
|
|
|
//
|
|
|
// Su ESCALA editorial (2.25em, 1.5em, 0.9em…) NO entra: es `em`-relativa a
|
|
|
// propósito —un `font-size` raíz re-deriva encabezados y márgenes, y por
|
|
|
// eso no hay una escala duplicada por talla como en tailwind-typography— y
|
|
|
// va anotada `/* literal: */` en el CSS, la válvula de recipe-contract §3.
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-md-font-size)',
|
|
|
'font-size-lg': 'var(--size-lg-font-size)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
// La tinta apagada: h6, los marcadores de lista y el pie de figura.
|
|
|
'fg-muted': 'var(--color-content-muted)',
|
|
|
'heading-fg': 'var(--color-content-primary)',
|
|
|
'heading-font-weight': 'var(--font-weight-semibold)',
|
|
|
// h1 pesa más que sus hermanos, y es lo único que lo distingue aquí.
|
|
|
'h1-font-weight': 'var(--font-weight-bold)',
|
|
|
'link-fg': 'var(--color-primary-text)',
|
|
|
'link-font-weight': 'var(--font-weight-medium)',
|
|
|
'strong-font-weight': 'var(--font-weight-semibold)',
|
|
|
'dt-font-weight': 'var(--font-weight-semibold)',
|
|
|
'th-font-weight': 'var(--font-weight-semibold)',
|
|
|
'mark-bg': 'var(--color-risk-track)',
|
|
|
'mark-radius': 'var(--radius-sm)',
|
|
|
// El borde-acento del blockquote: 3px ES el paso `thick` de la escala.
|
|
|
'blockquote-border': 'var(--color-border-strong)',
|
|
|
'blockquote-border-width': 'var(--border-width-thick)',
|
|
|
'blockquote-fg': 'var(--color-content-secondary)',
|
|
|
'code-font-family': 'var(--font-family-mono)',
|
|
|
'code-bg': 'var(--color-surface-muted)',
|
|
|
'code-radius': 'var(--radius-sm)',
|
|
|
'pre-bg': 'var(--color-surface-muted)',
|
|
|
'pre-radius': 'var(--radius-md)',
|
|
|
'kbd-font-family': 'var(--font-family-mono)',
|
|
|
'kbd-bg': 'var(--color-surface-default)',
|
|
|
'kbd-border': 'var(--color-border-default)',
|
|
|
'kbd-border-width': 'var(--border-width)',
|
|
|
'kbd-radius': 'var(--radius-sm)',
|
|
|
'kbd-shadow': '0 1px 0 1px var(--color-border-subtle)',
|
|
|
'media-radius': 'var(--radius-md)',
|
|
|
'hr-border': 'var(--color-border-default)',
|
|
|
'hr-border-width': 'var(--border-width)',
|
|
|
// La tabla: la cabecera y el pie marcan más fuerte que la fila.
|
|
|
'table-header-border': 'var(--color-border-strong)',
|
|
|
'table-cell-border': 'var(--color-border-subtle)',
|
|
|
'table-border-width': 'var(--border-width)',
|
|
|
measure: '65ch',
|
|
|
'line-height': '1.75'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// AnchorNav — scrollspy TOC rail. The per-depth indent step + the rail
|
|
|
// width are public; the active/depth logic keys off morfo data-attrs.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'anchor-nav': {
|
|
|
indent: 'var(--space-3)',
|
|
|
'rail-width': '2px',
|
|
|
// El raíl: su trazo ya era público, su TINTA no — y la del enlace activo
|
|
|
// vivía en un `border-inline-start-color` que el censo ni veía.
|
|
|
'rail-fg': 'var(--color-border-subtle)',
|
|
|
'active-rail-fg': 'var(--color-primary-solid)',
|
|
|
// La escala del índice: lee el bundle, no el primitivo suelto.
|
|
|
'font-size': 'var(--size-sm-font-size)',
|
|
|
'line-height': 'var(--size-sm-font-line-height)',
|
|
|
'link-padding-block': 'var(--space-1)',
|
|
|
'link-radius': 'var(--radius-sm)',
|
|
|
'link-fg': 'var(--color-content-muted)',
|
|
|
'hover-link-fg': 'var(--color-content-primary)',
|
|
|
'active-link-fg': 'var(--color-primary-text)',
|
|
|
'active-link-font-weight': 'var(--font-weight-medium)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// NavTree — data-driven docs navigation tree. `indent` is the per-depth
|
|
|
// step (multiplied by `--_nav-tree-depth`). Active / expanded / trail
|
|
|
// states key off morfo data-attrs; the chevron rotates on `data-state`.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'nav-tree': {
|
|
|
indent: 'var(--space-4)',
|
|
|
// La FILA es link + trigger a la vez: las reglas los tratan juntos y por eso
|
|
|
// el knob es de la fila, no de cada parte (pseudo-parte, como las regiones
|
|
|
// de waveform). El disparador sí tiene lo suyo: el suelo de 24 px del blanco
|
|
|
// táctil (WCAG 2.5.8) y su padding.
|
|
|
'font-size': 'var(--size-sm-font-size)',
|
|
|
'line-height': 'var(--size-sm-font-line-height)',
|
|
|
'row-gap': 'var(--space-1)',
|
|
|
'row-padding-block': 'var(--space-1)',
|
|
|
'row-padding-inline-end': 'var(--space-2)',
|
|
|
'row-radius': 'var(--radius-sm)',
|
|
|
'row-fg': 'var(--color-content-muted)',
|
|
|
'hover-row-fg': 'var(--color-content-primary)',
|
|
|
'active-row-fg': 'var(--color-primary-text)',
|
|
|
'active-row-font-weight': 'var(--font-weight-medium)',
|
|
|
'disabled-row-fg': 'var(--color-content-disabled)',
|
|
|
// La estela: los ancestros de la página actual leen como texto primario.
|
|
|
'trail-row-fg': 'var(--color-content-primary)',
|
|
|
'trigger-min-inline-size': 'var(--space-6)',
|
|
|
'trigger-padding-inline': 'var(--space-1)',
|
|
|
'badge-padding-inline-start': 'var(--space-2)',
|
|
|
// El galón es una escuadra dibujada con dos bordes: su lado es un knob y el
|
|
|
// grosor del trazo otro (mismo molde que la esquina de text-focus).
|
|
|
'chevron-size': '0.42em',
|
|
|
'chevron-stroke': '1.5px'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Sidebar — the app's vertical chrome. Widths are TOKENS, never TS
|
|
|
// constants (the dossier's note on shadcn shipping them as consts): the
|
|
|
// three of them are the whole layout contract — expanded, icon rail and
|
|
|
// the mobile Drawer. The panel animates `transform` when it goes
|
|
|
// off-canvas and `width` only in icon mode (animating width every frame
|
|
|
// is layout work; icon mode is the one case where the inset must follow).
|
|
|
//
|
|
|
// The ROW is a control, so it takes the canonical size bundle
|
|
|
// (`--size-{k}-*`, theming §5) instead of the hand-picked `--space-8` +
|
|
|
// `--font-size-sm` it carried until 2026-08-19: a spacing token as a
|
|
|
// control height rides the density-SPACE scale while every other control
|
|
|
// rides density-CONTROL, and a `font-size:` written in the recipe CSS is
|
|
|
// invisible to the bundle guard. Nav controls run `xs..lg` and default to
|
|
|
// `md` (the canon's default; `sm` reproduces the old density). The label
|
|
|
// steps ONE below its row — the label↔control hierarchy of the field
|
|
|
// doctrine, the only deliberate step-down in the system.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
sidebar: {
|
|
|
width: '16rem',
|
|
|
// The icon rail is the ROW plus the panel's padding on both sides —
|
|
|
// derived, not a constant: at `md` the row is 36px and a fixed 3rem
|
|
|
// would clip it. `host` scope because it reads a host-scoped row height.
|
|
|
'width-icon': {
|
|
|
value: 'calc(var(--sidebar-row-height) + 2 * var(--sidebar-padding))',
|
|
|
scope: 'host'
|
|
|
},
|
|
|
'width-mobile': '18rem',
|
|
|
'rail-width': 'var(--space-4)',
|
|
|
gap: 'var(--space-1)',
|
|
|
padding: 'var(--space-2)',
|
|
|
'row-height-xs': 'var(--size-xs-control-height)',
|
|
|
'row-height-sm': 'var(--size-sm-control-height)',
|
|
|
'row-height-md': 'var(--size-md-control-height)',
|
|
|
'row-height-lg': 'var(--size-lg-control-height)',
|
|
|
'row-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'row-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'row-font-size-md': 'var(--size-md-font-size)',
|
|
|
'row-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'row-line-height-xs': 'var(--size-xs-font-line-height)',
|
|
|
'row-line-height-sm': 'var(--size-sm-font-line-height)',
|
|
|
'row-line-height-md': 'var(--size-md-font-line-height)',
|
|
|
'row-line-height-lg': 'var(--size-lg-font-line-height)',
|
|
|
'row-icon-size-xs': 'var(--size-xs-icon-size)',
|
|
|
'row-icon-size-sm': 'var(--size-sm-icon-size)',
|
|
|
'row-icon-size-md': 'var(--size-md-icon-size)',
|
|
|
'row-icon-size-lg': 'var(--size-lg-icon-size)',
|
|
|
'row-padding-inline-xs': 'var(--size-xs-padding-inline)',
|
|
|
'row-padding-inline-sm': 'var(--size-sm-padding-inline)',
|
|
|
'row-padding-inline-md': 'var(--size-md-padding-inline)',
|
|
|
'row-padding-inline-lg': 'var(--size-lg-padding-inline)',
|
|
|
'row-gap-xs': 'var(--size-xs-gap)',
|
|
|
'row-gap-sm': 'var(--size-sm-gap)',
|
|
|
'row-gap-md': 'var(--size-md-gap)',
|
|
|
'row-gap-lg': 'var(--size-lg-gap)',
|
|
|
'row-radius-xs': 'var(--size-xs-radius)',
|
|
|
'row-radius-sm': 'var(--size-sm-radius)',
|
|
|
'row-radius-md': 'var(--size-md-radius)',
|
|
|
'row-radius-lg': 'var(--size-lg-radius)',
|
|
|
// One typographic step below the row (xs keeps xxs).
|
|
|
'label-font-size-xs': 'var(--size-xxs-font-size)',
|
|
|
'label-font-size-sm': 'var(--size-xs-font-size)',
|
|
|
'label-font-size-md': 'var(--size-sm-font-size)',
|
|
|
'label-font-size-lg': 'var(--size-md-font-size)',
|
|
|
// The resolved coordinates for the size in force. `host` default = `md`
|
|
|
// (the component's default); each `size:` scope is the emitted cascade
|
|
|
// entry, so the recipe CSS consumes ONE name per axis and the guard sees
|
|
|
// the bundle reference in the token, not a literal buried in a rule.
|
|
|
'row-height': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--sidebar-row-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--sidebar-row-height-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--sidebar-row-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--sidebar-row-height-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'row-font-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--sidebar-row-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--sidebar-row-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--sidebar-row-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--sidebar-row-font-size-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'row-line-height': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--sidebar-row-line-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--sidebar-row-line-height-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--sidebar-row-line-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--sidebar-row-line-height-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'row-icon-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--sidebar-row-icon-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--sidebar-row-icon-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--sidebar-row-icon-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--sidebar-row-icon-size-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'row-padding-inline': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--sidebar-row-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--sidebar-row-padding-inline-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--sidebar-row-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--sidebar-row-padding-inline-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'row-gap': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--sidebar-row-gap-md)', scope: 'host' },
|
|
|
{ value: 'var(--sidebar-row-gap-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--sidebar-row-gap-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--sidebar-row-gap-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'row-radius': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--sidebar-row-radius-md)', scope: 'host' },
|
|
|
{ value: 'var(--sidebar-row-radius-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--sidebar-row-radius-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--sidebar-row-radius-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'label-font-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--sidebar-label-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--sidebar-label-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--sidebar-label-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--sidebar-label-font-size-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'sub-indent': 'var(--space-4)',
|
|
|
// Band of the icon-mode flyout. The floating layer mirrors the content's
|
|
|
// computed z-index onto its positioner wrapper (the dropdown-menu note).
|
|
|
'sub-z': 'var(--z-index-overlay-floating)',
|
|
|
bg: 'var(--color-surface-muted)',
|
|
|
'border-fg': 'var(--color-border-subtle)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Callout — inline admonition. Per-color forwarders (track/text/solid)
|
|
|
// + the canonical `_palette-*` slots so the THM-2 shared layer routes
|
|
|
// `color="teal"` and the 33 scales with zero extra CSS (badge pattern).
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
callout: {
|
|
|
gap: 'var(--space-3)',
|
|
|
padding: 'var(--space-4)',
|
|
|
'accent-width': '3px',
|
|
|
radius: 'var(--radius-md)',
|
|
|
'row-gap': 'var(--space-1)',
|
|
|
// Typography of the admonition: it reads like body copy, so the value is
|
|
|
// the size bundle's and the knob is the callout's.
|
|
|
'font-size': 'var(--size-md-font-size)',
|
|
|
'line-height': 'var(--size-md-font-line-height)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'icon-size': 'var(--size-md-icon-size)',
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'primary-track': 'var(--color-primary-track)',
|
|
|
'primary-text': 'var(--color-primary-text)',
|
|
|
'primary-solid': 'var(--color-primary-solid)',
|
|
|
'secondary-track': 'var(--color-secondary-track)',
|
|
|
'secondary-text': 'var(--color-secondary-text)',
|
|
|
'secondary-solid': 'var(--color-secondary-solid)',
|
|
|
'neutral-track': 'var(--color-neutral-track)',
|
|
|
'neutral-text': 'var(--color-neutral-text)',
|
|
|
'neutral-solid': 'var(--color-neutral-solid)',
|
|
|
'affirm-track': 'var(--color-affirm-track)',
|
|
|
'affirm-text': 'var(--color-affirm-text)',
|
|
|
'affirm-solid': 'var(--color-affirm-solid)',
|
|
|
'fulfill-track': 'var(--color-fulfill-track)',
|
|
|
'fulfill-text': 'var(--color-fulfill-text)',
|
|
|
'fulfill-solid': 'var(--color-fulfill-solid)',
|
|
|
'risk-track': 'var(--color-risk-track)',
|
|
|
'risk-text': 'var(--color-risk-text)',
|
|
|
'risk-solid': 'var(--color-risk-solid)',
|
|
|
'threat-track': 'var(--color-threat-track)',
|
|
|
'threat-text': 'var(--color-threat-text)',
|
|
|
'threat-solid': 'var(--color-threat-solid)',
|
|
|
'loss-track': 'var(--color-loss-track)',
|
|
|
'loss-text': 'var(--color-loss-text)',
|
|
|
'loss-solid': 'var(--color-loss-solid)',
|
|
|
'_palette-track': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--callout-primary-track)', scope: 'host' },
|
|
|
{ value: 'var(--callout-secondary-track)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--callout-neutral-track)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--callout-affirm-track)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--callout-fulfill-track)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--callout-risk-track)', scope: 'color:risk' },
|
|
|
{ value: 'var(--callout-threat-track)', scope: 'color:threat' },
|
|
|
{ value: 'var(--callout-loss-track)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-text': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--callout-primary-text)', scope: 'host' },
|
|
|
{ value: 'var(--callout-secondary-text)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--callout-neutral-text)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--callout-affirm-text)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--callout-fulfill-text)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--callout-risk-text)', scope: 'color:risk' },
|
|
|
{ value: 'var(--callout-threat-text)', scope: 'color:threat' },
|
|
|
{ value: 'var(--callout-loss-text)', scope: 'color:loss' }
|
|
|
]
|
|
|
},
|
|
|
'_palette-solid': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--callout-primary-solid)', scope: 'host' },
|
|
|
{ value: 'var(--callout-secondary-solid)', scope: 'color:secondary' },
|
|
|
{ value: 'var(--callout-neutral-solid)', scope: 'color:neutral' },
|
|
|
{ value: 'var(--callout-affirm-solid)', scope: 'color:affirm' },
|
|
|
{ value: 'var(--callout-fulfill-solid)', scope: 'color:fulfill' },
|
|
|
{ value: 'var(--callout-risk-solid)', scope: 'color:risk' },
|
|
|
{ value: 'var(--callout-threat-solid)', scope: 'color:threat' },
|
|
|
{ value: 'var(--callout-loss-solid)', scope: 'color:loss' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Result — terminal flow/page state. Status→role color forwarders for
|
|
|
// the glyph only (HTTP situations stay neutral in the recipe); spacing
|
|
|
// + reading measures. Shares EmptyState's layout language.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
result: {
|
|
|
// The BOX is what this recipe owns: the page inset and the rhythm between
|
|
|
// its blocks. Type and ink read the canon in the CSS and mint nothing —
|
|
|
// the size bundle, the content slots, the weight scale.
|
|
|
gap: 'var(--space-3)',
|
|
|
'padding-block': 'var(--space-12)',
|
|
|
'padding-inline': 'var(--space-5)',
|
|
|
'actions-gap': 'var(--space-2)',
|
|
|
'actions-margin-block-start': 'var(--space-1)',
|
|
|
'extra-margin-block-start': 'var(--space-4)',
|
|
|
// ── The seam for type + ink (2026-08-22, firmado) ────────────────────
|
|
|
// The values are the canon's — size bundle, content slots, weight and
|
|
|
// leading scales — but the KNOB is the component's: PLAN §2-A. Five parts
|
|
|
// with distinct typography, so it is per PART (the catalogue's dominant
|
|
|
// shape) and not per role as in `chart`, whose two roles repeat over 19
|
|
|
// nodes. No named style plays "page terminal state", so nothing is bound
|
|
|
// wholesale to one.
|
|
|
'title-font-size': 'var(--size-xl-font-size)',
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'title-line-height': 'var(--font-line-height-sm)',
|
|
|
'title-fg': 'var(--color-content-primary)',
|
|
|
'description-font-size': 'var(--size-md-font-size)',
|
|
|
'description-line-height': 'var(--font-line-height-md)',
|
|
|
'description-fg': 'var(--color-content-muted)',
|
|
|
'extra-font-size': 'var(--size-sm-font-size)',
|
|
|
'extra-fg': 'var(--color-content-secondary)',
|
|
|
// The status glyph: THREE times the xl step is its identity, and the
|
|
|
// multiplier stays in the value so the glyph keeps following the scale.
|
|
|
'media-font-size': 'calc(var(--size-xl-font-size) * 3)',
|
|
|
'media-fg': 'var(--color-content-muted)',
|
|
|
// The big HTTP code — neutral by design (AntD never paints 404 red).
|
|
|
'code-font-family': 'var(--font-family-mono)',
|
|
|
'code-font-weight': 'var(--font-weight-semibold)',
|
|
|
'code-letter-spacing': '0.04em',
|
|
|
'code-fg': 'var(--color-content-muted)',
|
|
|
'description-measure': '45ch',
|
|
|
'extra-measure': '60ch',
|
|
|
'success-fg': 'var(--color-fulfill-solid)',
|
|
|
'error-fg': 'var(--color-threat-solid)',
|
|
|
'warning-fg': 'var(--color-risk-solid)',
|
|
|
'info-fg': 'var(--color-primary-solid)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// EmptyState — passive "nothing here / no results" surface. Public
|
|
|
// surface is intentionally small: spacing + the media plate + the
|
|
|
// description reading measure; density rides data-size internally.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'empty-state': {
|
|
|
gap: 'var(--space-3)',
|
|
|
'actions-gap': 'var(--space-2)',
|
|
|
'media-bg': 'var(--color-neutral-track)',
|
|
|
'media-fg': 'var(--color-content-muted)',
|
|
|
'media-radius': 'var(--radius-lg)',
|
|
|
'description-measure': '45ch',
|
|
|
// Density by size — sm is the in-collection empty (table / select /
|
|
|
// command), md the panel default, lg the full-page hero.
|
|
|
'padding-block-sm': 'var(--space-4)',
|
|
|
'padding-block-md': 'var(--space-8)',
|
|
|
'padding-block-lg': 'var(--space-12)',
|
|
|
'padding-inline-sm': 'var(--space-3)',
|
|
|
'padding-inline-md': 'var(--space-4)',
|
|
|
'padding-inline-lg': 'var(--space-5)',
|
|
|
// The title steps ONE discrete size ABOVE the component size: hierarchy
|
|
|
// without coupling to a heading level (level is semantic, size is visual).
|
|
|
'title-font-size-sm': 'var(--size-md-font-size)',
|
|
|
'title-font-size-md': 'var(--size-lg-font-size)',
|
|
|
'title-font-size-lg': 'var(--size-xl-font-size)',
|
|
|
'description-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'description-font-size-md': 'var(--size-md-font-size)',
|
|
|
'description-font-size-lg': 'var(--size-md-font-size)',
|
|
|
// The glyph; its tinted plate is derived from it (2x) in the recipe.
|
|
|
'media-glyph-size-sm': 'var(--size-sm-icon-size)',
|
|
|
'media-glyph-size-md': 'var(--size-lg-icon-size)',
|
|
|
'media-glyph-size-lg': 'var(--size-xl-icon-size)',
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'title-line-height': 'var(--size-sm-font-line-height)',
|
|
|
'title-fg': 'var(--color-content-primary)',
|
|
|
'description-line-height': 'var(--size-md-font-line-height)',
|
|
|
'description-fg': 'var(--color-content-muted)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Container — full-bleed wrapper that caps `max-width` at the canonical
|
|
|
// page width by default: the xl container token (= breakpoint xl, 1280 —
|
|
|
// user decision 2026-07-06). Callers usually push `maxWidth` via Box.
|
|
|
// The recipe token carries its OWN name (`--container-max-width`): a
|
|
|
// recipe key must never re-emit a foundation token name (the previous
|
|
|
// `width-xl` key shadowed the `--container-width-xl` primitive at :root
|
|
|
// through a phantom `--layout-container-width-xl` reference).
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
container: {
|
|
|
'max-width': 'var(--container-width-xl)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Section — vertical rhythm scale. Sizes map to the spacing primitives.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
section: {
|
|
|
'padding-block-sm': 'var(--space-8)',
|
|
|
'padding-block-md': 'var(--space-12)',
|
|
|
'padding-block-lg': 'var(--space-16)',
|
|
|
'padding-block-xl': 'calc(var(--space-16) * 1.5)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Separator — purely geometric divider.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
separator: {
|
|
|
thickness: 'var(--border-width)',
|
|
|
// The dedicated divider slot (step 6 — same tone as border-default, but
|
|
|
// semantically a SEPARATOR, so a theme can retune dividers independently).
|
|
|
fg: 'var(--color-neutral-separator)',
|
|
|
'min-length': 'var(--space-4)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// ToggleGroup — gap is component-owned; the color cascade modifies
|
|
|
// Toggle's `palette-*` tokens via TSC v2.2 cross-recipe composition
|
|
|
// (see `composition` block below).
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'toggle-group': {
|
|
|
gap: 'var(--space-1)'
|
|
|
// Per-color palette: the group's `data-color` routes to every item's Toggle
|
|
|
// palette via the shared per-instance layer (THM-2). The composition DSL
|
|
|
// can't presence-scope (`[data-color]` with no value), so the routing is
|
|
|
// hand-authored in `toggle-group.css` — ONE rule covering all roles + the
|
|
|
// 33 scales, replacing the old 4-role composition enumeration.
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// PinInput — one-time code cells.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'pin-input': {
|
|
|
gap: 'var(--space-2)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'cell-size-sm': '2rem',
|
|
|
'cell-size-md': '2.5rem',
|
|
|
'cell-size-lg': '3rem',
|
|
|
'cell-size-xl': '3.5rem',
|
|
|
'cell-font-size-sm': 'var(--size-md-font-size)',
|
|
|
'cell-font-size-md': 'var(--size-lg-font-size)',
|
|
|
'cell-font-size-lg': 'var(--size-xl-font-size)',
|
|
|
'cell-font-size-xl': 'var(--size-xxl-font-size)',
|
|
|
'cell-font-weight': '500',
|
|
|
'cell-radius': 'var(--radius-md)',
|
|
|
'cell-border-width': 'var(--border-width)',
|
|
|
'cell-border-fg': 'var(--color-border-default)',
|
|
|
'cell-bg': 'var(--color-surface-default)',
|
|
|
'cell-filled-bg': 'var(--color-primary-element)',
|
|
|
'cell-filled-border': 'var(--color-primary-border)',
|
|
|
'cell-active-border': 'var(--color-primary-solid)',
|
|
|
'cell-invalid-border': 'var(--color-threat-solid)',
|
|
|
'cell-readonly-bg': 'var(--color-surface-muted)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// ScrollArea — overlay scrollbars (macOS-style).
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'scroll-area': {
|
|
|
// Auto-fit max height — the CONSUMER sets this (e.g. the command list caps
|
|
|
// to min(60vh, 24rem)); the recipe declares the `none` default so the token
|
|
|
// is part of the public contract instead of an undeclared consumed var.
|
|
|
'auto-max-block': 'none',
|
|
|
bg: 'transparent',
|
|
|
radius: 'var(--radius-md)',
|
|
|
'scrollbar-padding': 'var(--space-0-5)',
|
|
|
'scrollbar-size': 'var(--space-2-5)',
|
|
|
'track-bg': 'color-mix(in srgb, var(--color-neutral-track) 60%, transparent)',
|
|
|
'hover-track-bg': 'color-mix(in srgb, var(--color-neutral-track) 84%, transparent)',
|
|
|
'thumb-bg': 'var(--color-content-muted)',
|
|
|
'thumb-bg-hover': 'var(--color-content-secondary)',
|
|
|
'thumb-radius': 'var(--radius-full)',
|
|
|
'thumb-min-size': 'var(--space-3)',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Splitter — resizable panels with a draggable handle + grip pill.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
splitter: {
|
|
|
bg: 'var(--color-surface-default)',
|
|
|
radius: 'var(--radius-md)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
'border-fg': 'var(--color-border-default)',
|
|
|
'disabled-opacity': 'var(--opacity-disabled)',
|
|
|
'panel-bg': 'transparent',
|
|
|
'handle-size': 'var(--space-2)',
|
|
|
'handle-bg': 'var(--color-border-subtle)',
|
|
|
'active-handle-bg': 'var(--color-primary-solid)',
|
|
|
'grip-radius': 'var(--radius-full)',
|
|
|
'grip-bg': 'var(--color-content-muted)',
|
|
|
'grip-main': 'var(--space-4)',
|
|
|
'grip-cross': '2px',
|
|
|
'transition-duration': 'var(--duration-fast)',
|
|
|
'transition-ease': 'var(--ease-default)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// NavigationMenu — soma writes the active-trigger rect into these
|
|
|
// indicator vars; the recipe paints the underline.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Combobox — floating result list. Shares the overlay micro-band rung
|
|
|
// `--z-index-overlay-floating` with `select` / `dropdown-menu` (the soma
|
|
|
// floating layer mirrors this computed z onto the positioner wrapper). The
|
|
|
// band is deliberately NOT the global `--z-index-*` ladder — see
|
|
|
// STATIC_Z_INDEX_OVERLAY.
|
|
|
// Kbd. `font-family` is NOT here: it reads `--style-code-font-family`, the
|
|
|
// typographic layer, and a consumer does not re-coin its layer's vocabulary
|
|
|
// (same call as code-block). The em-relative padding and min-width ARE knobs:
|
|
|
// a key cap grows with its letter, on purpose.
|
|
|
// Virtual-list paints chrome only — soma owns the windowing math. Size moves the
|
|
|
// chrome TYPE and nothing else: item dimensions come from props, and the radius
|
|
|
// is decoupled from size on purpose (Radix model, constant across the five).
|
|
|
'virtual-list': {
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-sm-font-size)',
|
|
|
'font-size-lg': 'var(--size-md-font-size)',
|
|
|
'font-size-xl': 'var(--size-lg-font-size)',
|
|
|
'font-size': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--virtual-list-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--virtual-list-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--virtual-list-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--virtual-list-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--virtual-list-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
radius: 'var(--radius-default)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
// Shared by surface and outline; ghost's `transparent` is variant identity.
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-border-default)',
|
|
|
'surface-bg': 'var(--color-surface-default)'
|
|
|
},
|
|
|
// Virtual-grid paints chrome only — soma owns the windowing math. Size moves the
|
|
|
// chrome TYPE and nothing else: cell dimensions come from props, and the radius
|
|
|
// is decoupled from size on purpose (Radix model, constant across the five).
|
|
|
'virtual-grid': {
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-sm-font-size)',
|
|
|
'font-size-lg': 'var(--size-md-font-size)',
|
|
|
'font-size-xl': 'var(--size-lg-font-size)',
|
|
|
'font-size': {
|
|
|
parts: ['root'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--virtual-grid-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--virtual-grid-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--virtual-grid-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--virtual-grid-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--virtual-grid-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
radius: 'var(--radius-default)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
// Shared by surface and outline; ghost's `transparent` is variant identity.
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-border-default)',
|
|
|
'surface-bg': 'var(--color-surface-default)'
|
|
|
},
|
|
|
// Announce is an ARIA live-region SERVICE: its host is `display: contents`
|
|
|
// and the region is visually hidden by default. Everything here is the card
|
|
|
// chrome for the minority case where the consumer opts the region IN to
|
|
|
// visible UI (`visuallyHidden={false}`). `font-family` is absent on purpose:
|
|
|
// it reads the `--style-label-*` layer, which a consumer does not re-coin.
|
|
|
announce: {
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'padding-block': 'var(--space-2)',
|
|
|
'padding-inline': 'var(--space-3)',
|
|
|
radius: 'var(--radius-md)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-border-default)',
|
|
|
bg: 'var(--color-surface-default)',
|
|
|
'font-size': 'var(--size-sm-font-size)',
|
|
|
'line-height': 'var(--leading-ui)'
|
|
|
},
|
|
|
// ScrollFrames is LAYOUT, not chrome: a tall track whose sticky viewport pins
|
|
|
// the media full-bleed while the track scrolls. The only thing it paints is
|
|
|
// the loading caption, so that is the whole contract.
|
|
|
// `--_scroll-frames-viewport-block-size` stays private on purpose: it is the
|
|
|
// hatch a consumer overrides to fit a container (100%) instead of the window
|
|
|
// (100vh) — layout geometry the SITE sets, not a theme value.
|
|
|
'scroll-frames': {
|
|
|
'loader-inset-block-end': '1.5rem',
|
|
|
'loader-font-size': 'var(--size-xs-font-size)',
|
|
|
'loader-letter-spacing': 'var(--tracking-widest)',
|
|
|
'loader-fg': 'var(--color-neutral-text)'
|
|
|
},
|
|
|
'menu-dial': {
|
|
|
// The dial owns ONLY its own chrome: the fan gap, the entrance stagger step
|
|
|
// and the backdrop tint. Actions are composed Buttons, and the floating
|
|
|
// placement belongs to the `viewport-placement` layer — whose override
|
|
|
// slots (`--_viewport-placement-offset` / `-z`) the recipe writes into.
|
|
|
// Those are NOT re-coined here: the axis is the layer's, and coining
|
|
|
// `placement-offset` would be the parallel vocabulary rule 2 forbids.
|
|
|
gap: 'var(--space-3)',
|
|
|
// The interval BETWEEN siblings, not an animation duration: it is what
|
|
|
// gives the fan its character.
|
|
|
'stagger-step': '28ms',
|
|
|
'backdrop-bg': 'color-mix(in oklab, var(--color-neutral-text-strong) 40%, transparent)'
|
|
|
},
|
|
|
// Knob. FOUR `--knob-*` names are deliberately absent: `progress`, `angle`,
|
|
|
// `start-angle` and `sweep` are written INLINE by the provider from control
|
|
|
// state (`knob-provider.svelte.ts:358`) — a value channel, not a theme
|
|
|
// surface, and no contract can outrank an inline style.
|
|
|
//
|
|
|
// The size axis is sm|md|lg|xl (no xs). It moves here rather than staying in
|
|
|
// a private on purpose: the recipe used to read `var(--knob-gap,
|
|
|
// var(--_knob-gap))`, where the public goes FIRST — so declaring the public
|
|
|
// in the contract would have killed the per-size scale outright.
|
|
|
// SkinMediaPlayer is a FIXED-TONE component: its hues are the depiction of
|
|
|
// physical objects (mahogany, brushed aluminium, chrome, ivory), not theme
|
|
|
// roles, and they live as literals in the SVG paint under the exception
|
|
|
// `recipe-css-contract.test.ts` already carves. What CAN be themed is its
|
|
|
// size — the recipe itself calls these "design values: retune here, in one
|
|
|
// place" — plus the unlit lamp and the key's hit area.
|
|
|
//
|
|
|
// The size axis is the PLAYER's: it stamps `data-size` on its own root and
|
|
|
// the skin reads it from there, so the resolved name targets the skin part
|
|
|
// while the scope comes from the ancestor.
|
|
|
'skin-media-player': {
|
|
|
// The base width is PER SKIN, not one number: each depicted object has its
|
|
|
// own natural width (a cassette is not a turntable). The root default is
|
|
|
// the deck's, which `controls.ts` supplies per device.
|
|
|
'base-width': '480px',
|
|
|
'record-base-width': '340px',
|
|
|
'cassette-base-width': '300px',
|
|
|
'deck-base-width': '480px',
|
|
|
'scale-sm': '0.7',
|
|
|
'scale-md': '1',
|
|
|
'scale-lg': '1.35',
|
|
|
'scale-xl': '1.7',
|
|
|
// An unlit bulb still shows its glass.
|
|
|
'lamp-opacity': '0.18',
|
|
|
'key-radius': 'var(--radius-sm)'
|
|
|
},
|
|
|
// TextCircular already consumed five publics through
|
|
|
// `var(--text-circular-x, <default>)` without declaring any of them, so a
|
|
|
// theme could not see them. Declaring is safe here (unlike `knob`): no
|
|
|
// per-size private hides behind the fallback, so the public cannot kill a
|
|
|
// scale. `color: currentColor` stays correct as a token — a custom property
|
|
|
// does not resolve `currentColor` until the point of use.
|
|
|
// TextFocus, same shape as text-circular: five publics were consumed through a
|
|
|
// fallback and none of them declared. The corner is a BRACKET drawn with
|
|
|
// borders — two of its four sides are zeroed per position — so its size is one
|
|
|
// knob, its stroke another. The `-0.625rem` offsets are NOT knobs: they place
|
|
|
// the bracket just outside the frame and follow from size + stroke.
|
|
|
'text-focus': {
|
|
|
gap: 'var(--space-4)',
|
|
|
'word-weight': '900',
|
|
|
'word-size': 'var(--font-size-4xl, 3rem)',
|
|
|
'corner-size': '1rem',
|
|
|
'corner-width': '3px',
|
|
|
// 3px verbatim, NOT `var(--radius-xs)`: that primitive DOES NOT EXIST.
|
|
|
// The recipe wrote `var(--radius-xs, 3px)` and the fallback was what
|
|
|
// painted — tokenizing the phantom name turned the declaration invalid and
|
|
|
// the corners square (48 diffs, measured 2026-08-22).
|
|
|
'corner-radius': '3px',
|
|
|
// The stroke and its halo: same accent, two layers, declared apart so a
|
|
|
// theme can tint the glow differently from the border.
|
|
|
'border-fg': 'var(--color-primary-solid)',
|
|
|
'glow-fg': 'var(--color-primary-solid)'
|
|
|
},
|
|
|
'text-circular': {
|
|
|
size: '12.5rem',
|
|
|
radius: 'var(--radius-full)',
|
|
|
weight: '900',
|
|
|
fg: 'currentColor',
|
|
|
'font-size': '1.5rem',
|
|
|
// The character's turn, kept verbatim: it is a gesture, not four axes.
|
|
|
'char-transition': 'all 0.5s cubic-bezier(0, 0, 0, 1)'
|
|
|
},
|
|
|
// TextGradient, third of the same shape: four publics consumed through a
|
|
|
// fallback and none declared, plus three literals. No size axis and no
|
|
|
// private behind the fallbacks, so declaring cannot kill a scale.
|
|
|
//
|
|
|
// `border-width` is the frame's stroke: the hole of the border is
|
|
|
// `calc(100% - 2px)`, which is TWO strokes of the linear-gradient showing
|
|
|
// around it, so the knob is the stroke and the hole derives from it.
|
|
|
// `blur` goes to the blur scale rather than a literal 4px because blur is a
|
|
|
// METRIC family under the scaling axis (theming §23) — a literal is blind to
|
|
|
// the global zoom the rest of the shell obeys.
|
|
|
//
|
|
|
// NOT declared: `duration` and `play-direction`. The wrapper writes both
|
|
|
// INLINE from props (`animationSpeed`, `yoyo`), and no contract outranks an
|
|
|
// inline style — same value channel as knob's four, carousel's item-gap and
|
|
|
// drag-drop's preview-z.
|
|
|
'text-gradient': {
|
|
|
radius: 'var(--radius-lg)',
|
|
|
'font-weight': 'var(--font-weight-medium)',
|
|
|
blur: 'var(--blur-sm)',
|
|
|
'border-padding-block': 'var(--space-1)',
|
|
|
'border-padding-inline': 'var(--space-2)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
// The seed hardcoded #000 here; the hole follows the theme's surface.
|
|
|
'border-inner-bg': 'var(--color-surface-default)'
|
|
|
},
|
|
|
knob: {
|
|
|
'gap-sm': 'var(--size-sm-gap)',
|
|
|
'gap-md': 'var(--size-md-gap)',
|
|
|
'gap-lg': 'var(--size-lg-gap)',
|
|
|
'gap-xl': 'var(--size-xl-gap)',
|
|
|
// The dial is TWICE the control height at every size — a proportion of the
|
|
|
// bundle, kept verbatim per coordinate.
|
|
|
'diameter-sm': 'calc(var(--size-sm-control-height) * 2)',
|
|
|
'diameter-md': 'calc(var(--size-md-control-height) * 2)',
|
|
|
'diameter-lg': 'calc(var(--size-lg-control-height) * 2)',
|
|
|
'diameter-xl': 'calc(var(--size-xl-control-height) * 2)',
|
|
|
'label-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'label-font-size-md': 'var(--size-md-font-size)',
|
|
|
'label-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'label-font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'value-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'value-font-size-md': 'var(--size-md-font-size)',
|
|
|
'value-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'value-font-size-xl': 'var(--size-xl-font-size)',
|
|
|
|
|
|
gap: {
|
|
|
declarations: [
|
|
|
{ value: 'var(--knob-gap-md)', scope: 'host' },
|
|
|
{ value: 'var(--knob-gap-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--knob-gap-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--knob-gap-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
diameter: {
|
|
|
declarations: [
|
|
|
{ value: 'var(--knob-diameter-md)', scope: 'host' },
|
|
|
{ value: 'var(--knob-diameter-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--knob-diameter-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--knob-diameter-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'label-font-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--knob-label-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--knob-label-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--knob-label-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--knob-label-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'value-font-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--knob-value-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--knob-value-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--knob-value-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--knob-value-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// The dial face and its ring.
|
|
|
radius: 'var(--radius-full)',
|
|
|
shadow: 'var(--shadow-2)',
|
|
|
'dragging-shadow': 'var(--shadow-3)',
|
|
|
'track-bg': 'var(--color-neutral-track)',
|
|
|
'face-bg': 'var(--color-surface-raised)',
|
|
|
// SPLIT from a single `--knob-arc-width` that carried two meanings with two
|
|
|
// different defaults: the face inset (--space-3) and the pointer's top
|
|
|
// offset (--space-2). One name that does not distinguish what it should.
|
|
|
'arc-width': 'var(--space-3)',
|
|
|
'pointer-inset': 'var(--space-2)',
|
|
|
'pointer-width': '3px',
|
|
|
'pointer-bg': 'var(--color-neutral-text-strong)',
|
|
|
'indicator-radius': 'var(--radius-sm)',
|
|
|
'value-fg': 'var(--color-neutral-text)',
|
|
|
'label-fg': 'var(--color-neutral-text)',
|
|
|
'label-line-height': 'var(--leading-ui)'
|
|
|
},
|
|
|
kbd: {
|
|
|
'font-size': 'var(--size-xs-font-size)',
|
|
|
'font-weight': 'var(--font-weight-medium)',
|
|
|
'letter-spacing': 'var(--tracking-wide)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
radius: 'var(--radius-sm)',
|
|
|
'padding-inline': '0.4em',
|
|
|
'padding-block': '0.18em',
|
|
|
'min-width': '1.5em',
|
|
|
// Shared by the surface and outline variants.
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-neutral-border)',
|
|
|
'surface-bg': 'var(--color-neutral-element)',
|
|
|
// The physical key-cap relief: a heavier bottom edge plus an inner
|
|
|
// highlight. Surface only — outline and ghost are flat by identity.
|
|
|
'surface-border-block-end-width': 'var(--border-width-medium)',
|
|
|
'surface-shadow': '0 1px 0 var(--color-neutral-border) inset'
|
|
|
},
|
|
|
// LinkPreview's panel is PORTALED and its `data-size` sits on the CONTENT,
|
|
|
// not on the trigger — hence `parts: ['content']` on every resolved name; a
|
|
|
// cascade on the host would never reach it. Size moves PHYSICAL dimensions
|
|
|
// only: family, weight and line-height are constant across sizes on purpose
|
|
|
// (the recipe says so), so those are flat.
|
|
|
'link-preview': {
|
|
|
'padding-block-xs': 'var(--space-2)',
|
|
|
'padding-block-sm': 'var(--space-2)',
|
|
|
'padding-block-md': 'var(--space-3)',
|
|
|
'padding-block-lg': 'var(--space-4)',
|
|
|
'padding-block-xl': 'var(--space-5)',
|
|
|
'padding-inline-xs': 'var(--space-3)',
|
|
|
'padding-inline-sm': 'var(--space-3)',
|
|
|
'padding-inline-md': 'var(--space-4)',
|
|
|
'padding-inline-lg': 'var(--space-5)',
|
|
|
'padding-inline-xl': 'var(--space-6)',
|
|
|
'font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'font-size-md': 'var(--size-sm-font-size)',
|
|
|
'font-size-lg': 'var(--size-md-font-size)',
|
|
|
'font-size-xl': 'var(--size-md-font-size)',
|
|
|
'min-width-xs': '11rem',
|
|
|
'min-width-sm': '12rem',
|
|
|
'min-width-md': '14rem',
|
|
|
'min-width-lg': '18rem',
|
|
|
'min-width-xl': '22rem',
|
|
|
|
|
|
'padding-block': {
|
|
|
parts: ['content'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--link-preview-padding-block-md)', scope: 'host' },
|
|
|
{ value: 'var(--link-preview-padding-block-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--link-preview-padding-block-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--link-preview-padding-block-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--link-preview-padding-block-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'padding-inline': {
|
|
|
parts: ['content'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--link-preview-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--link-preview-padding-inline-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--link-preview-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--link-preview-padding-inline-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--link-preview-padding-inline-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'font-size': {
|
|
|
parts: ['content'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--link-preview-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--link-preview-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--link-preview-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--link-preview-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--link-preview-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'min-width': {
|
|
|
parts: ['content'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--link-preview-min-width-md)', scope: 'host' },
|
|
|
{ value: 'var(--link-preview-min-width-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--link-preview-min-width-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--link-preview-min-width-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--link-preview-min-width-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// Flat: one value across the five sizes.
|
|
|
'max-width': 'min(28rem, calc(100vw - var(--space-4)))',
|
|
|
gap: 'var(--space-2)',
|
|
|
radius: 'var(--radius-md)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'font-weight': 'var(--font-weight-regular)',
|
|
|
// `font-family` / `line-height` are declared even though the `overlay` depth
|
|
|
// plane currently outranks them by LOAD ORDER at an equal (0,1,0) — see the
|
|
|
// recipe. Keeping them is the conservative call: retiring them would fix
|
|
|
// that tie in the plane's favour, and who owns overlay typography is a
|
|
|
// signature still open (next-features §12.9, 20 components affected).
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-normal)',
|
|
|
|
|
|
// Trigger. The chained fallback is PART of the value — the comma is not a
|
|
|
// second knob — so it travels verbatim.
|
|
|
'trigger-radius': 'var(--radius-sm)',
|
|
|
'trigger-fg': 'var(--color-primary-text, var(--color-content-primary))',
|
|
|
'hover-trigger-fg': 'var(--color-primary-solid, var(--color-content-primary))',
|
|
|
// The resting underline: a faint mix of the ink itself. The mix IS the
|
|
|
// look, so the knob is the whole declaration, not the 35 %.
|
|
|
'trigger-underline': 'color-mix(in srgb, currentColor 35%, transparent)'
|
|
|
|
|
|
// NO arrow tokens: its fill/stroke consume the depth system's `overlay`
|
|
|
// plane so the caret always matches the panel (A1, like popover). Coining
|
|
|
// `arrow-fg` would break exactly the guarantee that rule provides.
|
|
|
},
|
|
|
// PickerShell is a COMPONENT the composite pickers import (date, color,
|
|
|
// chronos), not a shared CSS layer — there is no layer hook and no borrowed
|
|
|
// vocabulary.
|
|
|
//
|
|
|
// Its size axis is `data-picker-size` on `[data-popover-content]`, a PORTALED
|
|
|
// ancestor — not `data-size` on itself. The TSC's `size:` scope would emit
|
|
|
// `[data-picker-shell][data-size='x']`, which never matches, so these are
|
|
|
// FLAT public coordinates and the recipe reads them from its own
|
|
|
// `[data-popover-content][data-picker-size='X']` selectors. The base rule is
|
|
|
// the `sm` default; `md` only re-declares what actually changes.
|
|
|
'picker-shell': {
|
|
|
'section-gap-xs': 'var(--space-2)',
|
|
|
'section-gap-sm': 'var(--space-3)',
|
|
|
'section-gap-md': 'var(--space-3-5)',
|
|
|
'section-gap-lg': 'var(--space-4)',
|
|
|
// Header's bottom inset and footer's top inset, one number so the two rows
|
|
|
// stay symmetric around the body.
|
|
|
'row-spacing-xs': 'var(--space-2)',
|
|
|
'row-spacing-sm': 'var(--space-3)',
|
|
|
'row-spacing-lg': 'var(--space-4)',
|
|
|
'footer-gap-xs': 'var(--space-1-5)',
|
|
|
'footer-gap-sm': 'var(--space-2)',
|
|
|
'footer-gap-lg': 'var(--space-2-5)',
|
|
|
'header-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'header-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'header-font-size-md': 'var(--size-md-font-size)',
|
|
|
'header-font-size-lg': 'var(--size-md-font-size)',
|
|
|
|
|
|
// Header and footer share the rule that separates them from the body: one
|
|
|
// width, one colour.
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-border-subtle)',
|
|
|
'header-gap': 'var(--space-2)',
|
|
|
'header-fg': 'var(--color-content-primary)',
|
|
|
'header-font-weight': 'var(--font-weight-medium)',
|
|
|
'header-line-height': 'var(--leading-ui)',
|
|
|
'body-gap': 'var(--space-3)',
|
|
|
|
|
|
// The time row of the composite date+time pickers. It hangs off CLASS
|
|
|
// hooks (`.picker-time-row`) and carries a THIRD axis — `data-size` on the
|
|
|
// class itself, default md — so its coordinates are flat too. The class
|
|
|
// hooks are pre-existing debt (next-features §13, same as gradient-picker).
|
|
|
'time-row-gap': 'var(--space-3)',
|
|
|
'time-row-padding-block-start': 'var(--space-2)',
|
|
|
'time-row-padding-inline': 'var(--space-2)',
|
|
|
'time-row-border-width': 'var(--border-width)',
|
|
|
'time-row-border': 'var(--color-neutral-border)',
|
|
|
'time-label-fg': 'var(--color-content-secondary)',
|
|
|
'time-label-font-size-xs': 'var(--size-xxs-font-size)',
|
|
|
'time-label-font-size-sm': 'var(--size-xs-font-size)',
|
|
|
'time-label-font-size-md': 'var(--size-sm-font-size)',
|
|
|
'time-label-font-size-lg': 'var(--size-md-font-size)'
|
|
|
},
|
|
|
// CardGroup is a COMPOSITION SHELL (ToggleGroup + Collapsible + Card +
|
|
|
// Button): the cards' chrome belongs to Card's recipe and the title's to
|
|
|
// Button's. Only group-level visual lives here. The size axis is sm|md|lg —
|
|
|
// `CardGroupSize = Extract<Size, 'sm' | 'md' | 'lg'>` — so there are no
|
|
|
// xs/xl coordinates to write.
|
|
|
'card-group': {
|
|
|
'padding-sm': 'var(--space-3)',
|
|
|
'padding-md': 'var(--space-4)',
|
|
|
'padding-lg': 'var(--space-5)',
|
|
|
'grid-gap-sm': 'var(--space-2)',
|
|
|
'grid-gap-md': 'var(--space-3)',
|
|
|
'grid-gap-lg': 'var(--space-4)',
|
|
|
padding: {
|
|
|
declarations: [
|
|
|
{ value: 'var(--card-group-padding-md)', scope: 'host' },
|
|
|
{ value: 'var(--card-group-padding-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--card-group-padding-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'grid-gap': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--card-group-grid-gap-md)', scope: 'host' },
|
|
|
{ value: 'var(--card-group-grid-gap-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--card-group-grid-gap-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// The card corner the group nests concentrically against. The recipe
|
|
|
// COMPUTES `--shape-outer-radius` from this plus `padding` (§30), so a
|
|
|
// theme moves the two coordinates and the outer radius recomposes itself —
|
|
|
// that system token is not re-coined here.
|
|
|
'card-radius': 'var(--radius-md)',
|
|
|
|
|
|
// Title — both forms (the disclosure Button and the static heading) share
|
|
|
// the weight. Everything else about the button is Button's recipe.
|
|
|
'title-font-weight': 'var(--font-weight-semibold)',
|
|
|
'title-fg': 'var(--color-content-primary)',
|
|
|
'title-font-family': 'var(--style-label-font-family)',
|
|
|
'title-font-size': 'var(--size-md-font-size)',
|
|
|
'title-line-height': 'var(--leading-ui)',
|
|
|
// The gap under the header, the one part of its padding shorthand that is
|
|
|
// not the group's own inset.
|
|
|
'title-padding-block-end': 'var(--space-2)',
|
|
|
|
|
|
// Chevron — own geometry (two borders rotated 45°), not a composed icon.
|
|
|
// Its colour is `currentColor` on purpose: it inherits from the title.
|
|
|
'chevron-size': '0.5em',
|
|
|
'chevron-width': 'var(--border-width-medium)',
|
|
|
|
|
|
'description-fg': 'var(--color-content-muted)',
|
|
|
'description-font-size': 'var(--size-sm-font-size)',
|
|
|
'description-padding-block-end': 'var(--space-2)'
|
|
|
},
|
|
|
// CodeBlock. The `[data-code-block-code]` node is deliberately ABSENT from
|
|
|
// this contract: it consumes the `--style-code-*` typographic layer (with the
|
|
|
// wrapper's per-instance escape hatch `--_code-block-font-size` in front),
|
|
|
// and a layer's vocabulary is not re-coined by its consumer (shared-layer
|
|
|
// rule 2, the D-TH.2-b reasoning). Everything here is the SHELL around it.
|
|
|
'code-block': {
|
|
|
radius: 'var(--radius-md)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
// Shared by the surface/outline borders and the header's bottom rule.
|
|
|
// Ghost's `transparent` / `border: 0` are variant IDENTITY, not knobs.
|
|
|
'border-width': 'var(--border-width)',
|
|
|
border: 'var(--color-neutral-border)',
|
|
|
'surface-bg': 'var(--color-neutral-element)',
|
|
|
|
|
|
// Header strip.
|
|
|
'header-gap': 'var(--space-2)',
|
|
|
'header-padding-inline': 'var(--space-3)',
|
|
|
'header-padding-block': 'var(--space-1)',
|
|
|
'header-bg': 'var(--color-neutral-track)',
|
|
|
'title-font-family': 'var(--font-family-mono)',
|
|
|
'title-font-size': 'var(--size-xs-font-size)',
|
|
|
'title-fg': 'var(--color-content-secondary)',
|
|
|
'lang-font-family': 'var(--font-family-mono)',
|
|
|
'lang-font-size': 'var(--size-xxs-font-size)',
|
|
|
'lang-fg': 'var(--color-content-muted)',
|
|
|
'lang-letter-spacing': 'var(--tracking-caps)',
|
|
|
|
|
|
// Copy slot. `copy-fg` tints the transient "Copied" INDICATOR, not the
|
|
|
// trigger: the trigger's chrome comes from button.css.
|
|
|
'copy-gap': 'var(--space-1)',
|
|
|
'copy-font-size': 'var(--size-xs-font-size)',
|
|
|
'copy-fg': 'var(--color-affirm-text)',
|
|
|
|
|
|
// The <pre> gutter around the code.
|
|
|
'pre-padding': 'var(--space-3)'
|
|
|
},
|
|
|
combobox: {
|
|
|
'content-z': 'var(--z-index-overlay-floating)',
|
|
|
// ── Size axis (theme-reach 2026-08-20) ───────────────────────────────
|
|
|
// Two tiers: per-size coordinates at a stable scope + ONE resolved name
|
|
|
// the recipe consumes. The control's height/font/gap ride the `--size-{k}-*`
|
|
|
// bundle (identical computed, and the raw primitive is forbidden there);
|
|
|
// its inline padding DEVIATES from the bundle on xs/sm/lg/xl and keeps
|
|
|
// today's value verbatim — the deviation stays visible instead of being
|
|
|
// silently rounded to the bundle (that would be a visual change).
|
|
|
'control-height-xs': 'var(--size-xs-control-height)',
|
|
|
'control-height-sm': 'var(--size-sm-control-height)',
|
|
|
'control-height-md': 'var(--size-md-control-height)',
|
|
|
'control-height-lg': 'var(--size-lg-control-height)',
|
|
|
'control-height-xl': 'var(--size-xl-control-height)',
|
|
|
'control-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'control-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'control-font-size-md': 'var(--size-md-font-size)',
|
|
|
'control-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'control-font-size-xl': 'var(--size-xl-font-size)',
|
|
|
'control-gap-xs': 'var(--space-1)',
|
|
|
'control-gap-sm': 'var(--space-1-5)',
|
|
|
'control-gap-md': 'var(--space-2)',
|
|
|
'control-gap-lg': 'var(--space-2-5)',
|
|
|
'control-gap-xl': 'var(--space-3)',
|
|
|
'control-padding-inline-xs': 'var(--space-2)',
|
|
|
'control-padding-inline-sm': 'var(--space-2-5)',
|
|
|
'control-padding-inline-md': 'var(--space-3)',
|
|
|
'control-padding-inline-lg': 'var(--space-3-5)',
|
|
|
'control-padding-inline-xl': 'var(--space-4)',
|
|
|
'content-padding-block-xs': 'var(--space-0-5)',
|
|
|
'content-padding-block-sm': 'var(--space-1)',
|
|
|
'content-padding-block-md': 'var(--space-1-5)',
|
|
|
'content-padding-block-lg': 'var(--space-2)',
|
|
|
'content-padding-block-xl': 'var(--space-2-5)',
|
|
|
'content-padding-inline-xs': 'var(--space-0-5)',
|
|
|
'content-padding-inline-sm': 'var(--space-1)',
|
|
|
'content-padding-inline-md': 'var(--space-1-5)',
|
|
|
'content-padding-inline-lg': 'var(--space-2)',
|
|
|
'content-padding-inline-xl': 'var(--space-2-5)',
|
|
|
'content-font-size-xs': 'var(--size-xs-font-size)',
|
|
|
'content-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'content-font-size-md': 'var(--size-md-font-size)',
|
|
|
'content-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'content-font-size-xl': 'var(--size-xl-font-size)',
|
|
|
// The resolved coordinates. The CONTENT is portaled, so its per-size
|
|
|
// cascade targets that part explicitly (`parts`) — the same reason
|
|
|
// `select` declares its accent on trigger + content (tsc.md, multi-part).
|
|
|
'control-height': {
|
|
|
parts: ['control', 'input'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--combobox-control-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--combobox-control-height-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--combobox-control-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--combobox-control-height-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--combobox-control-height-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'control-font-size': {
|
|
|
parts: ['control', 'input'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--combobox-control-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--combobox-control-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--combobox-control-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--combobox-control-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--combobox-control-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'control-gap': {
|
|
|
parts: ['control', 'input'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--combobox-control-gap-md)', scope: 'host' },
|
|
|
{ value: 'var(--combobox-control-gap-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--combobox-control-gap-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--combobox-control-gap-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--combobox-control-gap-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'control-padding-inline': {
|
|
|
parts: ['control', 'input'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--combobox-control-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--combobox-control-padding-inline-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--combobox-control-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--combobox-control-padding-inline-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--combobox-control-padding-inline-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'content-padding-block': {
|
|
|
parts: ['content'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--combobox-content-padding-block-md)', scope: 'host' },
|
|
|
{ value: 'var(--combobox-content-padding-block-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--combobox-content-padding-block-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--combobox-content-padding-block-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--combobox-content-padding-block-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'content-padding-inline': {
|
|
|
parts: ['content'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--combobox-content-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--combobox-content-padding-inline-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--combobox-content-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--combobox-content-padding-inline-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--combobox-content-padding-inline-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
'content-font-size': {
|
|
|
parts: ['content'],
|
|
|
declarations: [
|
|
|
{ value: 'var(--combobox-content-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--combobox-content-font-size-xs)', scope: 'size:xs' },
|
|
|
{ value: 'var(--combobox-content-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--combobox-content-font-size-lg)', scope: 'size:lg' },
|
|
|
{ value: 'var(--combobox-content-font-size-xl)', scope: 'size:xl' }
|
|
|
]
|
|
|
},
|
|
|
// ── Chrome ───────────────────────────────────────────────────────────
|
|
|
gap: 'var(--space-2)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
'control-radius': 'var(--radius-md)',
|
|
|
'control-bg': 'var(--color-surface-default)',
|
|
|
'control-border': 'var(--color-border-default)',
|
|
|
'control-shadow': 'var(--shadow-subtle)',
|
|
|
'control-fg': 'var(--color-content-primary)',
|
|
|
'control-line-height': 'var(--leading-ui)',
|
|
|
'disabled-control-bg': 'var(--color-surface-raised)',
|
|
|
'disabled-control-border': 'var(--color-border-subtle)',
|
|
|
'disabled-control-fg': 'var(--color-content-disabled)',
|
|
|
'placeholder-fg': 'var(--color-content-muted)',
|
|
|
'disabled-input-fg': 'var(--color-content-disabled)',
|
|
|
'trigger-size': 'var(--size-xs-control-height)',
|
|
|
'trigger-radius': 'var(--radius-full)',
|
|
|
'trigger-fg': 'var(--color-content-secondary)',
|
|
|
'indicator-size': 'var(--size-sm-icon-size)',
|
|
|
// ── Panel ────────────────────────────────────────────────────────────
|
|
|
'content-min-width': '14rem',
|
|
|
'content-max-width': '28rem',
|
|
|
'content-max-height': '22rem',
|
|
|
'content-radius': 'var(--radius-lg)',
|
|
|
'content-fg': 'var(--color-content-primary)',
|
|
|
'content-font-family': 'var(--style-label-font-family)',
|
|
|
'content-line-height': 'var(--leading-ui)',
|
|
|
'viewport-gap': 'var(--space-0-5)',
|
|
|
'viewport-padding-block-end': 'var(--space-0-5)',
|
|
|
'scrollbar-inset': 'var(--space-3-5)',
|
|
|
'group-gap': 'var(--space-1)',
|
|
|
'group-padding-block': 'var(--space-1)',
|
|
|
'group-heading-padding-block': 'var(--space-1)',
|
|
|
'group-heading-padding-inline': 'var(--space-2)',
|
|
|
'group-heading-bg': 'var(--color-surface-overlay)',
|
|
|
'group-heading-fg': 'var(--color-content-muted)',
|
|
|
'group-heading-font-family': 'var(--style-label-font-family)',
|
|
|
'group-heading-font-size': 'var(--size-xs-font-size)',
|
|
|
'group-heading-font-weight': 'var(--font-weight-semibold)',
|
|
|
'group-heading-line-height': 'var(--leading-ui)',
|
|
|
'group-heading-letter-spacing': 'var(--tracking-caps)',
|
|
|
'separator-thickness': 'var(--border-width)',
|
|
|
'separator-margin-block': 'var(--space-1)',
|
|
|
'separator-bg': 'var(--color-neutral-separator)',
|
|
|
'item-gap': 'var(--space-2)',
|
|
|
'item-padding-inline': 'var(--space-2)',
|
|
|
'item-fg': 'var(--color-content-primary)',
|
|
|
'disabled-item-fg': 'var(--color-content-disabled)',
|
|
|
'item-indicator-size': 'var(--size-sm-icon-size)',
|
|
|
// ── Selected tags ────────────────────────────────────────────────────
|
|
|
'selected-tags-gap': 'var(--space-1-5)',
|
|
|
'selected-tags-margin-block-end': 'var(--space-2)',
|
|
|
'selected-tag-gap': 'var(--space-1)',
|
|
|
'selected-tag-padding-inline-start': 'var(--space-2)',
|
|
|
'selected-tag-padding-inline-end': 'var(--space-0-5)',
|
|
|
// The tag is the control MINUS a step: the expression is the knob, so a
|
|
|
// theme moving `control-height` still gets a proportional tag.
|
|
|
'selected-tag-inset': 'var(--space-2)',
|
|
|
'selected-tag-radius': 'var(--radius-sm)',
|
|
|
'selected-tag-bg': 'var(--color-surface-raised)',
|
|
|
'selected-tag-border': 'var(--color-border-default)',
|
|
|
'selected-tag-fg': 'var(--color-content-primary)',
|
|
|
'selected-tag-line-height': 'var(--leading-ui)',
|
|
|
// Optical −1px on the token-derived size (kept verbatim from the recipe).
|
|
|
'selected-tag-font-size-offset': '1px',
|
|
|
'selected-tag-remove-size': 'var(--space-4)',
|
|
|
'selected-tag-remove-margin-inline-start': 'var(--space-0-5)',
|
|
|
'selected-tag-remove-radius': 'var(--radius-sm)',
|
|
|
// Accent palette forward (THM-2) — the cascade lived on FOUR parts
|
|
|
// (control / input / trigger / content), so the forward targets all four
|
|
|
// via `parts`. Host = primary; shared layer routes roles + 33 scales + custom.
|
|
|
'_palette-track': {
|
|
|
parts: ['control', 'input', 'trigger', 'content'],
|
|
|
declarations: [{ value: 'var(--color-primary-track)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-border': {
|
|
|
parts: ['control', 'input', 'trigger', 'content'],
|
|
|
declarations: [{ value: 'var(--color-primary-border)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-text': {
|
|
|
parts: ['control', 'input', 'trigger', 'content'],
|
|
|
declarations: [{ value: 'var(--color-primary-text)', scope: 'host' }]
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// Command palette. Size axis sm | md | lg on the root; the panel is the same
|
|
|
// surface inline and inside <Command.Dialog>, so every knob lives here and
|
|
|
// the dialog only swaps the radius reference.
|
|
|
command: {
|
|
|
// Per-size coordinates - all four map 1:1 to the size bundle (measured
|
|
|
// 2026-08-20: space-2/3/4 and font-size sm/md/lg ARE the bundle's).
|
|
|
'input-padding-inline-sm': 'var(--size-sm-padding-inline)',
|
|
|
'input-padding-inline-md': 'var(--size-md-padding-inline)',
|
|
|
'input-padding-inline-lg': 'var(--size-lg-padding-inline)',
|
|
|
'input-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'input-font-size-md': 'var(--size-md-font-size)',
|
|
|
'input-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'item-padding-inline-sm': 'var(--size-sm-padding-inline)',
|
|
|
'item-padding-inline-md': 'var(--size-md-padding-inline)',
|
|
|
'item-padding-inline-lg': 'var(--size-lg-padding-inline)',
|
|
|
'item-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'item-font-size-md': 'var(--size-md-font-size)',
|
|
|
'item-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
|
|
|
// The RESOLVED names - the only ones the recipe reads. Default `host`
|
|
|
// (= md) plus the two overrides; the root stamps `data-size` and every
|
|
|
// consumer hangs from it, so no `parts` are needed.
|
|
|
'input-padding-inline': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--command-input-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--command-input-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--command-input-padding-inline-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'input-font-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--command-input-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--command-input-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--command-input-font-size-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'item-padding-inline': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--command-item-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--command-item-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--command-item-padding-inline-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'item-font-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--command-item-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--command-item-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--command-item-font-size-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
|
|
|
// Panel. `radius` is the INLINE palette's own surface radius; inside
|
|
|
// <Command.Dialog> the dialog panel is the surface and the recipe zeroes
|
|
|
// the command's radius, so the token has no consumer there (the input
|
|
|
// keeps its own `input-radius`). A re-point at the dialog's radius used
|
|
|
// to be claimed here - measured dead 2026-08-21 and retired.
|
|
|
radius: 'var(--radius-lg)',
|
|
|
'font-family': 'var(--style-label-font-family)',
|
|
|
'line-height': 'var(--leading-ui)',
|
|
|
fg: 'var(--color-content-primary)',
|
|
|
|
|
|
// Input - the boxed search field (same treatment as the Select trigger).
|
|
|
'input-padding-block': 'var(--space-1-5)',
|
|
|
'input-margin': 'var(--space-2)',
|
|
|
'input-border-width': 'var(--border-width)',
|
|
|
'input-border': 'var(--color-border-default)',
|
|
|
'input-radius': 'var(--radius-md)',
|
|
|
'input-bg': 'var(--color-surface-default)',
|
|
|
'input-fg': 'var(--color-content-primary)',
|
|
|
'input-placeholder-fg': 'var(--color-content-muted)',
|
|
|
'focus-input-border': 'var(--color-primary-border)',
|
|
|
|
|
|
// Loading bar under the input.
|
|
|
'loading-height': '2px',
|
|
|
'loading-fg': 'var(--color-primary-solid)',
|
|
|
|
|
|
// List viewport. `scrollbar-inset` is the extra inline room the design
|
|
|
// scrollbar takes when it shows - the same name combobox uses, so the
|
|
|
// two list surfaces read alike.
|
|
|
'viewport-max-block': 'min(60vh, 24rem)',
|
|
|
'viewport-gap': 'var(--space-1)',
|
|
|
'viewport-padding-inline': 'var(--space-2)',
|
|
|
'viewport-padding-block-end': 'var(--space-1)',
|
|
|
'scrollbar-inset': 'var(--space-3-5)',
|
|
|
'viewport-padding': 'var(--space-1)',
|
|
|
|
|
|
// Groups.
|
|
|
'group-gap': 'var(--space-1)',
|
|
|
'group-heading-padding-block': 'var(--space-1)',
|
|
|
'group-heading-fg': 'var(--color-content-muted)',
|
|
|
'group-heading-font-size': 'var(--size-xs-font-size)',
|
|
|
'group-heading-font-weight': 'var(--font-weight-medium)',
|
|
|
'group-heading-tracking': 'var(--tracking-caps)',
|
|
|
'group-items-gap': '1px',
|
|
|
|
|
|
// Rows.
|
|
|
'item-gap': 'var(--space-2)',
|
|
|
'item-fg': 'var(--color-content-primary)',
|
|
|
'item-icon-size': '1.125em',
|
|
|
'item-icon-fg': 'var(--color-content-secondary)',
|
|
|
'item-shortcut-gap': 'var(--space-1)',
|
|
|
'item-shortcut-fg': 'var(--color-content-muted)',
|
|
|
'item-shortcut-font-size': '0.875em',
|
|
|
'selected-item-sub-fg': 'var(--color-content-primary)',
|
|
|
|
|
|
// Empty state.
|
|
|
'empty-padding-block': 'var(--space-4)',
|
|
|
'empty-fg': 'var(--color-content-muted)',
|
|
|
|
|
|
// Separator.
|
|
|
'separator-height': 'var(--border-width)',
|
|
|
'separator-margin-block': 'var(--space-1)',
|
|
|
'separator-bg': 'var(--color-neutral-separator)'
|
|
|
},
|
|
|
|
|
|
// DragDrop — the drag preview/ghost that tracks the cursor. Floats above
|
|
|
// local content during an active drag.
|
|
|
'drag-drop': {
|
|
|
// The PREVIEW — the ghost that follows the pointer. It exists only during a
|
|
|
// drag, so neither the probe nor the guard sees it at rest.
|
|
|
'preview-padding-block': 'var(--space-2)',
|
|
|
'preview-padding-inline': 'var(--space-3)',
|
|
|
'preview-radius': 'var(--radius-md)',
|
|
|
'preview-border-width': 'var(--border-width)',
|
|
|
'preview-bg': 'var(--color-surface-default)',
|
|
|
'preview-fg': 'var(--color-content-primary)',
|
|
|
'preview-font-family': 'var(--style-label-font-family)',
|
|
|
'preview-font-size': 'var(--size-sm-font-size)',
|
|
|
// The lift under the ghost. The accent ring stacked on top of it comes from
|
|
|
// the palette forward and stays private.
|
|
|
'preview-shadow': 'var(--shadow-raised)',
|
|
|
// NO `preview-z`: soma writes `z-index: 9999` INLINE on the preview
|
|
|
// (`drag-drop-provider.svelte.ts:663,671`), so no cascade can win. Measured
|
|
|
// on the REAL node during an actual drag — the adversarial review of F2-B,
|
|
|
// 2026-08-22. Same class as carousel's retired `item-gap*`: a value channel
|
|
|
// of soma, not a theme surface (next-features §13).
|
|
|
// Selection palette (THM-2) — `data-color` sits on `[data-drag-drop-root]`.
|
|
|
'_palette-solid': {
|
|
|
parts: ['root'],
|
|
|
declarations: [{ value: 'var(--color-primary-solid)', scope: 'host' }]
|
|
|
},
|
|
|
'_palette-element': {
|
|
|
parts: ['root'],
|
|
|
declarations: [{ value: 'var(--color-primary-element)', scope: 'host' }]
|
|
|
}
|
|
|
},
|
|
|
|
|
|
'navigation-menu': {
|
|
|
// Inline (non-portaled) dropdown panel: stacks above normal page flow.
|
|
|
'content-z': 'var(--z-index-overlay-inline)',
|
|
|
// `-w` is soma's VALUE CHANNEL, not theming surface: the provider writes
|
|
|
// the active trigger's rect into it on every move and the recipe reads it
|
|
|
// with a `0px` fallback. `-x` sat here too and NOBODY read it — soma
|
|
|
// stamps the horizontal tracking as an inline `left` (its own README says
|
|
|
// so, and the recipe's comment records that adding `translateX(var(--…-x))`
|
|
|
// DOUBLED the offset), so the declaration moved nothing anywhere:
|
|
|
// retired 2026-08-23, 0 diffs, which is the proof it was dead. `-y` / `-h`
|
|
|
// were never declared, which is the same reading from the other side.
|
|
|
// The underline's own FORM — thickness, fill, radius — is what belongs to
|
|
|
// a theme, and those are the three below.
|
|
|
'indicator-w': '0px',
|
|
|
'indicator-thickness': 'var(--border-width-medium)',
|
|
|
'indicator-bg': 'var(--color-primary-solid)',
|
|
|
'indicator-radius': 'var(--radius-full)',
|
|
|
// The in-bar TRIGGER (and the Link, which shares its chrome) is a
|
|
|
// control, so it takes the canonical size bundle (`--size-{k}-*`) — the
|
|
|
// sidebar row's precedent, 2026-08-19. Until then its whole chrome lived
|
|
|
// in recipe privates: `--_navigation-menu-radius` pinned to
|
|
|
// `--radius-default` («decoupled from size», a line that read Radix as
|
|
|
// a rule when Radix paints nothing), `padding-block: var(--space-2)` as
|
|
|
// the height (a SPACE token riding the density-space scale: measured
|
|
|
// 33.5 / 36 / 41 px across sm/md/lg, only `md` on the control scale by
|
|
|
// arithmetic luck), and the hover / open fills hand-picked. None of it
|
|
|
// was reachable by a theme short of moving `--radius-default` for the
|
|
|
// whole system. Nav runs `sm..lg`, default `md` — and at `md` the
|
|
|
// default is exactly what shipped (36px · 6px radius).
|
|
|
'trigger-height-sm': 'var(--size-sm-control-height)',
|
|
|
'trigger-height-md': 'var(--size-md-control-height)',
|
|
|
'trigger-height-lg': 'var(--size-lg-control-height)',
|
|
|
'trigger-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'trigger-font-size-md': 'var(--size-md-font-size)',
|
|
|
'trigger-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'trigger-line-height-sm': 'var(--size-sm-font-line-height)',
|
|
|
'trigger-line-height-md': 'var(--size-md-font-line-height)',
|
|
|
'trigger-line-height-lg': 'var(--size-lg-font-line-height)',
|
|
|
'trigger-padding-inline-sm': 'var(--size-sm-padding-inline)',
|
|
|
'trigger-padding-inline-md': 'var(--size-md-padding-inline)',
|
|
|
'trigger-padding-inline-lg': 'var(--size-lg-padding-inline)',
|
|
|
'trigger-radius-sm': 'var(--size-sm-radius)',
|
|
|
'trigger-radius-md': 'var(--size-md-radius)',
|
|
|
'trigger-radius-lg': 'var(--size-lg-radius)',
|
|
|
// The resolved coordinates for the size in force (`host` = `md`, the
|
|
|
// component's default); each `size:` scope is the emitted cascade entry,
|
|
|
// so the recipe consumes ONE name per axis and the bundle guard sees
|
|
|
// the reference in the token, not a literal buried in a rule.
|
|
|
'trigger-height': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--navigation-menu-trigger-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--navigation-menu-trigger-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--navigation-menu-trigger-height-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'trigger-font-size': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--navigation-menu-trigger-font-size-md)', scope: 'host' },
|
|
|
{ value: 'var(--navigation-menu-trigger-font-size-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--navigation-menu-trigger-font-size-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'trigger-line-height': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--navigation-menu-trigger-line-height-md)', scope: 'host' },
|
|
|
{ value: 'var(--navigation-menu-trigger-line-height-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--navigation-menu-trigger-line-height-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'trigger-padding-inline': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--navigation-menu-trigger-padding-inline-md)', scope: 'host' },
|
|
|
{ value: 'var(--navigation-menu-trigger-padding-inline-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--navigation-menu-trigger-padding-inline-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
'trigger-radius': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--navigation-menu-trigger-radius-md)', scope: 'host' },
|
|
|
{ value: 'var(--navigation-menu-trigger-radius-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--navigation-menu-trigger-radius-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
// Text and the ACTIVE accent. Hover is NOT a token and NOT the
|
|
|
// recipe's: it is the system's state layer (`--state-hover`,
|
|
|
// `archetypes.css`, config `primitives.state`), painted on the trigger's
|
|
|
// OWN box and radius — the same hover every interactive element has. The
|
|
|
// grey square the author saw was that veil painted on the `<li>` (no
|
|
|
// radius, no shape) through a stray `archetype: 'item'`; the answer was
|
|
|
// to take the archetype off the wrapper, not to invent a nav-only hover.
|
|
|
'trigger-fg': 'var(--color-content-primary)',
|
|
|
'active-trigger-fg': 'var(--color-content-primary)',
|
|
|
'active-trigger-bg': 'var(--color-primary-element)',
|
|
|
'disabled-trigger-fg': 'var(--color-content-muted)',
|
|
|
// The bar's TYPE. The family completes the pair the size bundle already
|
|
|
// covered (`trigger-font-size` / `-line-height`, both consumed on the
|
|
|
// root): the value is the system's — the named typographic layer — and
|
|
|
// the KNOB is the component's, so a theme can set the nav's face without
|
|
|
// moving `label` across the app. Accordion's `trigger-font-family` is the
|
|
|
// mould. NOT declared for the panel: its `[data-depth='overlay']` plane
|
|
|
// writes `font-family` / `line-height` at the same specificity and later
|
|
|
// in the cascade, so a token there would read dead (§12.9, pending
|
|
|
// signature).
|
|
|
'trigger-font-family': 'var(--style-label-font-family)',
|
|
|
'trigger-gap': 'var(--space-1)',
|
|
|
// Chevron — the glyph is drawn with two borders, so ONE stroke token
|
|
|
// feeds both edges. Size stays a literal: every chevron in the catalogue
|
|
|
// is one (`nav-tree` 0.42em, `dropdown-menu` 0.625rem, `card-group`
|
|
|
// 0.5em) — a glyph is not a step of the spacing scale.
|
|
|
'chevron-size': '0.5rem',
|
|
|
'chevron-stroke': 'var(--border-width-medium)',
|
|
|
// The bar's rhythm rides the size axis: it was a private with THREE
|
|
|
// sources (0 / space-1 / space-2 by `data-size`), which is a SCALE, not a
|
|
|
// switch — so it goes up to the TSC like the trigger bundle above.
|
|
|
'list-gap-sm': '0',
|
|
|
'list-gap-md': 'var(--space-1)',
|
|
|
'list-gap-lg': 'var(--space-2)',
|
|
|
'list-gap': {
|
|
|
declarations: [
|
|
|
{ value: 'var(--navigation-menu-list-gap-md)', scope: 'host' },
|
|
|
{ value: 'var(--navigation-menu-list-gap-sm)', scope: 'size:sm' },
|
|
|
{ value: 'var(--navigation-menu-list-gap-lg)', scope: 'size:lg' }
|
|
|
]
|
|
|
},
|
|
|
// The mega-menu panel. Same names as the sibling menus (dropdown-menu /
|
|
|
// context-menu / select), different values — this panel is wider and
|
|
|
// rounder because it holds a grid of links, not a column of items.
|
|
|
'content-min-width': '14rem',
|
|
|
'content-padding': 'var(--space-2)',
|
|
|
'content-radius': 'var(--radius-lg)',
|
|
|
'content-fg': 'var(--color-content-primary)',
|
|
|
// The rows INSIDE the panel are the consumer's bare `<a>`, not the `Item`
|
|
|
// part (that is the bar's `<li>`) and not the bar `Link` (which shares the
|
|
|
// trigger's chrome): naming them `item-*` or `link-*` would promise a
|
|
|
// theme control over a node the token does not reach. Their tint keeps
|
|
|
// reading `trigger-fg`, as it shipped.
|
|
|
'content-link-padding-block': 'var(--space-2)',
|
|
|
'content-link-padding-inline': 'var(--space-3)',
|
|
|
'content-link-radius': 'var(--radius-md)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// DropdownMenu — panel chrome + item rows. Mirror tokens to context-menu.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'dropdown-menu': {
|
|
|
// Floating menu panel stacks in the top overlay band (matches
|
|
|
// `select` — both are click-triggered list overlays). Without this
|
|
|
// the portaled panel inherits `z-index: auto` and any positioned
|
|
|
// page element with a positive z-index (e.g. a selected
|
|
|
// `[data-toggle-group-item]`, z-index 1) paints THROUGH it.
|
|
|
'content-z': 'var(--z-index-overlay-floating)',
|
|
|
// El cromo del panel y de sus filas, gemelo del de context-menu: mismos
|
|
|
// nombres, misma doctrina. Con dos limpiezas de paso, las mismas que allí:
|
|
|
// el `font-family` era respaldo de SÍ MISMO y el peso del encabezado
|
|
|
// llevaba un `500` muerto detrás del token que ya vale 500.
|
|
|
// NO `content-font-family`: the panel carries `data-depth=overlay` and the
|
|
|
// PLANE declares the family at the same specificity, later in the cascade —
|
|
|
// it wins, so a token here would lie (measured 2026-08-23; PLAN §12.9).
|
|
|
'content-fg': 'var(--color-content-primary)',
|
|
|
'item-fg': 'var(--color-content-primary)',
|
|
|
'item-line-height': 'var(--leading-ui)',
|
|
|
// El galón del submenú: dos bordes girados 45°, un trazo y una caja.
|
|
|
'chevron-stroke': 'var(--border-width-medium)',
|
|
|
'chevron-size': '0.625rem',
|
|
|
'heading-fg': 'var(--color-content-muted)',
|
|
|
'heading-font-weight': 'var(--style-label-font-weight)',
|
|
|
'heading-letter-spacing': 'var(--tracking-caps)',
|
|
|
// El filo del separador es el trazo fino del sistema, no un 1px suelto.
|
|
|
'separator-thickness': 'var(--border-width)',
|
|
|
'content-min-width': '12rem',
|
|
|
'content-max-height': 'min(60vh, 22rem)',
|
|
|
'content-padding': 'var(--space-1)',
|
|
|
'content-radius': 'var(--radius-md)',
|
|
|
'content-font-size': 'var(--size-sm-font-size)',
|
|
|
'item-gap': 'var(--space-2)',
|
|
|
'item-height': '2rem',
|
|
|
'item-padding-inline': 'var(--space-2)',
|
|
|
'item-padding-block': 'var(--space-1)',
|
|
|
'item-radius': 'var(--radius-sm)',
|
|
|
'hover-item-bg': 'var(--color-primary-element)',
|
|
|
'hover-item-fg': 'var(--color-content-primary)',
|
|
|
'item-disabled-opacity': '0.55',
|
|
|
'heading-padding-inline': 'var(--space-2)',
|
|
|
'heading-padding-block': 'var(--space-1)',
|
|
|
'heading-font-size': 'var(--size-xs-font-size)',
|
|
|
'separator-bg': 'var(--color-neutral-separator)',
|
|
|
'separator-margin-block': 'var(--space-1)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Menubar — the bar surface, the in-bar trigger and the role=dialog Panel
|
|
|
// (the form surface a bar entry can drop instead of a role=menu Content;
|
|
|
// the Content's chrome belongs to dropdown-menu, which paints it).
|
|
|
//
|
|
|
// The size axis is the bar's: the trigger resolves its step from the bar's
|
|
|
// `data-size`, the Panel from its own (it travels by portal). Nothing here
|
|
|
// is coined for the hover of the trigger — that veil is the SYSTEM's, on
|
|
|
// the archetype (recipe-contract §2), and the shorthand that overrides it
|
|
|
// today is a defect of the navigation-menu axis, not a knob to declare.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
menubar: {
|
|
|
// The bar.
|
|
|
bg: 'var(--color-surface-raised)',
|
|
|
'border-fg': 'var(--color-border-default)',
|
|
|
'border-width': 'var(--border-width)',
|
|
|
// Radius DECOUPLED from the size axis (Radix model): one value, and the
|
|
|
// shape engine derives the trigger's from it.
|
|
|
radius: 'var(--radius-default)',
|
|
|
'gap-sm': 'var(--space-0-5)',
|
|
|
'gap-md': 'var(--space-1)',
|
|
|
'gap-lg': 'var(--space-2)',
|
|
|
'padding-sm': 'var(--space-0-5)',
|
|
|
'padding-md': 'var(--space-1)',
|
|
|
'padding-lg': 'var(--space-2)',
|
|
|
'height-sm': 'var(--size-sm-control-height)',
|
|
|
'height-md': 'var(--size-md-control-height)',
|
|
|
'height-lg': 'var(--size-lg-control-height)',
|
|
|
// The in-bar trigger.
|
|
|
'trigger-fg': 'var(--color-content-primary)',
|
|
|
'trigger-font-family': 'var(--style-label-font-family)',
|
|
|
'trigger-font-weight': 'var(--font-weight-medium)',
|
|
|
'trigger-line-height': 'var(--leading-ui)',
|
|
|
'trigger-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'trigger-font-size-md': 'var(--size-md-font-size)',
|
|
|
'trigger-font-size-lg': 'var(--size-lg-font-size)',
|
|
|
'trigger-padding-inline-sm': 'var(--space-2)',
|
|
|
'trigger-padding-inline-md': 'var(--space-3)',
|
|
|
'trigger-padding-inline-lg': 'var(--space-4)',
|
|
|
// Open entry — its own chrome, not a hover: the pair travels together
|
|
|
// even where both read the same value today.
|
|
|
'open-trigger-bg': 'var(--color-primary-element)',
|
|
|
'open-trigger-fg': 'var(--color-content-primary)',
|
|
|
'disabled-trigger-fg': 'var(--color-content-muted)',
|
|
|
// The Panel. Same overlay band as the sibling menu panels — both drop
|
|
|
// from the bar.
|
|
|
'panel-z': 'var(--z-index-overlay-floating)',
|
|
|
'panel-min-width': '16rem',
|
|
|
'panel-max-height': 'min(70vh, 28rem)',
|
|
|
'panel-padding': 'var(--space-3)',
|
|
|
'panel-radius': 'var(--radius-md)',
|
|
|
'panel-fg': 'var(--color-content-primary)',
|
|
|
// NO `panel-font-family` / `panel-line-height`: the panel carries
|
|
|
// `data-depth='overlay'` and the PLANE declares both at the same
|
|
|
// specificity, later in the cascade — it wins, so a token here would lie
|
|
|
// (measured 2026-08-23; it is the open question of PLAN §12.9).
|
|
|
'panel-font-size-sm': 'var(--size-sm-font-size)',
|
|
|
'panel-font-size-md': 'var(--size-md-font-size)',
|
|
|
'panel-font-size-lg': 'var(--size-lg-font-size)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// ContextMenu — same surface as DropdownMenu, with a span Trigger.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'context-menu': {
|
|
|
// El cromo propio del panel y de su disparador. Dos limpiezas de paso: el
|
|
|
// `font-family` leía `var(--style-label-font-family, var(--style-label-font-family))`
|
|
|
// —respaldo de sí mismo— y el peso del encabezado llevaba un `500` muerto
|
|
|
// detrás del token que ya vale 500.
|
|
|
// NO `content-font-family`: the panel carries `data-depth=overlay` and the
|
|
|
// PLANE declares the family at the same specificity, later in the cascade —
|
|
|
// it wins, so a token here would lie (measured 2026-08-23; PLAN §12.9).
|
|
|
'content-fg': 'var(--color-content-primary)',
|
|
|
'item-fg': 'var(--color-content-primary)',
|
|
|
'item-line-height': 'var(--leading-ui)',
|
|
|
// El galón del submenú: dos bordes girados 45°, un solo trazo.
|
|
|
'chevron-stroke': 'var(--border-width-medium)',
|
|
|
'heading-fg': 'var(--color-content-muted)',
|
|
|
'heading-font-weight': 'var(--style-label-font-weight)',
|
|
|
'heading-letter-spacing': 'var(--tracking-caps)',
|
|
|
// El disparador abierto se marca con un contorno DISCONTINUO: el estilo es
|
|
|
// identidad de la afordancia, el grosor y el color son knobs.
|
|
|
'open-trigger-outline-width': 'var(--border-width)',
|
|
|
'open-trigger-outline': 'var(--color-border-default)',
|
|
|
'open-trigger-radius': 'var(--radius-sm)',
|
|
|
'content-min-width': '12rem',
|
|
|
'content-max-height': 'min(60vh, 22rem)',
|
|
|
'content-padding': 'var(--space-1)',
|
|
|
'content-radius': 'var(--radius-md)',
|
|
|
'content-font-size': 'var(--size-sm-font-size)',
|
|
|
'item-gap': 'var(--space-2)',
|
|
|
'item-height': '2rem',
|
|
|
'item-padding-inline': 'var(--space-2)',
|
|
|
'item-padding-block': 'var(--space-1)',
|
|
|
'item-radius': 'var(--radius-sm)',
|
|
|
'hover-item-bg': 'var(--color-primary-element)',
|
|
|
'hover-item-fg': 'var(--color-content-primary)',
|
|
|
'item-disabled-opacity': '0.55',
|
|
|
'heading-padding-inline': 'var(--space-2)',
|
|
|
'heading-padding-block': 'var(--space-1)',
|
|
|
'heading-font-size': 'var(--size-xs-font-size)',
|
|
|
'separator-bg': 'var(--color-neutral-separator)',
|
|
|
'separator-margin-block': 'var(--space-1)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// DateRangeField — layout only; chrome inherits from date-field.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'date-range-field': {
|
|
|
// El hueco ENTRE los dos campos y su separador. La clave se llamaba
|
|
|
// `row-gap` y alimentaba el `column-gap`: nombre corregido, valor idéntico.
|
|
|
'column-gap': 'var(--space-3)',
|
|
|
// Y el de VERDAD: etiqueta ↔ fila de campos, que leía `--space-1-5` a pelo.
|
|
|
'row-gap': 'var(--space-1-5)',
|
|
|
// La raíz del rango NO es un `[data-field]`, así que no puede heredar la
|
|
|
// etiqueta de field.css (sus tokens de etiqueta viven en ese ámbito). Los
|
|
|
// mismos valores, nombrados aquí. Cuando el mandato de composición de Field
|
|
|
// (§5.3-3) convierta la raíz en un campo, estas tres desaparecen.
|
|
|
'label-fg': 'var(--color-content-primary)',
|
|
|
'label-font-size': 'var(--size-sm-font-size)',
|
|
|
'label-font-weight': 'var(--font-weight-regular)',
|
|
|
'separator-fg': 'var(--color-content-muted)',
|
|
|
'separator-glyph': "'—'"
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// TimeRangeField — layout only; chrome inherits from time-field.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'time-range-field': {
|
|
|
// Gemelo exacto de date-range-field, y con el mismo nombre equivocado: la
|
|
|
// clave `row-gap` alimentaba el `column-gap`. Dos claves, cada una con el
|
|
|
// nombre de lo que mueve, y el computado sin tocar.
|
|
|
'column-gap': 'var(--space-3)',
|
|
|
'row-gap': 'var(--space-1-5)',
|
|
|
// La raíz del rango NO es un `[data-field]`: no puede heredar la etiqueta de
|
|
|
// field.css. Las tres desaparecen cuando el mandato §5.3-3 la convierta en
|
|
|
// un campo.
|
|
|
'label-fg': 'var(--color-content-primary)',
|
|
|
'label-font-size': 'var(--size-sm-font-size)',
|
|
|
'label-font-weight': 'var(--font-weight-regular)',
|
|
|
'separator-fg': 'var(--color-content-muted)',
|
|
|
'separator-glyph': "'—'"
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// Fab — the FAB-specific size / icon / elevation scale. Deliberately OFF
|
|
|
// the control-height ladder (Material 40 / 56 / 72); the VALUES live here
|
|
|
// (generated to `--fab-*`), not as literals in fab.css.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
fab: {
|
|
|
'size-xs': '1.875rem',
|
|
|
'size-sm': '2.5rem',
|
|
|
'size-md': '3.5rem',
|
|
|
'size-lg': '4.5rem',
|
|
|
'icon-xs': '1rem',
|
|
|
'icon-sm': '1.25rem',
|
|
|
'icon-md': '1.5rem',
|
|
|
'icon-lg': '1.75rem',
|
|
|
// NO `offset` / `z` here (retired 2026-08-15). Placement values belong to
|
|
|
// the shared viewport-placement layer, which declares them itself in
|
|
|
// `lib/viewport-placement.css` — it has no recipe key at all, and that is
|
|
|
// what lets it live outside any component's directory. The one value Fab
|
|
|
// differs on (the `sticky` band) rides `--_viewport-placement-z` in its
|
|
|
// bridge. Minting `--fab-offset` / `--fab-z` only created a parallel
|
|
|
// vocabulary whose sole reader was the bridge translating it back.
|
|
|
shadow: 'var(--shadow-overlay)',
|
|
|
lift: '2px'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// OnionMenu — the radial chrome. The geometry comes from the geometry
|
|
|
// engine + the fills from the colour engine; this owns the magic numbers
|
|
|
// that USED to be literals in onion-menu.css. The centre trigger IS a FAB,
|
|
|
// so its lift / shadow REFERENCE the fab tokens — they can't drift apart.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'onion-menu': {
|
|
|
'trigger-lift': 'var(--fab-lift)',
|
|
|
'trigger-shadow': 'var(--fab-shadow)',
|
|
|
'trigger-radius': 'var(--radius-full)',
|
|
|
'trigger-fg': 'var(--color-content-on-solid)',
|
|
|
'sector-shadow': 'var(--shadow-raised)',
|
|
|
// ONE ring colour for the two states: the hover ring is the SAME colour
|
|
|
// as the selection ring, told apart by its opacity (below) — the recipe
|
|
|
// says so and the census counted it twice.
|
|
|
'sector-ring-fg': 'var(--color-content-on-solid)',
|
|
|
'sector-ring-width': '2.5',
|
|
|
'sector-hover-ring-width': '2',
|
|
|
'label-font-family': 'var(--style-label-font-family)',
|
|
|
'label-font-weight': 'var(--style-label-font-weight)',
|
|
|
// A RATIO of its geometry box, not pixels, so the trigger's +/x scales with
|
|
|
// the ring sizing. (The option icon has no twin: the rule that would size
|
|
|
// it is DROPPED — see the CSS.)
|
|
|
'glyph-size': '42%',
|
|
|
'glyph-bar-thickness': '2px',
|
|
|
'glyph-bar-radius': 'var(--radius-full)',
|
|
|
'muted-opacity': '0.85',
|
|
|
'hover-ring-opacity': '0.65',
|
|
|
// ── `luminous` look: the lit rim (a blurred pass under a crisp one) ──
|
|
|
'rim-width': '2',
|
|
|
'rim-glow-width': '9',
|
|
|
'rim-glow-opacity': 'var(--opacity-45)',
|
|
|
'rim-glow-blur': 'var(--blur-sm)'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// ButtonGroup — composes <Group> + <Button>; owns only the segmented seam.
|
|
|
// The divider strength (currentColor mix) is the lone magic number.
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'button-group': {
|
|
|
'divider-strength': '18%'
|
|
|
},
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
// SplitButton — composes <ButtonGroup> + <Button> + <DropdownMenu>. Owns
|
|
|
// only the slim ▾ tail's inline padding (a consumer-overridable knob).
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
'split-button': {
|
|
|
'trigger-padding-inline': 'var(--space-1)'
|
|
|
}
|
|
|
} as const satisfies RecipeTokenSet);
|