diff --git a/src/uix/eidos/README.md b/src/uix/eidos/README.md index f9610610b..272892945 100644 --- a/src/uix/eidos/README.md +++ b/src/uix/eidos/README.md @@ -554,6 +554,7 @@ Resumen rápido de lo que cubre, para no duplicar aquí: | **Theme builder en runtime: `eidos.applyColorScheme(seed)` (motor `uix.color`)** | §26 | | **Salida wide-gamut OKLCH default-on (hex fallback + `oklch()` sibling)** | §27 | | **a11y forced-colors (focus outline fallback) + ramp de bordes (slot 6→7)** | §28 | +| **Canon de escalas — auditoría de theming: blur · inset-shadow/ring · gradientes · breakpoints+container · opacidad · border-width · tracking (2026-06-15)** | §35 | Lo que sigue en este README son las decisiones operativas de la **capa visual como módulo** (typography sourcing, picker patterns, API diff --git a/src/uix/eidos/THEMING.md b/src/uix/eidos/THEMING.md index f6a1bd39a..62bb195de 100644 --- a/src/uix/eidos/THEMING.md +++ b/src/uix/eidos/THEMING.md @@ -63,6 +63,7 @@ 32. [Focus ring — modelo de dos anillos parametrizado](#32-focus-ring--modelo-de-dos-anillos-parametrizado-2026-06-11) 33. [Glifos de stepper themeables (`spin-field`)](#33-glifos-de-stepper-themeables-spin-field--2026-06-11) 34. [`spin-field` — visual compartido del stepper-field](#34-spin-field--visual-compartido-del-stepper-field-number-field--css-field--2026-06-11) +35. [Canon de escalas — auditoría de theming (2026-06-15)](#35-canon-de-escalas--auditoría-de-theming-2026-06-15) --- @@ -511,15 +512,21 @@ Cada size canónico genera tokens coordinados: ``` --size-md-control-height: 36px ---size-md-font-size: 14px ---size-md-font-line-height: 1.5 ---size-md-icon-size: 16px +--size-md-font-size: 14px /* = --font-size-sm (control compacto), NO 16px */ +--size-md-font-line-height: 1.45 +--size-md-icon-size: 18px /* = --icon-size-md */ --size-md-padding-inline: 12px --size-md-padding-block: 8px --size-md-gap: 8px --size-md-radius: 6px ``` +> **Dos `md` distintos** (la confusión que la auditoría señaló): el **bundle de +> control** `--size-md-font-size` = 14px (fuente `sm`, porque un control de 36px lleva +> texto compacto — paridad Radix/Material). La **escala tipográfica** `--font-size-md` +> = 16px (body). Comparten el nombre `md` pero son ejes distintos: control-size vs +> tipo. Ver los arquetipos size→fuente abajo. + ### Regla clave: `md` NO cambia por viewport Lo responsive decide **qué size activo se usa**, NO redefine los @@ -532,7 +539,7 @@ tokens están disponibles y el wrapper elige cuál. @media (max-width: 768px) { - :root { --toggle-size-md-control-height: 32px; } + :root { --toggle-height-md: 32px; } /* NO redefinas el token del size por viewport */ } ``` @@ -550,6 +557,33 @@ recipe soporta. Categorías: La categorización vive en [`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md §12.8`](../../../web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md). +### Arquetipos size→fuente (Fase 7 — 2026-06-15) + +El bundle `--size-*` (arriba) es la **referencia del arquetipo `control`**: empareja +`md`-control con fuente `sm` (14px) e icono `md`. Pero los componentes NO consumen el +bundle directamente — cada recipe declara su `font-size-{size}` apuntando a `--font-size-*` +(la auditoría encontró que esa indirección, hand-rolled, derivó en varias convenciones). +Las **convenciones canónicas** (cada size → token de fuente) son: + +| Arquetipo | sm | md | lg | xl | Componentes | +|---|---|---|---|---|---| +| **control** | sm | sm | md | lg | button, toggle\*, switch | +| **compact** | xs | sm | md | lg | badge, breadcrumb, select, editable, file-upload, stepper, tooltip | +| **dense** | xs | sm | sm | md | field, form, calendar, pagination, spin/date/time/color-field, radio-group | + +(\* toggle/avatar son una variante extra-compacta — un escalón más abajo.) Todas +comparten `md→sm` (14px, decisión 2 = paridad Radix/Material). + +**Regla dura (guard de coherencia)** — `recipe-css-contract.test.ts`: +> Ningún token `font-size-*` / `icon-size-*` de recipe puede ser un **literal px/rem** +> — DEBE referenciar `--font-size-*` / `--icon-size-*` (si no, el texto deja de seguir +> la escala tipográfica, `--scaling` y `applyTypeScale()`). + +Cierra el hueco que el guard anterior (solo-CSS) dejaba — los valores de recipe-token +se vuelven custom properties, no la propiedad `font-size:`. (`words`/`palabras`/`chronos` +excluidos — track activo.) El refactor a *consumir* el bundle del arquetipo (en vez de +re-declarar el mapeo) queda como follow-up; el guard es lo que **impide la deriva**. + ### Escala tipográfica ↔ escala de iconos Son dos escalas paralelas — `--font-size-{name}` (`lib/primitives/typography.ts`) @@ -613,9 +647,18 @@ Donde `{prefix}` es uno de: | `--size-{key}-{slot}` | Size primitive | `--size-md-control-height` | | `--space-{n}` | Spacing scale | `--space-3` | | `--radius-{key}` | Radius scale | `--radius-md` | -| `--shadow-{n}` | Shadow scale | `--shadow-3` | +| `--border-width-{key}` | Border-width scale (lineal `none·thin·medium·thick·heavy` = 0/1/2/3/4) — §35 | `--border-width-thick` | +| `--ring-inset-width` | Default ancho del inset-ring (anillo interior `box-shadow`) — §35 | `--ring-inset-width` | +| `--shadow-{n}` | Shadow scale (gota) | `--shadow-3` | +| `--shadow-inset-{key}` | Inner-shadow / recessed (`subtle·deep`, mode-aware) — §35 | `--shadow-inset-subtle` | +| `--blur-{key}` | Blur scale (backdrop/frost, `none·sm·md·lg·xl·xxl`) — §35 | `--blur-lg` | +| `--gradient-{name}` | Gradiente nombrado (themeable, role-composed) — §35 | `--gradient-shimmer` | +| `--gradient-angle-{dir}` | Dirección de gradiente (8 brújulas) — §35 | `--gradient-angle-to-r` | +| `--breakpoint-{key}` | Breakpoint responsive (fuente = ActiveDom) — §35 | `--breakpoint-md` | | `--z-index-{key}` | Z-index layer | `--z-index-modal` | -| `--opacity-{key}` | Opacity | `--opacity-disabled` | +| `--opacity-{key}` | Opacidad — escala dual numérica (`0..100`) + semántica (`disabled·muted·…`) — §35 | `--opacity-disabled` | +| `--tracking-{key}` | Letter-spacing scale (incl. `caps` para MAYÚSCULAS) — §35 | `--tracking-caps` | +| `--leading-{key}` | Line-height scale | `--leading-ui` | | `--duration-{key}` | Motion duration | `--duration-fast` | | `--ease-{key}` | Motion ease | `--ease-out` | | `--style-{name}-*` | Typography named style | `--style-h1-font-size` | @@ -1750,6 +1793,127 @@ sigue opaco salvo que pida `data-frost`). El builder runtime `ActiveEidos.applyD en vivo — hermano de `applyColorScheme` / `applyTypeScale`. Demo: `/temas/profundidad` §Materiales. +**Tier de sombra interior** (`--shadow-inset-*`, 2026-06-15): la escala de sombra +gana un tier **inset** mode-aware, distinto de las sombras de gota (exteriores) y +de los *inset-rings* (anillo nítido `inset 0 0 0 Npx`, otro eje): + +| Token | Light | Dark | +|---|---|---| +| `--shadow-inset-subtle` | `inset 0 1px 2px rgb(15 23 42 / 0.08)` | `inset 0 1px 2px rgb(0 0 0 / 0.30)` | +| `--shadow-inset-deep` | `inset 0 2px 4px rgb(15 23 42 / 0.12)` | `inset 0 2px 4px rgb(0 0 0 / 0.45)` | + +El plano `recessed` lo consume (`--depth-recessed-shadow: var(--shadow-inset-subtle)`), +sustituyendo el `color-mix(neutral-contrast …)` inline previo — que en dark daba un +borde claro (embossado) en vez de un hundido; ahora es mode-correcto (inset oscuro en +ambos modos). Referencias: Tailwind `inset-shadow-{2xs,xs,sm}`, Bootstrap `shadow-inset`, +Chakra `inner`. Disponible además para estados *pressed* / wells. + +**Inset-ring** (`--ring-inset-width`, 2026-06-15): eje hermano pero **distinto** — +un anillo interior **nítido** (no difuminado), como el `inset-ring` de Tailwind +(`inset 0 0 0 Npx `). El ancho sale de la escala `--border-width-*` +(`--ring-inset-width` por defecto `thick`=2px, retunable por tema / override por uso); +el color va por el hook `--ring-inset-color`. **No** se puede hacer un token único +`--ring-inset` pre-resuelto: CSS hornea los `var()` anidados en el scope donde se +declara (`:root`), así que el color/ancho por-elemento no propagaría — la expresión +vive en el punto de uso: `box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor)`. +Consumidores: date-field (focus de segmento), drag-drop (accepting 1px / dragover 2px), +float-panel (focus + grabbed + resize-grip), select (item checked+highlighted). Las +marcas laterales de un solo lado (range-calendar `inset ±2px 0 0 0`) **no** son anillos +→ se quedan. De paso, este eje da el primer uso real a los pasos `thin`/`thick` de +`--border-width-*`. + +**Escala de blur canónica** (`--blur-*`, 2026-06-15): el desenfoque es un +**primitivo** (`STATIC_BLUR` → `lib/primitives/static.ts`), no un px disperso. +Valores alineados a Tailwind, escalados por `--scaling` como `--icon-size-*`: + +| Token | px | = Tailwind | +|---|---|---| +| `--blur-none` | 0 | — | +| `--blur-sm` | 4 | xs | +| `--blur-md` | 8 | sm | +| `--blur-lg` | 12 | md | +| `--blur-xl` | 16 | lg | +| `--blur-xxl` | 24 | xl | + +Dos modelos de referencia: **Tailwind** (escala numérica cruda) y **Apple** (materiales +semánticos `ultraThin…thick` que acoplan blur+translucidez). Eidos toma el de Tailwind como +**eje crudo** y lo compone en la capa semántica de **profundidad**: los planos consumen +`--blur-*` (`--depth-overlay-blur: var(--blur-lg)`, `--depth-modal-blur: var(--blur-xl)`), +igual que color separa `--scale-*` (crudo) de los roles. Un futuro tema "cristal" acopla +blur+alpha por plano (el modelo Apple) sobre esta escala. Consumidores ya migrados: +planos overlay/modal, `tooltip` (frost), `dialog`/`drawer` (`overlay-blur`). Nada +inventa px de blur a mano. + +**Gradientes themeables** (`--gradient-angle-*` + `gradients`, 2026-06-15): eje de +dos capas, espejo de Tailwind (que declara **0 gradientes nombrados** — solo +maquinaria): +- **Direcciones** (`--gradient-angle-*`): las 8 brújulas de Tailwind como ángulos CSS + (`to-t 0deg · to-tr 45 · to-r 90 · to-br 135 · to-b 180 · to-bl 225 · to-l 270 · to-tl 315`). +- **Nombrados** (`gradients` config → `--gradient-*`): **extensibles** (jaula abierta: + `extendEidosConfig({ primitives: { gradients: {…} } })`), **role/surface-composed** + → mode-aware vía los tokens que referencian. Default fuerte mínimo: **un solo** + nombrado, `--gradient-shimmer` (barrido de carga; lo consume `image`). Un tema añade + sus gradientes de marca aquí. + +Los gradientes **funcionales** (color-picker HSV/checkerboard, `conic` de progress/meter, +líneas 1px de cropper/tree-grid, máscara de scroll de tabs, split bicolor de +range-calendar, grip de float-panel, barra de carga de command) **no** son de tema y +siguen crudos — no son decorativos. `skeleton` tinta su shimmer por variante de color +(data-driven), así que conserva su gradiente local pero dogfoodea `var(--gradient-angle-to-r)`. + +**Breakpoints — fuente única + container queries** (`EidosConfig.breakpoints`, +`--breakpoint-*`, 2026-06-15): la **fuente de verdad de los breakpoints es el servicio +runtime** `ActiveDom` (el dev los setea vía `createActiveUix({ dom: { breakpoints } })`; +`BREAKPOINTS_DEFAULT` es solo el seed). `ActiveEidos` threadea `dom.breakpoints.current` +a `renderStaticCss`, que los emite como tokens `--breakpoint-{sm..xxl}` **y** los usa en +los `@media` de tipografía responsive — así el CSS generado deja de congelarse en un const +duplicado y sigue los breakpoints configurados. **Container queries**: una recipe declara +overrides por breakpoint en la key reservada `container` (hermana de `composition`): + +```ts +recipes: { card: { container: { md: { 'pad': 'var(--space-6)' } } } } +// → @container (min-width: 768px) { [data-card] { --card-pad: var(--space-6) } } +``` + +El generador (`emitContainerQueries`) usa los **mismos** breakpoints configurados (px +literal — CSS prohíbe `var()` en condiciones `@container`/`@media`, así que la sincronía +solo es posible generándolo). Opt-in: un ancestro con `data-container` activa +`container-type: inline-size`. Eje themeable, 0 consumidores hoy (jaula abierta). + +**Opacidad — escala coordinada de dos capas** (`--opacity-*`, 2026-06-15): mismo +patrón dual que la sombra (numérico + semántico). +- **Numérico** (`--opacity-{0,5,…,100}`, Tailwind step-5): granularidad fina para + interfaces etéreas / cristal (capas translúcidas en el tramo bajo). +- **Semántico** (los roles que consumen los recipes): `ghost 0.3 · disabled 0.4 · + scrim 0.45 · muted 0.65 · overlay 0.65 · subtle 0.8 · press 0.85 · hover 0.9 · full 1`. + `disabled = 0.4` (estándar moderno ≈ Material 38%). + +**Unificación**: el estado `disabled` se renderizaba con ~10 valores distintos +(0.45–0.72) en recipes (`disabled-opacity`) + CSS (`[data-disabled]`/`:disabled`). +Ahora TODOS consumen `var(--opacity-disabled)`. La deriva ad-hoc de CSS +(muted/ghost/subtle) migrada a sus roles. Quedan crudos solo los de animación +(`spinner` keyframe) y `scroll-frames` (rol no semántico). Retunable por tema, como +size/sombra/superficie (decisión del usuario). + +**Border-width — escala lineal** (`--border-width-*`, 2026-06-15): adoptada la +**lineal de Bootstrap** (`none 0 · thin 1 · medium 2 · thick 3 · heavy 4`) — la única +escala de referencia que tiene el **3px** que los componentes usan (Tailwind salta +1/2/4/8). Podados los pasos muertos `hairline`(0.5) y el viejo `medium`(1.5) (0 +consumidores); `medium` retuneado a 2, `thick` a 3, `heavy`(4) nuevo. Los 3 +consumidores de `thick`(2px) — focus-ring de select, quote-border, separator — + +el default de `--ring-inset-width` movidos a `medium`(2px, sin cambio visual). **Todos +los anchos crudos tokenizados** (consumo completo de la escala — la tesis de la +auditoría): `3px→thick`, `2px→medium`, `1px→var(--border-width)`, `1.5px→medium` +(chevron de navigation-menu) en ~35 ficheros. Así un tema retunea el ancho de borde +de una vez (p. ej. `--border-width` denso) y todos los bordes lo siguen. + +**Tracking — `caps` para mayúsculas** (`--tracking-*`, 2026-06-15): añadidos +`caps 0.04em` (micro-tracking canónico de etiquetas en MAYÚSCULAS — el patrón +dominante en menús/headings) y `widest 0.1em`. Los 11 `letter-spacing` crudos de +CSS migrados a sus roles (`0.04→caps · 0.05→wider · 0.02→wide · 0.1→widest`). El +`letterSpacing` óptico por-tamaño de la escala tipográfica (xxs/xs…) NO se migra: +es la corrección óptica intrínseca de cada paso. + **Pendiente** (menor): el cue `scrim` está disponible como token (`--depth-{plane}-scrim`) pero sin regla cableada — el backdrop dim de los modales lo gestiona hoy cada componente. @@ -1927,6 +2091,80 @@ refactor aparte. --- -**Última revisión**: 2026-06-11. Si algo en este doc no coincide con +## 35. Canon de escalas — auditoría de theming (2026-06-15) + +Sprint de auditoría que llevó las escalas del theming a paridad con las referencias +(Tailwind · Material 3 · Apple HIG · Bootstrap · Radix) y, sobre todo, **forzó su +consumo**: la tesis de la auditoría es que *una escala canónica que los componentes +no consumen (la bypassan con literales) deriva en N variantes del mismo valor*. Cada +eje es ahora **retunable por tema** (igual que size/sombra/superficie) y los recipes +**consumen el token, nunca un literal**. Detalle por-eje en el addendum de §29; tokens +en la tabla de §6. + +| Eje | Token(s) | Canon | Decisión clave | +|---|---|---|---| +| **Blur** | `--blur-{none,sm,md,lg,xl,xxl}` | 0/4/8/12/16/24 (Tailwind) | numérico crudo; los planos de depth lo consumen (`--depth-*-blur`) | +| **Inner-shadow** | `--shadow-inset-{subtle,deep}` | mode-aware (light slate / dark negro) | lo usa el plano `recessed`; ≠ inset-ring | +| **Inset-ring** | `--ring-inset-width` + `--ring-inset-color` | `inset 0 0 0 var(width) var(color)` **en el punto de uso** | un token único pre-resuelto es imposible (CSS hornea el `var()` anidado en `:root`) | +| **Gradientes** | `--gradient-angle-*` + `gradients`→`--gradient-*` | 8 direcciones + nombrados role-composed | Tailwind ship 0 nombrados → solo `shimmer`; los funcionales (HSV, conic, líneas) NO son de tema | +| **Breakpoints** | `--breakpoint-{sm..xxl}`, `EidosConfig.breakpoints` | **fuente = `ActiveDom`** (runtime, dev-settable) | `ActiveEidos` threadea `dom.breakpoints` al generador; los `@media` dejan de congelarse | +| **Container queries** | recipe key `container` → `@container` + `[data-container]` | px literal **generado** (CSS prohíbe `var()` en `@container`) | jaula abierta, 0 consumidores hoy | +| **Opacidad** | `--opacity-{0..100}` + semánticos | dual numérico + semántico; `disabled 0.4` (≈ Material 38%) | unificado (~10 valores de disabled → 1); numérico fino = glass-friendly | +| **Border-width** | `--border-width-{none,thin,medium,thick,heavy}` | **lineal Bootstrap** 0/1/2/3/4 (única ref con el 3px real) | **todos** los anchos crudos tokenizados (~35 ficheros) | +| **Tracking** | `--tracking-{…,caps,widest}` | + `caps 0.04em` (MAYÚSCULAS) + `widest 0.1em` | 11 `letter-spacing` crudos migrados; el óptico por-tamaño NO | + +**Incidente registrado**: una reescritura masiva por PowerShell (`WriteAllText`) +corrompió 11 ficheros (`o→p`); recuperados con `git checkout` + rehechos con la +herramienta Edit. Regla: **modificar ficheros del repo SOLO con Edit/Write**, nunca +PowerShell en bloque. + +**Fase 7 (size→fuente — parcial)**: documentados los **3 arquetipos** canónicos +(`control · compact · dense`, §5) + `--size-*` como referencia del `control`. **Guard +de coherencia** activo: ningún `font-size-*`/`icon-size-*` de recipe puede ser literal +px/rem (cierra el hueco del guard solo-CSS). Arreglados los últimos hardcodes +(`toggle`, `avatar` → `--font-size-*`, valores preservados). El `icon-size` de +`password-field` desde `--control-height-*` es **correcto** (es el tamaño del botón +reveal, no del glifo) — falso positivo de la auditoría. **Deferido**: el refactor a +*consumir* el bundle del arquetipo (en vez de re-declarar el mapeo) — grande, con +edge-cases (fuentes semánticas por-parte, sistema `--text-N` de accordion) + edición +en paralelo; el guard es lo que impide la deriva mientras tanto. + +### Bloque C — números mágicos sueltos (z-index · duración · border/ring) + +Cierre de los literales que bypasseaban una escala ya existente. **Regla**: un +literal que iguala un paso de escala DEBE consumir el token; nada de "intencionales". + +- **z-index de overlays flotantes** — `combobox` (era `80`), `navigation-menu` + (era `50`) y `drag-drop` preview (era `99`) hardcodeaban su z. Ahora declaran un + token de recipe (`content-z` / `preview-z`) como ya hacían sus 5 hermanos + (`popover`/`select`/`tooltip`/`link-preview`/`dropdown-menu`). Estos NO usan la + escala global `--z-index-*` a propósito: son una **micro-banda** baja (75-80) que + la capa flotante de soma **espeja** leyendo el z computado del wrapper; subirlos a + 300/400 rompería el espejo. El fallback redundante `, 80` de `dropdown-menu` se + eliminó (el valor vive una vez, en el recipe). Los `z-index: 0..5` de apilado + local (avatar, tabs, sticky cells) son ordenación relativa, NO mágicos — se quedan. +- **Duración** — los que igualaban un paso de la escala la consumen: `dialog` enter + `120ms`→`var(--duration-fast)`, exit `280ms`→`var(--duration-slow)` (asimetría + rápida-entra/lenta-sale preservada, ya 100% en escala); `card` emerge + `320ms`→`var(--duration-slow)`; banner/code-block/link `120ms`→`fast`. Los + fallbacks muertos `, 220ms`/`, 720ms` (checkbox/button, cuyo recipe ya declaraba + el token) se quitaron. **Excepción razonada**: `press-duration 80ms`, + `spinner-duration 720ms`, `loading-indicator-duration 900ms` se quedan como token + de recipe — son **periodos de animación continua** (giro / shimmer) o un press + deliberadamente sub-`fast`, NO transiciones de interacción; la escala de 5 pasos + (`instant..slow`) es para interacciones, no tiene sitio para ellos. +- **Border / ring width** — los anchos **únicos** de borde/ring que igualaban un + paso (`2px`=`medium`, `3px`=`thick`, `1px`=`thin`) se tokenizaron a + `var(--border-width-*)` (avatar border + badge + carve, drawer drag-ring, slider + thumb, grid-list focus-ring, toast accent-stripe → `thick`, table/menu cell/content + border → `var(--border-width)`). Valor-preservante, cero cambio visual. Lo que se + **queda como escala dimensional propia** (NO es el concepto border-width + re-derivado): el ring del avatar `sm/md/lg = 1.5/2/3px` (el `1.5` quedó fuera de la + escala global al podar el `0.5/1.5`), `ring-thickness 3..10px`, `track-width`, + `content-width`, offsets — escalas locales coherentes, no literales sueltos. + +--- + +**Última revisión**: 2026-06-16. Si algo en este doc no coincide con el código, el código gana — pero abre un issue para que actualicemos el doc. diff --git a/src/uix/eidos/THEMING_NOTES.md b/src/uix/eidos/THEMING_NOTES.md index da2dec8e6..4743b776b 100644 --- a/src/uix/eidos/THEMING_NOTES.md +++ b/src/uix/eidos/THEMING_NOTES.md @@ -37,13 +37,20 @@ source: extracted from src/uix/eidos/THEMING.md (was §15 + §17) | **Cross-axis collision** | ✅ explícito | ❌ | ⚠️ partial | ❌ | N/A | N/A | | **Composite scopes** | ✅ `[axis:v, …]` | ❌ | ✅ conditional pairs | ❌ | N/A | N/A | | **9 color roles canónicos** | ✅ libro | ⚠️ 6 accents | ❌ open | ❌ open | ⚠️ 4 roles | ❌ open | -| **6 size canon coordinated** | ✅ | ⚠️ 1-3 | ⚠️ 5 | ⚠️ 5 | ❌ | N/A | +| **6 size canon coordinated** | ⚠️ canon + guard¹ | ⚠️ 1-3 | ⚠️ 5 | ⚠️ 5 | ❌ | N/A | | **Density runtime** | ✅ 3 levels | ❌ | ❌ | ⚠️ partial | ❌ | ❌ | | **Contract introspection** | ✅ typed | ⚠️ docs | ✅ Panda | ⚠️ docs | ❌ | ❌ | | **Runtime override** | ✅ contract-aware | ⚠️ via CSS vars | ❌ | ✅ CSSVarsProvider | ⚠️ via CSS | ❌ | | **Persistence versioned** | ✅ envelope | ❌ | ❌ | ❌ | ❌ | N/A | | **Perceptual layer (sema)** | ✅ unique | ❌ | ❌ | ❌ | ❌ | ❌ | +¹ El bundle `--size-*` existe + emite, pero los componentes NO lo consumen +directamente (re-declaran su mapeo size→fuente). La auditoría 2026-06-15 formalizó +los **3 arquetipos** (`control · compact · dense`, THEMING.md §5) y añadió un **guard +de coherencia** (sin literales px/rem en `font-size-*`/`icon-size-*` de recipe). El +refactor a *consumir* el bundle queda deferido; el guard impide la deriva. Por eso ⚠️ +(canon + enforcement) y no ✅ (consumo pleno). + ### Mental model | Sistema | Token philosophy | diff --git a/src/uix/eidos/TSC.md b/src/uix/eidos/TSC.md index 7bd247359..6c9c0a079 100644 --- a/src/uix/eidos/TSC.md +++ b/src/uix/eidos/TSC.md @@ -269,6 +269,25 @@ Reglas: sus items). Pattern reutilizable para futuros wrappers compositivos (button-group, nav-menu). +### Container queries — `container: { ... }` (2026-06-15) + +Segunda key reservada sibling de los tokens (como `composition`): +overrides de token por breakpoint, emitidos dentro de `@container`: + +```ts +recipes: { card: { container: { md: { 'pad': 'var(--space-6)' } } } } +// → @container (min-width: 768px) { [data-card] { --card-pad: var(--space-6) } } +``` + +- Usa los **mismos breakpoints configurados** que `@media` (de + `EidosConfig.breakpoints`, threaded por `ActiveEidos` desde `ActiveDom`) + — px **literal**, porque CSS prohíbe `var()` en condiciones `@container`. +- Opt-in del contenedor: un ancestro con `[data-container]` activa + `container-type: inline-size`. +- `stripCompositionKey` (render-css) salta `composition` **y** `container`; + `contract.ts` los excluye del contrato público de tokens. +- Eje themeable, 0 consumidores hoy (jaula abierta). Detalle: THEMING.md §35. + ### Pipeline de defensas (5 capas) ``` diff --git a/src/uix/eidos/active-eidos-config.test.ts b/src/uix/eidos/active-eidos-config.test.ts index 0c84db471..4a5d7b902 100644 --- a/src/uix/eidos/active-eidos-config.test.ts +++ b/src/uix/eidos/active-eidos-config.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { renderThemeCss } from './lib/render-css' +import { renderStaticCss, renderThemeCss } from './lib/render-css' import { COLOR_ALPHA_STEPS, COLOR_SCALE_STEPS, @@ -521,8 +521,12 @@ describe('ActiveEidos config', () => { it('emits the Fase 4 frost atmosphere (opt-in [data-frost] + blur tokens)', () => { const css = createThemeBaseEidos().renderStaticCss(); - expect(css).toContain('--depth-overlay-blur: 10px;'); - expect(css).toContain('--depth-modal-blur: 16px;'); + // Blur is a canonical primitive (`--blur-*`); the planes consume it instead + // of hardcoding px. See THEMING.md "Escala de blur". + expect(css).toContain('--blur-lg: calc(12px * var(--scaling));'); + expect(css).toContain('--blur-xl: calc(16px * var(--scaling));'); + expect(css).toContain('--depth-overlay-blur: var(--blur-lg);'); + expect(css).toContain('--depth-modal-blur: var(--blur-xl);'); // frost is opt-in (gated on data-frost) so it never turns an opaque overlay translucent expect(css).toContain("[data-depth='overlay'][data-frost] {"); expect(css).toContain('backdrop-filter: blur(var(--depth-overlay-blur));'); @@ -541,6 +545,46 @@ describe('ActiveEidos config', () => { eidos.clearDepth(); // reverts cleanly, no throw }); + it('emits --breakpoint-* tokens; configured breakpoints override the defaults', () => { + // Default (no breakpoints in config) → canonical fallback. + const dflt = renderStaticCss(THEME_BASE_OPTIONS); + expect(dflt).toContain('--breakpoint-md: 768px;'); + expect(dflt).toContain('--breakpoint-xxl: 1536px;'); + + // Developer-configured breakpoints (as ActiveEidos threads from ActiveDom) + // flow into the emitted `--breakpoint-*` tokens (and the responsive `@media` + // blocks, which the base theme's fluid type scale doesn't currently emit). + const custom = renderStaticCss({ + ...THEME_BASE_OPTIONS, + breakpoints: { base: 0, sm: 480, md: 900, lg: 1024, xl: 1280, xxl: 1536 } + }); + expect(custom).toContain('--breakpoint-md: 900px;'); + expect(custom).not.toContain('--breakpoint-md: 768px;'); + }); + + it('emits @container blocks from recipe `container` overrides + the data-container opt-in', () => { + // Opt-in rule is always emitted. + expect(renderStaticCss(THEME_BASE_OPTIONS)).toContain('[data-container] {'); + + // A recipe's `container` overrides emit `@container` blocks scoped to the + // component, using the configured breakpoints (literal px — var() is illegal + // in @container conditions). + const cfg = { + ...THEME_BASE_OPTIONS, + recipes: { widget: { container: { md: { gap: 'var(--space-6)' } } } } + }; + const css = renderStaticCss(cfg); + expect(css).toContain('@container (min-width: 768px)'); + expect(css).toContain('--widget-gap: var(--space-6)'); + + // Configured breakpoints flow into the @container condition too. + const custom = renderStaticCss({ + ...cfg, + breakpoints: { base: 0, sm: 480, md: 900, lg: 1024, xl: 1280, xxl: 1536 } + }); + expect(custom).toContain('@container (min-width: 900px)'); + }); + it('applyTheme composes all five builders from one seed (the runtime capstone)', () => { const eidos = createThemeBaseEidos(); const result = eidos.applyTheme({ @@ -720,7 +764,7 @@ describe('ActiveEidos config', () => { delete (cfg.primitives.border!.width as Record)['thin'] ;(cfg.primitives.border as { defaultWidth: string }).defaultWidth = 'missing-width' - ;(cfg.primitives.opacity as Record)['ghost'] = '0.4' + ;(cfg.primitives.opacity as Record)['bogus'] = '0.4' delete (cfg.primitives.zIndex as Record)['toast'] delete (cfg.themes!['base-light'].shadow as Record)['6'] @@ -731,7 +775,7 @@ describe('ActiveEidos config', () => { expect(report.issues.some((issue) => issue.path === 'primitives.border.defaultWidth')).toBe( true ) - expect(report.issues.some((issue) => issue.path === 'primitives.opacity.ghost')).toBe(true) + expect(report.issues.some((issue) => issue.path === 'primitives.opacity.bogus')).toBe(true) expect(report.issues.some((issue) => issue.path === 'primitives.zIndex.toast')).toBe(true) expect(report.issues.some((issue) => issue.path === 'themes.base-light.shadow.6')).toBe(true) }) @@ -1019,7 +1063,7 @@ describe('ActiveEidos config', () => { expect(css).toContain('--scaling-110: 1.1;') expect(css).toContain('--scaling: var(--scaling-100);') expect(css).toContain("[data-scaling='110'] {") - expect(css).toContain('--opacity-disabled: 0.48;') + expect(css).toContain('--opacity-disabled: 0.4;') expect(css).toContain('--z-index-modal: 700;') expect(css).toContain('--size-md-control-height: var(--control-height-md);') expect(css).toContain('--size-md-font-size: var(--font-size-sm);') diff --git a/src/uix/eidos/active-eidos.svelte.ts b/src/uix/eidos/active-eidos.svelte.ts index 00e7c0c7f..7a7f54351 100644 --- a/src/uix/eidos/active-eidos.svelte.ts +++ b/src/uix/eidos/active-eidos.svelte.ts @@ -448,7 +448,11 @@ export class ActiveEidos { renderStaticCss(): string { this.assertValid(); - return renderEidosStaticCss(this.#config); + // Thread the runtime-configured breakpoints (developer-set on ActiveDom) into + // the generator so the emitted `@media` blocks + `--breakpoint-*` tokens match + // the live `ResponsiveProp` resolution. Falls back to defaults when no DOM. + const breakpoints = this.#dom?.breakpoints.current; + return renderEidosStaticCss(breakpoints ? { ...this.#config, breakpoints } : this.#config); } /** diff --git a/src/uix/eidos/components/announce/announce.css b/src/uix/eidos/components/announce/announce.css index 7492c5967..c049e7792 100644 --- a/src/uix/eidos/components/announce/announce.css +++ b/src/uix/eidos/components/announce/announce.css @@ -26,7 +26,7 @@ display: block; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); background: var(--color-surface-default); font-size: var(--font-size-sm); line-height: var(--leading-ui); diff --git a/src/uix/eidos/components/badge/badge.css b/src/uix/eidos/components/badge/badge.css index d60fe9ce9..a5e4c51ee 100644 --- a/src/uix/eidos/components/badge/badge.css +++ b/src/uix/eidos/components/badge/badge.css @@ -167,7 +167,7 @@ background: transparent; color: inherit; cursor: pointer; - opacity: 0.75; + opacity: var(--opacity-subtle); transition: opacity var(--duration-fast) var(--ease-default), background var(--duration-fast) var(--ease-default); diff --git a/src/uix/eidos/components/banner/banner.css b/src/uix/eidos/components/banner/banner.css index 6b3d34faa..a6123fe7e 100644 --- a/src/uix/eidos/components/banner/banner.css +++ b/src/uix/eidos/components/banner/banner.css @@ -129,7 +129,7 @@ [data-banner][data-variant='outline'] { --_banner-bg: transparent; --_banner-fg: var(--_banner-text); - --_banner-border-width: 1px; + --_banner-border-width: var(--border-width); --_banner-border-color: var(--_banner-border); } @@ -154,8 +154,8 @@ border: 0; border-radius: var(--radius-sm, var(--space-1)); cursor: pointer; - opacity: 0.8; - transition: opacity 120ms ease, background-color 120ms ease; + opacity: var(--opacity-subtle); + transition: opacity var(--duration-fast) ease, background-color var(--duration-fast) ease; } [data-banner-close]:hover { @@ -164,7 +164,7 @@ } [data-banner-close]:focus-visible { - outline: 2px solid currentColor; + outline: var(--border-width-medium) solid currentColor; outline-offset: 1px; opacity: 1; } diff --git a/src/uix/eidos/components/button/button.css b/src/uix/eidos/components/button/button.css index 616612ced..a48a97b5d 100644 --- a/src/uix/eidos/components/button/button.css +++ b/src/uix/eidos/components/button/button.css @@ -85,7 +85,7 @@ } [data-button]:focus-visible { - outline: 2px solid var(--button-palette-border); + outline: var(--border-width-medium) solid var(--button-palette-border); outline-offset: 2px; } @@ -294,7 +294,7 @@ .eidos-button-spinner-ring { inline-size: 100%; block-size: 100%; - animation: button-spinner-spin var(--button-spinner-duration, 720ms) linear infinite; + animation: button-spinner-spin var(--button-spinner-duration) linear infinite; } @keyframes button-spinner-spin { diff --git a/src/uix/eidos/components/carousel/carousel.css b/src/uix/eidos/components/carousel/carousel.css index fda23ae14..1998d654b 100644 --- a/src/uix/eidos/components/carousel/carousel.css +++ b/src/uix/eidos/components/carousel/carousel.css @@ -105,21 +105,21 @@ /* ── Variant — surface (default) ─ */ [data-carousel][data-variant='surface'] { padding: var(--space-2); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--_carousel-radius); background: var(--color-surface-default); } [data-carousel][data-variant='outline'] { padding: var(--space-2); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--_carousel-radius); background: transparent; } [data-carousel][data-variant='ghost'] { padding: 0; - border: 1px solid transparent; + border: var(--border-width) solid transparent; background: transparent; } @@ -173,7 +173,7 @@ } [data-carousel][data-disabled] { - opacity: 0.55; + opacity: var(--opacity-disabled); pointer-events: none; } @@ -257,7 +257,7 @@ [data-carousel-prev-trigger][data-disabled], [data-carousel-next-trigger][data-disabled] { - opacity: 0.4; + opacity: var(--opacity-disabled); cursor: not-allowed; } @@ -350,6 +350,6 @@ } [data-carousel-indicator][data-disabled] { - opacity: 0.4; + opacity: var(--opacity-disabled); cursor: not-allowed; } diff --git a/src/uix/eidos/components/checkbox/checkbox.css b/src/uix/eidos/components/checkbox/checkbox.css index 1955ea4d8..750f8a27f 100644 --- a/src/uix/eidos/components/checkbox/checkbox.css +++ b/src/uix/eidos/components/checkbox/checkbox.css @@ -176,7 +176,7 @@ [data-checkbox-indicator] svg[data-svg='dash'] path { stroke-dasharray: 100; stroke-dashoffset: 100; - transition: stroke-dashoffset var(--checkbox-stroke-duration, 220ms) var(--checkbox-stroke-ease, cubic-bezier(0.4, 0, 0.2, 1)); + transition: stroke-dashoffset var(--checkbox-stroke-duration) var(--checkbox-stroke-ease); } [data-checkbox][data-state='checked'] [data-checkbox-indicator] svg[data-svg='check'] path, diff --git a/src/uix/eidos/components/code-block/code-block.css b/src/uix/eidos/components/code-block/code-block.css index ae5125e71..f48d41398 100644 --- a/src/uix/eidos/components/code-block/code-block.css +++ b/src/uix/eidos/components/code-block/code-block.css @@ -18,12 +18,12 @@ [data-code-block][data-variant='surface'] { background: var(--color-neutral-element); - border: 1px solid var(--color-neutral-border); + border: var(--border-width) solid var(--color-neutral-border); } [data-code-block][data-variant='outline'] { background: transparent; - border: 1px solid var(--color-neutral-border); + border: var(--border-width) solid var(--color-neutral-border); } [data-code-block][data-variant='ghost'] { @@ -37,7 +37,7 @@ gap: var(--space-2); padding-inline: var(--space-3); padding-block: var(--space-1); - border-bottom: 1px solid var(--color-neutral-border); + border-bottom: var(--border-width) solid var(--color-neutral-border); background: var(--color-neutral-track); } @@ -62,7 +62,7 @@ font-size: var(--font-size-xxs); color: var(--color-content-muted); text-transform: lowercase; - letter-spacing: 0.04em; + letter-spacing: var(--tracking-caps); white-space: nowrap; } @@ -80,16 +80,16 @@ font-size: var(--font-size-xs); color: var(--color-content-secondary); background: transparent; - border: 1px solid var(--color-neutral-border); + border: var(--border-width) solid var(--color-neutral-border); border-radius: var(--radius-sm); padding-inline: var(--space-2); padding-block: 2px; cursor: pointer; white-space: nowrap; transition: - background 120ms ease, - border-color 120ms ease, - color 120ms ease; + background var(--duration-fast) ease, + border-color var(--duration-fast) ease, + color var(--duration-fast) ease; } [data-code-block-copy]:hover { @@ -98,7 +98,7 @@ } [data-code-block-copy]:focus-visible { - outline: 2px solid var(--color-primary-solid); + outline: var(--border-width-medium) solid var(--color-primary-solid); outline-offset: 2px; } diff --git a/src/uix/eidos/components/code/code.css b/src/uix/eidos/components/code/code.css index b7ad4e080..ca34f11ae 100644 --- a/src/uix/eidos/components/code/code.css +++ b/src/uix/eidos/components/code/code.css @@ -32,7 +32,7 @@ } [data-code][data-variant='outline'] { - border: 1px solid var(--color-neutral-border); + border: var(--border-width) solid var(--color-neutral-border); padding-inline: 0.32em; padding-block: 0.1em; } diff --git a/src/uix/eidos/components/color-picker/color-picker.css b/src/uix/eidos/components/color-picker/color-picker.css index 672da8807..e31021de4 100644 --- a/src/uix/eidos/components/color-picker/color-picker.css +++ b/src/uix/eidos/components/color-picker/color-picker.css @@ -178,7 +178,7 @@ inline-size: var(--_color-picker-trigger-swatch-size); block-size: var(--_color-picker-trigger-swatch-size); border-radius: var(--color-picker-trigger-swatch-radius); - border: 1px solid var(--color-picker-trigger-swatch-border); + border: var(--border-width) solid var(--color-picker-trigger-swatch-border); background-image: var(--cp-checker); background-size: var(--cp-checker-cell) var(--cp-checker-cell); background-position: @@ -215,7 +215,7 @@ inline-size: var(--_color-picker-swatch-size); block-size: var(--_color-picker-swatch-size); border-radius: var(--color-picker-swatch-radius); - border: 1px solid var(--color-picker-swatch-border); + border: var(--border-width) solid var(--color-picker-swatch-border); background-image: var(--cp-checker); background-size: var(--cp-checker-cell) var(--cp-checker-cell); background-position: @@ -451,7 +451,7 @@ /* Reinforce thumb visibility against any colour: bright ring + dark shadow. We tweak the slider thumb only within the colour-picker scope. */ [data-color-picker-channel-slider] [data-slider-thumb] { - border: 2px solid var(--color-surface-default); + border: var(--border-width-medium) solid var(--color-surface-default); box-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); @@ -508,7 +508,7 @@ inline-size: auto; block-size: auto; padding: 0; - border: 1px solid var(--color-picker-swatch-border); + border: var(--border-width) solid var(--color-picker-swatch-border); border-radius: var(--color-picker-swatch-radius); background-image: var(--cp-checker); background-size: var(--cp-checker-cell) var(--cp-checker-cell); diff --git a/src/uix/eidos/components/combobox/combobox.css b/src/uix/eidos/components/combobox/combobox.css index 6a849b8a9..1eac017b3 100644 --- a/src/uix/eidos/components/combobox/combobox.css +++ b/src/uix/eidos/components/combobox/combobox.css @@ -259,7 +259,7 @@ } [data-floating-wrapper]:has(> [data-combobox-content]) { - z-index: 80; + z-index: var(--combobox-content-z); } [data-combobox-content] { @@ -369,7 +369,7 @@ font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); line-height: var(--leading-ui); - letter-spacing: 0.04em; + letter-spacing: var(--tracking-caps); text-transform: uppercase; } @@ -497,7 +497,7 @@ } [data-combobox-selected-tag][data-disabled] { - opacity: 0.56; + opacity: var(--opacity-disabled); } [data-combobox-selected-tag-remove] { @@ -511,7 +511,7 @@ border-radius: var(--radius-sm); background: transparent; color: inherit; - opacity: 0.7; + opacity: var(--opacity-muted); cursor: pointer; transition: background var(--duration-fast) var(--ease-default), @@ -532,5 +532,5 @@ [data-combobox-selected-tag-remove][data-disabled], [data-combobox-selected-tag-remove]:disabled { cursor: not-allowed; - opacity: 0.4; + opacity: var(--opacity-disabled); } diff --git a/src/uix/eidos/components/command/command.css b/src/uix/eidos/components/command/command.css index bddb280cd..ad339f519 100644 --- a/src/uix/eidos/components/command/command.css +++ b/src/uix/eidos/components/command/command.css @@ -33,7 +33,7 @@ flex-direction: column; inline-size: 100%; min-inline-size: 0; - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-overlay); box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo); @@ -67,7 +67,7 @@ block-size: var(--_command-input-height); padding-inline: var(--_command-input-px); border: 0; - border-block-end: 1px solid var(--color-border-subtle); + border-block-end: var(--border-width) solid var(--color-border-subtle); background: transparent; color: var(--color-content-primary); font-family: var(--font-ui); @@ -134,7 +134,7 @@ font-size: var(--font-size-xs); font-weight: 500; text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: var(--tracking-caps); } [data-command-group-items] { @@ -173,7 +173,7 @@ [data-command-item][data-disabled], [data-command-link-item][data-disabled] { cursor: default; - opacity: 0.55; + opacity: var(--opacity-disabled); } /* ── Item sub-parts (eidos-only) ──────────────────────────────────── */ diff --git a/src/uix/eidos/components/context-menu/context-menu.css b/src/uix/eidos/components/context-menu/context-menu.css index f4b2f8a33..3b4392c74 100644 --- a/src/uix/eidos/components/context-menu/context-menu.css +++ b/src/uix/eidos/components/context-menu/context-menu.css @@ -94,10 +94,10 @@ margin-inline-start: auto; inline-size: 0.625rem; block-size: 0.625rem; - border-inline-end: 2px solid currentColor; - border-block-start: 2px solid currentColor; + border-inline-end: var(--border-width-medium) solid currentColor; + border-block-start: var(--border-width-medium) solid currentColor; transform: rotate(45deg); - opacity: 0.65; + opacity: var(--opacity-muted); } /* ── Group / GroupHeading / Separator ─────────────────────────────── */ @@ -113,7 +113,7 @@ font-size: var(--context-menu-heading-font-size, var(--font-size-xs)); font-weight: var(--style-label-font-weight, 500); text-transform: uppercase; - letter-spacing: 0.04em; /* literal: micro-tracking for caps */ + letter-spacing: var(--tracking-caps); } [data-context-menu-separator] { diff --git a/src/uix/eidos/components/cropper/cropper.css b/src/uix/eidos/components/cropper/cropper.css index 7a82558c5..b582ac592 100644 --- a/src/uix/eidos/components/cropper/cropper.css +++ b/src/uix/eidos/components/cropper/cropper.css @@ -12,7 +12,7 @@ } [data-cropper][data-disabled] { - opacity: 0.6; + opacity: var(--opacity-disabled); pointer-events: none; } @@ -169,7 +169,7 @@ flex: 0 0 auto; inline-size: var(--cropper-zoom-btn-size); block-size: var(--cropper-zoom-btn-size); - border: 1px solid var(--cropper-zoom-btn-border); + border: var(--border-width) solid var(--cropper-zoom-btn-border); border-radius: var(--radius-md); background: var(--cropper-zoom-btn-bg); color: var(--cropper-zoom-btn-fg); diff --git a/src/uix/eidos/components/date-field/date-field.css b/src/uix/eidos/components/date-field/date-field.css index fea99b934..3b918ca89 100644 --- a/src/uix/eidos/components/date-field/date-field.css +++ b/src/uix/eidos/components/date-field/date-field.css @@ -211,7 +211,8 @@ background: var(--_date-field-accent-element, var(--color-primary-element)); color: var(--_date-field-accent-text); font-weight: 600; - box-shadow: inset 0 0 0 2px var(--_date-field-accent-border); + --ring-inset-color: var(--_date-field-accent-border); + box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor); outline: none; } diff --git a/src/uix/eidos/components/dialog/dialog.css b/src/uix/eidos/components/dialog/dialog.css index 5687c797e..760968ccd 100644 --- a/src/uix/eidos/components/dialog/dialog.css +++ b/src/uix/eidos/components/dialog/dialog.css @@ -25,7 +25,7 @@ justify-content: center; height: var(--control-height-md); padding-inline: var(--space-3); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-raised); color: var(--color-content-primary); @@ -50,7 +50,7 @@ [data-dialog-trigger]:disabled, [data-dialog-trigger][data-disabled] { cursor: default; - opacity: 0.5; + opacity: var(--opacity-disabled); } /* ── Overlay ───────────────────────────────────────────────────────────── */ @@ -69,12 +69,12 @@ /* Overlay + content enter/exit motion comes from the motion system (presets * `fade` / `scale-fade` via `data-animation-style`). `data-state` here is only - * steady styling. The prior recipe timing (120ms in / 280ms out) is preserved - * via the per-component duration override. See `eidos-motion.md`. */ + * steady styling. Asymmetric timing — `fast` in / `slow` out — drawn from the + * canonical duration scale. See `eidos-motion.md`. */ [data-dialog-overlay], [data-dialog-content] { - --motion-duration-enter: 120ms; - --motion-duration-exit: 280ms; + --motion-duration-enter: var(--duration-fast); + --motion-duration-exit: var(--duration-slow); } /* ── Content ───────────────────────────────────────────────────────────── */ @@ -390,7 +390,7 @@ [data-dialog-close]:disabled, [data-dialog-close][data-disabled] { cursor: default; - opacity: 0.5; + opacity: var(--opacity-disabled); } /* ── data-last-action exit tinting ───────────────────────────────────────── diff --git a/src/uix/eidos/components/drag-drop/drag-drop.css b/src/uix/eidos/components/drag-drop/drag-drop.css index 7e99b7ea7..4c86f0be2 100644 --- a/src/uix/eidos/components/drag-drop/drag-drop.css +++ b/src/uix/eidos/components/drag-drop/drag-drop.css @@ -46,12 +46,12 @@ } [data-drag-drop-draggable][data-dragging] { - opacity: 0.4; + opacity: var(--opacity-ghost); } [data-drag-drop-draggable][data-disabled] { cursor: not-allowed; - opacity: 0.55; + opacity: var(--opacity-disabled); } [data-drag-drop-draggable]:focus-visible { @@ -69,18 +69,21 @@ /* Highlight all compatible drop targets while ANY drag is active. */ [data-drag-drop-droppable][data-accepting] { - box-shadow: inset 0 0 0 1px var(--_drag-drop-accent-soft); + --ring-inset-color: var(--_drag-drop-accent-soft); + --ring-inset-width: var(--border-width-thin); + box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor); } /* Pointer / focus is over THIS droppable. */ [data-drag-drop-droppable][data-dragover] { background: var(--_drag-drop-accent-soft); - box-shadow: inset 0 0 0 2px var(--_drag-drop-accent); + --ring-inset-color: var(--_drag-drop-accent); + box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor); } [data-drag-drop-droppable][data-disabled] { cursor: not-allowed; - opacity: 0.55; + opacity: var(--opacity-disabled); } [data-drag-drop-droppable]:focus-visible { @@ -91,10 +94,10 @@ /* ── Preview (floating pointer follower) ─ */ [data-drag-drop-preview] { pointer-events: none; - z-index: 99; + z-index: var(--drag-drop-preview-z); padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); - border: 1px solid var(--_drag-drop-accent); + border: var(--border-width) solid var(--_drag-drop-accent); background: var(--color-surface-default); box-shadow: var(--shadow-raised), diff --git a/src/uix/eidos/components/drawer/drawer.css b/src/uix/eidos/components/drawer/drawer.css index 1d497e4fd..7aa5ac7c9 100644 --- a/src/uix/eidos/components/drawer/drawer.css +++ b/src/uix/eidos/components/drawer/drawer.css @@ -28,7 +28,7 @@ [data-drawer-trigger]:disabled, [data-drawer-trigger][data-disabled] { cursor: default; - opacity: 0.5; + opacity: var(--opacity-disabled); } /* ── Overlay ────────────────────────────────────────────────────────────── */ @@ -106,16 +106,16 @@ * etc. (the edge facing the user before closing). Only meaningful in * overlay/inline variants where the drawer animates out. */ [data-drawer-content][data-side='right'] { - border-left: 3px solid transparent; + border-left: var(--border-width-thick) solid transparent; } [data-drawer-content][data-side='left'] { - border-right: 3px solid transparent; + border-right: var(--border-width-thick) solid transparent; } [data-drawer-content][data-side='top'] { - border-bottom: 3px solid transparent; + border-bottom: var(--border-width-thick) solid transparent; } [data-drawer-content][data-side='bottom'] { - border-top: 3px solid transparent; + border-top: var(--border-width-thick) solid transparent; } /* ── Content sizing (variant-shared) ────────────────────────────────────── */ diff --git a/src/uix/eidos/components/dropdown-menu/dropdown-menu.css b/src/uix/eidos/components/dropdown-menu/dropdown-menu.css index ba700bb4a..c6eb3f736 100644 --- a/src/uix/eidos/components/dropdown-menu/dropdown-menu.css +++ b/src/uix/eidos/components/dropdown-menu/dropdown-menu.css @@ -30,7 +30,7 @@ * content (e.g. selected toggle-group chips with z-index 1). The soma * floating layer reads this computed z-index and mirrors it onto the * positioner wrapper. */ - z-index: var(--dropdown-menu-content-z, 80); + z-index: var(--dropdown-menu-content-z); min-inline-size: var(--dropdown-menu-content-min-width, 12rem); max-block-size: var(--dropdown-menu-content-max-height, min(60vh, 22rem)); overflow-y: auto; diff --git a/src/uix/eidos/components/feed/feed.css b/src/uix/eidos/components/feed/feed.css index e9172fb7c..722012f42 100644 --- a/src/uix/eidos/components/feed/feed.css +++ b/src/uix/eidos/components/feed/feed.css @@ -122,18 +122,18 @@ [data-feed-root][data-variant='surface'] [data-feed-article] { background: var(--color-surface-default); - border: 1px solid var(--_feed-divider); + border: var(--border-width) solid var(--_feed-divider); } [data-feed-root][data-variant='outline'] [data-feed-article] { background: transparent; - border: 1px solid var(--_feed-divider); + border: var(--border-width) solid var(--_feed-divider); } [data-feed-root][data-variant='ghost'] [data-feed-article] { background: transparent; border: 0; - border-block-end: 1px solid var(--_feed-divider); + border-block-end: var(--border-width) solid var(--_feed-divider); border-radius: 0; padding-inline: 0; } @@ -171,12 +171,12 @@ gap: var(--_feed-gap); margin-block-start: var(--space-2); padding-inline-start: var(--_feed-thread-indent); - border-inline-start: 2px solid var(--_feed-accent-soft); + border-inline-start: var(--border-width-medium) solid var(--_feed-accent-soft); } [data-feed-root][dir='rtl'] [data-feed-thread] { border-inline-start: 0; - border-inline-end: 2px solid var(--_feed-accent-soft); + border-inline-end: var(--border-width-medium) solid var(--_feed-accent-soft); } /* ── Sentinel ─ @@ -200,7 +200,7 @@ inline-size: 0.75rem; block-size: 0.75rem; margin-inline-end: var(--space-2); - border: 2px solid var(--_feed-accent-soft); + border: var(--border-width-medium) solid var(--_feed-accent-soft); border-block-start-color: var(--_feed-accent); border-radius: var(--radius-full); animation: feed-spin 0.7s linear infinite; diff --git a/src/uix/eidos/components/float-panel/float-panel.css b/src/uix/eidos/components/float-panel/float-panel.css index f60504e2d..6f9d3d16f 100644 --- a/src/uix/eidos/components/float-panel/float-panel.css +++ b/src/uix/eidos/components/float-panel/float-panel.css @@ -120,7 +120,7 @@ padding-inline: var(--float-panel-header-px); background: var(--float-panel-header-bg); /* Accent rule under the header — reflects `data-color` visibly. */ - border-block-end: 2px solid var(--_float-panel-accent); + border-block-end: var(--border-width-medium) solid var(--_float-panel-accent); cursor: grab; touch-action: none; user-select: none; @@ -133,10 +133,12 @@ ring, and a steady ring while in keyboard "grab mode" (data-grabbed). */ [data-float-panel-header]:focus-visible { outline: none; - box-shadow: inset 0 0 0 2px var(--_float-panel-accent); + --ring-inset-color: var(--_float-panel-accent); + box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor); } [data-float-panel-header][data-grabbed] { - box-shadow: inset 0 0 0 2px var(--_float-panel-accent); + --ring-inset-color: var(--_float-panel-accent); + box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor); cursor: grabbing; } @@ -253,7 +255,7 @@ background-size: 10px 10px; background-position: bottom right; background-repeat: no-repeat; - opacity: 0.65; + opacity: var(--opacity-muted); } [data-float-panel-resize-grip]:hover, [data-float-panel-resize-grip][data-grabbed] { @@ -263,7 +265,8 @@ [data-float-panel-resize-grip]:focus-visible { outline: none; opacity: 1; - box-shadow: inset 0 0 0 2px var(--_float-panel-accent); + --ring-inset-color: var(--_float-panel-accent); + box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor); border-radius: var(--radius-sm); } diff --git a/src/uix/eidos/components/grid-list/grid-list.css b/src/uix/eidos/components/grid-list/grid-list.css index 88bc98f6b..b5edb4b0d 100644 --- a/src/uix/eidos/components/grid-list/grid-list.css +++ b/src/uix/eidos/components/grid-list/grid-list.css @@ -32,7 +32,7 @@ flex-direction: column; gap: 1px; padding: var(--space-1); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-default); color: var(--color-content-primary); @@ -115,7 +115,7 @@ } [data-grid-list][data-disabled] { - opacity: 0.55; + opacity: var(--opacity-disabled); cursor: not-allowed; } @@ -158,7 +158,7 @@ [data-grid-list-row][data-disabled] { color: var(--color-content-muted); cursor: default; - opacity: 0.6; + opacity: var(--opacity-disabled); } [data-grid-list-row]:focus-visible { @@ -199,7 +199,7 @@ flex: 0 0 auto; inline-size: var(--_grid-list-checkbox-size); block-size: var(--_grid-list-checkbox-size); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: 0.2rem; background: transparent; cursor: pointer; diff --git a/src/uix/eidos/components/image-adjustments/image-adjustments.css b/src/uix/eidos/components/image-adjustments/image-adjustments.css index 16f475151..0ec63e735 100644 --- a/src/uix/eidos/components/image-adjustments/image-adjustments.css +++ b/src/uix/eidos/components/image-adjustments/image-adjustments.css @@ -16,7 +16,7 @@ } [data-image-adjustments][data-disabled] { - opacity: 0.55; + opacity: var(--opacity-disabled); } [data-image-adjustments-item] { @@ -74,7 +74,7 @@ } [data-image-adjustments-reset][disabled] { - opacity: 0.4; + opacity: var(--opacity-disabled); cursor: default; text-decoration: none; } diff --git a/src/uix/eidos/components/image-picker/image-picker.css b/src/uix/eidos/components/image-picker/image-picker.css index 7a5e9ec67..555d806f5 100644 --- a/src/uix/eidos/components/image-picker/image-picker.css +++ b/src/uix/eidos/components/image-picker/image-picker.css @@ -16,7 +16,7 @@ } [data-image-picker][data-disabled] { - opacity: 0.6; + opacity: var(--opacity-disabled); } [data-image-picker-preview] { @@ -24,7 +24,7 @@ aspect-ratio: var(--image-picker-preview-aspect); border-radius: var(--image-picker-preview-radius); background: var(--image-picker-preview-bg); - border: 1px solid var(--image-picker-preview-border); + border: var(--border-width) solid var(--image-picker-preview-border); overflow: hidden; } @@ -71,7 +71,7 @@ justify-content: center; inline-size: var(--image-picker-button-size); block-size: var(--image-picker-button-size); - border: 1px solid var(--image-picker-button-border); + border: var(--border-width) solid var(--image-picker-button-border); border-radius: var(--image-picker-button-radius); background: var(--image-picker-button-bg); color: var(--image-picker-button-fg); @@ -93,7 +93,7 @@ [data-image-picker-rotate][disabled], [data-image-picker-remove][disabled] { - opacity: 0.5; + opacity: var(--opacity-disabled); cursor: default; } diff --git a/src/uix/eidos/components/image/image.css b/src/uix/eidos/components/image/image.css index 3cb58eaf5..cb5a47c55 100644 --- a/src/uix/eidos/components/image/image.css +++ b/src/uix/eidos/components/image/image.css @@ -148,12 +148,7 @@ content: ''; position: absolute; inset: 0; - background: linear-gradient( - 90deg, - var(--image-placeholder-base) 0%, - var(--image-placeholder-highlight) 50%, - var(--image-placeholder-base) 100% - ); + background: var(--gradient-shimmer); background-size: 200% 100%; animation: image-shimmer var(--image-shimmer-duration) linear infinite; z-index: -1; diff --git a/src/uix/eidos/components/kbd/kbd.css b/src/uix/eidos/components/kbd/kbd.css index 5ceffd4df..9ebf96aa6 100644 --- a/src/uix/eidos/components/kbd/kbd.css +++ b/src/uix/eidos/components/kbd/kbd.css @@ -27,19 +27,19 @@ vertical-align: baseline; user-select: none; white-space: nowrap; - letter-spacing: 0.02em; + letter-spacing: var(--tracking-wide); } [data-kbd][data-variant='surface'] { background: var(--color-neutral-element); - border: 1px solid var(--color-neutral-border); - border-bottom-width: 2px; + border: var(--border-width) solid var(--color-neutral-border); + border-bottom-width: var(--border-width-medium); box-shadow: 0 1px 0 var(--color-neutral-border) inset; } [data-kbd][data-variant='outline'] { background: transparent; - border: 1px solid var(--color-neutral-border); + border: var(--border-width) solid var(--color-neutral-border); } [data-kbd][data-variant='ghost'] { diff --git a/src/uix/eidos/components/link-preview/link-preview.css b/src/uix/eidos/components/link-preview/link-preview.css index 273240e92..59fb6f95e 100644 --- a/src/uix/eidos/components/link-preview/link-preview.css +++ b/src/uix/eidos/components/link-preview/link-preview.css @@ -60,7 +60,7 @@ min-inline-size: var(--_link-preview-min-width); max-inline-size: var(--_link-preview-max-width); padding: var(--_link-preview-py) var(--_link-preview-px); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-overlay); color: var(--color-content-primary); diff --git a/src/uix/eidos/components/link/link.css b/src/uix/eidos/components/link/link.css index cb2fc2d35..2664bb8a2 100644 --- a/src/uix/eidos/components/link/link.css +++ b/src/uix/eidos/components/link/link.css @@ -27,8 +27,8 @@ cursor: pointer; text-decoration: none; transition: - color 120ms ease, - text-decoration-color 120ms ease; + color var(--duration-fast) ease, + text-decoration-color var(--duration-fast) ease; } [data-link][data-color='primary'] { @@ -90,7 +90,7 @@ [data-link][data-variant='subtle']:hover, [data-link][data-variant='plain']:hover { color: inherit; - opacity: 0.85; + opacity: var(--opacity-subtle); } [data-link]:active { @@ -98,13 +98,13 @@ } [data-link]:focus-visible { - outline: 2px solid var(--color-primary-solid); + outline: var(--border-width-medium) solid var(--color-primary-solid); outline-offset: 2px; } [data-link][data-disabled] { cursor: not-allowed; - opacity: 0.5; + opacity: var(--opacity-disabled); pointer-events: none; } diff --git a/src/uix/eidos/components/listbox/listbox.css b/src/uix/eidos/components/listbox/listbox.css index 76ccd9a85..4bbff5d3a 100644 --- a/src/uix/eidos/components/listbox/listbox.css +++ b/src/uix/eidos/components/listbox/listbox.css @@ -30,7 +30,7 @@ flex-direction: column; gap: var(--space-1); padding: var(--space-1); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-default); color: var(--color-content-primary); @@ -136,7 +136,7 @@ } [data-listbox][data-disabled] { - opacity: 0.55; + opacity: var(--opacity-disabled); cursor: not-allowed; } @@ -183,7 +183,7 @@ [data-listbox-item][data-disabled] { color: var(--color-content-muted); cursor: default; - opacity: 0.6; + opacity: var(--opacity-disabled); } /* ── ItemIndicator (✓ glyph on selected items) ─ @@ -221,5 +221,5 @@ font-size: var(--font-size-xs); font-weight: 500; text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: var(--tracking-caps); } diff --git a/src/uix/eidos/components/menubar/menubar.css b/src/uix/eidos/components/menubar/menubar.css index 8a9ebd682..90908711c 100644 --- a/src/uix/eidos/components/menubar/menubar.css +++ b/src/uix/eidos/components/menubar/menubar.css @@ -34,7 +34,7 @@ align-items: center; gap: var(--_menubar-gap); padding: var(--_menubar-padding); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--_menubar-radius); background: var(--color-surface-raised); } diff --git a/src/uix/eidos/components/navigation-menu/navigation-menu.css b/src/uix/eidos/components/navigation-menu/navigation-menu.css index 859b25293..b61b10feb 100644 --- a/src/uix/eidos/components/navigation-menu/navigation-menu.css +++ b/src/uix/eidos/components/navigation-menu/navigation-menu.css @@ -135,10 +135,10 @@ inline-size: 0.5rem; block-size: 0.5rem; margin-inline-start: var(--space-1); - border-block-end: 1.5px solid currentColor; - border-inline-end: 1.5px solid currentColor; + border-block-end: var(--border-width-medium) solid currentColor; + border-inline-end: var(--border-width-medium) solid currentColor; transform: translateY(-1px) rotate(45deg); - opacity: 0.65; + opacity: var(--opacity-muted); transition: transform var(--duration-fast) var(--ease-default); } @@ -161,11 +161,11 @@ position: absolute; inset-block-start: calc(100% + var(--space-1)); inset-inline-start: 0; - z-index: 50; + z-index: var(--navigation-menu-content-z); min-inline-size: 14rem; padding: var(--space-2); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--radius-lg); background: var(--color-surface-overlay); color: var(--color-content-primary); diff --git a/src/uix/eidos/components/password-field/password-field.css b/src/uix/eidos/components/password-field/password-field.css index e3578418e..6513b5ea2 100644 --- a/src/uix/eidos/components/password-field/password-field.css +++ b/src/uix/eidos/components/password-field/password-field.css @@ -204,7 +204,7 @@ font: inherit; outline: none; /* Slightly wider tracking helps recognise the · placeholder mask. */ - letter-spacing: 0.02em; + letter-spacing: var(--tracking-wide); } [data-password-field-input]::placeholder { diff --git a/src/uix/eidos/components/popover/popover.css b/src/uix/eidos/components/popover/popover.css index 4b077fea2..e3c5a87ca 100644 --- a/src/uix/eidos/components/popover/popover.css +++ b/src/uix/eidos/components/popover/popover.css @@ -9,7 +9,7 @@ justify-content: center; height: var(--control-height-md); padding-inline: var(--space-3); - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-raised); color: var(--color-content-primary); @@ -229,25 +229,25 @@ * Tint the trigger-facing edge (opposite to data-side) during the exit * animation so the user gets a brief tinted flash by causa of the close. */ -[data-popover-content][data-state='closed'][data-side='top'][data-last-action='saved'] { border-bottom: 2px solid var(--color-fulfill-element); } -[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='saved'] { border-top: 2px solid var(--color-fulfill-element); } -[data-popover-content][data-state='closed'][data-side='right'][data-last-action='saved'] { border-left: 2px solid var(--color-fulfill-element); } -[data-popover-content][data-state='closed'][data-side='left'][data-last-action='saved'] { border-right: 2px solid var(--color-fulfill-element); } +[data-popover-content][data-state='closed'][data-side='top'][data-last-action='saved'] { border-bottom: var(--border-width-medium) solid var(--color-fulfill-element); } +[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='saved'] { border-top: var(--border-width-medium) solid var(--color-fulfill-element); } +[data-popover-content][data-state='closed'][data-side='right'][data-last-action='saved'] { border-left: var(--border-width-medium) solid var(--color-fulfill-element); } +[data-popover-content][data-state='closed'][data-side='left'][data-last-action='saved'] { border-right: var(--border-width-medium) solid var(--color-fulfill-element); } -[data-popover-content][data-state='closed'][data-side='top'][data-last-action='failed'] { border-bottom: 2px solid var(--color-threat-element); } -[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='failed'] { border-top: 2px solid var(--color-threat-element); } -[data-popover-content][data-state='closed'][data-side='right'][data-last-action='failed'] { border-left: 2px solid var(--color-threat-element); } -[data-popover-content][data-state='closed'][data-side='left'][data-last-action='failed'] { border-right: 2px solid var(--color-threat-element); } +[data-popover-content][data-state='closed'][data-side='top'][data-last-action='failed'] { border-bottom: var(--border-width-medium) solid var(--color-threat-element); } +[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='failed'] { border-top: var(--border-width-medium) solid var(--color-threat-element); } +[data-popover-content][data-state='closed'][data-side='right'][data-last-action='failed'] { border-left: var(--border-width-medium) solid var(--color-threat-element); } +[data-popover-content][data-state='closed'][data-side='left'][data-last-action='failed'] { border-right: var(--border-width-medium) solid var(--color-threat-element); } [data-popover-content][data-state='closed'][data-side='top'][data-last-action='cancelled'], [data-popover-content][data-state='closed'][data-side='top'][data-last-action='dismissed'], -[data-popover-content][data-state='closed'][data-side='top'][data-last-action='dismissed-outside'] { border-bottom: 2px solid var(--color-neutral-element); } +[data-popover-content][data-state='closed'][data-side='top'][data-last-action='dismissed-outside'] { border-bottom: var(--border-width-medium) solid var(--color-neutral-element); } [data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='cancelled'], [data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed'], -[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed-outside'] { border-top: 2px solid var(--color-neutral-element); } +[data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed-outside'] { border-top: var(--border-width-medium) solid var(--color-neutral-element); } [data-popover-content][data-state='closed'][data-side='right'][data-last-action='cancelled'], [data-popover-content][data-state='closed'][data-side='right'][data-last-action='dismissed'], -[data-popover-content][data-state='closed'][data-side='right'][data-last-action='dismissed-outside'] { border-left: 2px solid var(--color-neutral-element); } +[data-popover-content][data-state='closed'][data-side='right'][data-last-action='dismissed-outside'] { border-left: var(--border-width-medium) solid var(--color-neutral-element); } [data-popover-content][data-state='closed'][data-side='left'][data-last-action='cancelled'], [data-popover-content][data-state='closed'][data-side='left'][data-last-action='dismissed'], -[data-popover-content][data-state='closed'][data-side='left'][data-last-action='dismissed-outside'] { border-right: 2px solid var(--color-neutral-element); } +[data-popover-content][data-state='closed'][data-side='left'][data-last-action='dismissed-outside'] { border-right: var(--border-width-medium) solid var(--color-neutral-element); } diff --git a/src/uix/eidos/components/scroll-frames/scroll-frames.css b/src/uix/eidos/components/scroll-frames/scroll-frames.css index 5e8aaf55e..24431d209 100644 --- a/src/uix/eidos/components/scroll-frames/scroll-frames.css +++ b/src/uix/eidos/components/scroll-frames/scroll-frames.css @@ -33,7 +33,7 @@ inset-block-end: 1.5rem; font-size: var(--font-size-xs); line-height: 1; - letter-spacing: 0.1em; + letter-spacing: var(--tracking-widest); color: var(--color-neutral-text); opacity: 0.5; } diff --git a/src/uix/eidos/components/search-field/search-field.css b/src/uix/eidos/components/search-field/search-field.css index 846fe35fe..4315d5da6 100644 --- a/src/uix/eidos/components/search-field/search-field.css +++ b/src/uix/eidos/components/search-field/search-field.css @@ -224,6 +224,6 @@ [data-search-field-loading-indicator]::before { animation: none; border-top-color: var(--search-field-loading-indicator-track); - opacity: 0.6; + opacity: var(--opacity-muted); } } diff --git a/src/uix/eidos/components/select/select.css b/src/uix/eidos/components/select/select.css index 3b095faa7..a5134a7e6 100644 --- a/src/uix/eidos/components/select/select.css +++ b/src/uix/eidos/components/select/select.css @@ -304,7 +304,9 @@ /* Highlighted + checked: keep accent track but layer an inset focus shadow so the row reads as "this checked item is the keyboard focus". */ [data-select-item][data-highlighted][data-state='checked'] { - box-shadow: inset 0 0 0 var(--select-item-focus-ring-width, 2px) var(--_select-accent-border); + --ring-inset-color: var(--_select-accent-border); + --ring-inset-width: var(--select-item-focus-ring-width, var(--border-width-medium)); + box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor); } [data-select-item][data-disabled] { @@ -319,12 +321,12 @@ color: var(--select-item-description-color, var(--color-content-muted)); font-size: var(--select-item-description-font-size, 0.85em); line-height: var(--select-line-height); - opacity: 0.8; + opacity: var(--opacity-subtle); } [data-select-item][data-state='checked'] [data-select-item-description] { color: inherit; - opacity: 0.85; + opacity: var(--opacity-subtle); } [data-select-item-indicator] { diff --git a/src/uix/eidos/components/skeleton/skeleton.css b/src/uix/eidos/components/skeleton/skeleton.css index 67b5ead07..84d3b96ae 100644 --- a/src/uix/eidos/components/skeleton/skeleton.css +++ b/src/uix/eidos/components/skeleton/skeleton.css @@ -101,7 +101,7 @@ [data-skeleton][data-animated] { background-image: linear-gradient( - 90deg, + var(--gradient-angle-to-r), var(--_skeleton-bg) 0%, var(--_skeleton-highlight) 50%, var(--_skeleton-bg) 100% @@ -117,7 +117,7 @@ [data-skeleton][data-shape='text'][data-animated] [data-skeleton-line] { background-image: linear-gradient( - 90deg, + var(--gradient-angle-to-r), var(--_skeleton-bg) 0%, var(--_skeleton-highlight) 50%, var(--_skeleton-bg) 100% diff --git a/src/uix/eidos/components/table/table.css b/src/uix/eidos/components/table/table.css index ed1258183..e1901d864 100644 --- a/src/uix/eidos/components/table/table.css +++ b/src/uix/eidos/components/table/table.css @@ -54,17 +54,17 @@ /* ── Variant — surface (default) ─ */ [data-table-root][data-variant='surface'] { - border: 1px solid var(--_table-divider); + border: var(--border-width) solid var(--_table-divider); background: var(--color-surface-default); } [data-table-root][data-variant='outline'] { - border: 1px solid var(--_table-divider); + border: var(--border-width) solid var(--_table-divider); background: transparent; } [data-table-root][data-variant='ghost'] { - border: 1px solid transparent; + border: var(--border-width) solid transparent; background: transparent; } @@ -158,7 +158,7 @@ } [data-table-root][data-disabled] { - opacity: 0.55; + opacity: var(--opacity-disabled); pointer-events: none; } @@ -199,7 +199,7 @@ text-align: start; font-weight: var(--font-weight-semibold, 600); color: var(--color-content-primary); - border-block-end: 1px solid var(--_table-divider); + border-block-end: var(--border-width) solid var(--_table-divider); white-space: nowrap; vertical-align: middle; user-select: none; @@ -267,7 +267,7 @@ box-sizing: border-box; padding-block: var(--_table-cell-py); padding-inline: var(--_table-cell-px); - border-block-end: 1px solid var(--_table-divider); + border-block-end: var(--border-width) solid var(--_table-divider); vertical-align: middle; color: inherit; } @@ -289,7 +289,7 @@ [data-table-footer] [data-table-cell] { font-weight: var(--font-weight-semibold, 600); border-block-end: 0; - border-block-start: 1px solid var(--_table-divider); + border-block-start: var(--border-width) solid var(--_table-divider); } /* ── RowDetail (disclosure) ─ */ @@ -303,7 +303,7 @@ [data-table-row-detail] > td { padding: var(--space-3) var(--_table-cell-px); - border-block-end: 1px solid var(--_table-divider); + border-block-end: var(--border-width) solid var(--_table-divider); color: var(--color-content-secondary); } @@ -338,7 +338,7 @@ justify-content: center; inline-size: 1em; block-size: 1em; - opacity: 0.55; + opacity: var(--opacity-disabled); transition: opacity var(--duration-fast) var(--ease-default); } diff --git a/src/uix/eidos/components/textarea/textarea.css b/src/uix/eidos/components/textarea/textarea.css index fce4a58df..47313345d 100644 --- a/src/uix/eidos/components/textarea/textarea.css +++ b/src/uix/eidos/components/textarea/textarea.css @@ -141,7 +141,7 @@ [data-textarea-input][data-disabled] { cursor: not-allowed; - opacity: var(--opacity-disabled, 0.6); + opacity: var(--opacity-disabled); } [data-textarea-input]::placeholder { @@ -167,7 +167,7 @@ } [data-textarea-count-sep] { - opacity: 0.5; + opacity: var(--opacity-muted); } @media (prefers-reduced-motion: reduce) { diff --git a/src/uix/eidos/components/time-picker/time-picker.css b/src/uix/eidos/components/time-picker/time-picker.css index 35a38068c..d6c2d7099 100644 --- a/src/uix/eidos/components/time-picker/time-picker.css +++ b/src/uix/eidos/components/time-picker/time-picker.css @@ -178,6 +178,20 @@ inline-size: 100%; } +/* Channel caption naming each slider (Hour / Minute / Second). Visual + only — the soma Slider already carries the same text as `aria-label`, + so the caption is `aria-hidden`. */ +.time-picker-slider-label { + margin-block-end: var(--space-1); + color: var(--color-content-secondary); + font-family: var(--font-ui); + font-size: var(--font-size-xxs); + font-weight: var(--font-weight-medium); + letter-spacing: var(--tracking-caps); + line-height: 1; + text-transform: uppercase; +} + .time-picker-scale { position: relative; block-size: 0.875rem; diff --git a/src/uix/eidos/components/time-range-picker/time-range-picker.css b/src/uix/eidos/components/time-range-picker/time-range-picker.css index 97522ae43..89aea157c 100644 --- a/src/uix/eidos/components/time-range-picker/time-range-picker.css +++ b/src/uix/eidos/components/time-range-picker/time-range-picker.css @@ -196,7 +196,7 @@ font-size: var(--font-size-xxs); font-weight: 600; text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: var(--tracking-wider); color: var(--color-content-muted); } @@ -298,7 +298,7 @@ display: inline-flex; gap: var(--space-1); padding: 2px; - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-default); } @@ -340,7 +340,7 @@ [data-time-range-picker][data-readonly] [data-time-range-picker-trigger], [data-time-range-picker][data-readonly] [data-time-range-picker-clear] { pointer-events: none; - opacity: 0.7; + opacity: var(--opacity-muted); cursor: default; } diff --git a/src/uix/eidos/components/toast/toast.css b/src/uix/eidos/components/toast/toast.css index 4d1e076f1..9275f5cf7 100644 --- a/src/uix/eidos/components/toast/toast.css +++ b/src/uix/eidos/components/toast/toast.css @@ -227,7 +227,7 @@ * extra child element. */ [data-toast-item][data-loading] [data-toast-status] { background: transparent; - border: 2px solid color-mix(in srgb, var(--_toast-status-color) 30%, transparent); + border: var(--border-width-medium) solid color-mix(in srgb, var(--_toast-status-color) 30%, transparent); border-block-start-color: var(--_toast-status-color); animation: toast-spinner 0.9s linear infinite; } diff --git a/src/uix/eidos/components/tooltip/tooltip.css b/src/uix/eidos/components/tooltip/tooltip.css index 07ac29ae2..16bf9c1e0 100644 --- a/src/uix/eidos/components/tooltip/tooltip.css +++ b/src/uix/eidos/components/tooltip/tooltip.css @@ -69,7 +69,7 @@ --_tooltip-bg: color-mix(in srgb, var(--color-surface-overlay) 70%, transparent); --_tooltip-border: transparent; --_tooltip-shadow: none; - backdrop-filter: blur(8px); + backdrop-filter: blur(var(--blur-md)); } /* ── Arrow ──────────────────────────────────────────────────────────────── */ diff --git a/src/uix/eidos/components/tree-grid/tree-grid.css b/src/uix/eidos/components/tree-grid/tree-grid.css index c8ead6010..30b79a605 100644 --- a/src/uix/eidos/components/tree-grid/tree-grid.css +++ b/src/uix/eidos/components/tree-grid/tree-grid.css @@ -50,15 +50,15 @@ /* ── Variant ─ */ [data-tree-grid-root][data-variant='surface'] { - border: 1px solid var(--_tree-grid-divider); + border: var(--border-width) solid var(--_tree-grid-divider); background: var(--color-surface-default); } [data-tree-grid-root][data-variant='outline'] { - border: 1px solid var(--_tree-grid-divider); + border: var(--border-width) solid var(--_tree-grid-divider); background: transparent; } [data-tree-grid-root][data-variant='ghost'] { - border: 1px solid transparent; + border: var(--border-width) solid transparent; background: transparent; } @@ -137,7 +137,7 @@ } [data-tree-grid-root][data-disabled] { - opacity: 0.55; + opacity: var(--opacity-disabled); pointer-events: none; } @@ -178,7 +178,7 @@ /* ── Header ─ */ [data-tree-grid-header] { background: var(--_tree-grid-header-bg); - border-block-end: 1px solid var(--_tree-grid-divider); + border-block-end: var(--border-width) solid var(--_tree-grid-divider); font-weight: var(--font-weight-semibold, 600); } @@ -197,7 +197,7 @@ /* ── Body row ─ */ [data-tree-grid-row] { background: var(--color-surface-default); - border-block-end: 1px solid var(--_tree-grid-divider); + border-block-end: var(--border-width) solid var(--_tree-grid-divider); transition: background var(--duration-fast) var(--ease-default); outline: none; } @@ -225,7 +225,7 @@ [data-tree-grid-row][data-disabled] { color: var(--color-content-muted); cursor: not-allowed; - opacity: 0.6; + opacity: var(--opacity-disabled); } [data-tree-grid-row]:focus-visible { diff --git a/src/uix/eidos/components/tree-view/tree-view.css b/src/uix/eidos/components/tree-view/tree-view.css index 8c4c0d841..4f3626cf4 100644 --- a/src/uix/eidos/components/tree-view/tree-view.css +++ b/src/uix/eidos/components/tree-view/tree-view.css @@ -46,19 +46,19 @@ /* ── Variant ─ */ [data-tree-view-root][data-variant='surface'] { - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); background: var(--color-surface-default); padding: var(--space-2); } [data-tree-view-root][data-variant='outline'] { - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); background: transparent; padding: var(--space-2); } [data-tree-view-root][data-variant='ghost'] { - border: 1px solid transparent; + border: var(--border-width) solid transparent; background: transparent; padding: 0; } @@ -178,7 +178,7 @@ [data-tree-view-item][data-disabled] { color: var(--color-content-muted); cursor: not-allowed; - opacity: 0.6; + opacity: var(--opacity-disabled); } [data-tree-view-branch-control]:focus-visible, diff --git a/src/uix/eidos/components/virtual-grid/virtual-grid.css b/src/uix/eidos/components/virtual-grid/virtual-grid.css index 03bc96792..0a2a6a36d 100644 --- a/src/uix/eidos/components/virtual-grid/virtual-grid.css +++ b/src/uix/eidos/components/virtual-grid/virtual-grid.css @@ -46,15 +46,15 @@ } [data-virtual-grid-root][data-variant='surface'] { - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); background: var(--color-surface-default); } [data-virtual-grid-root][data-variant='outline'] { - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); background: transparent; } [data-virtual-grid-root][data-variant='ghost'] { - border: 1px solid transparent; + border: var(--border-width) solid transparent; background: transparent; } diff --git a/src/uix/eidos/components/virtual-list/virtual-list.css b/src/uix/eidos/components/virtual-list/virtual-list.css index 7ec69d3cb..595927aee 100644 --- a/src/uix/eidos/components/virtual-list/virtual-list.css +++ b/src/uix/eidos/components/virtual-list/virtual-list.css @@ -51,15 +51,15 @@ /* ── Variant ─ */ [data-virtual-list-root][data-variant='surface'] { - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); background: var(--color-surface-default); } [data-virtual-list-root][data-variant='outline'] { - border: 1px solid var(--color-border-default); + border: var(--border-width) solid var(--color-border-default); background: transparent; } [data-virtual-list-root][data-variant='ghost'] { - border: 1px solid transparent; + border: var(--border-width) solid transparent; background: transparent; } diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 839bb6a77..fe699bf4e 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -124,6 +124,11 @@ } :root { + --breakpoint-sm: 480px; + --breakpoint-md: 768px; + --breakpoint-lg: 1024px; + --breakpoint-xl: 1280px; + --breakpoint-xxl: 1536px; --space-0: 0px; --space-1: calc(4px * var(--density-space-scale) * var(--scaling)); --space-2: calc(8px * var(--density-space-scale) * var(--scaling)); @@ -157,16 +162,17 @@ --radius-xxl: 20px; --radius-full: 9999px; --border-width-none: 0px; - --border-width-hairline: 0.5px; --border-width-thin: 1px; - --border-width-medium: 1.5px; - --border-width-thick: 2px; + --border-width-medium: 2px; + --border-width-thick: 3px; + --border-width-heavy: 4px; --border-style-solid: solid; --border-style-dashed: dashed; --border-style-dotted: dotted; --border-width: var(--border-width-thin); --border-style: var(--border-style-solid); --border: var(--border-width) var(--border-style) var(--color-border-default); + --ring-inset-width: var(--border-width-medium); --focus-ring-offset: 1px; --focus-ring-width: 2px; --focus-ring-inner-width: 0px; @@ -242,13 +248,50 @@ --icon-stroke-width-sm: 1.25; --icon-stroke-width-md: 1.5; --icon-stroke-width-lg: 1.75; + --blur-none: 0px; + --blur-sm: calc(4px * var(--scaling)); + --blur-md: calc(8px * var(--scaling)); + --blur-lg: calc(12px * var(--scaling)); + --blur-xl: calc(16px * var(--scaling)); + --blur-xxl: calc(24px * var(--scaling)); + --gradient-angle-to-t: 0deg; + --gradient-angle-to-tr: 45deg; + --gradient-angle-to-r: 90deg; + --gradient-angle-to-br: 135deg; + --gradient-angle-to-b: 180deg; + --gradient-angle-to-bl: 225deg; + --gradient-angle-to-l: 270deg; + --gradient-angle-to-tl: 315deg; + --gradient-shimmer: linear-gradient(var(--gradient-angle-to-r), var(--color-surface-raised) 0%, color-mix(in srgb, var(--color-surface-default) 70%, var(--color-surface-overlay)) 50%, var(--color-surface-raised) 100%); --opacity-0: 0; - --opacity-muted: 0.72; - --opacity-disabled: 0.48; - --opacity-scrim: 0.42; - --opacity-overlay: 0.66; - --opacity-hover: 0.92; - --opacity-press: 0.84; + --opacity-5: 0.05; + --opacity-10: 0.1; + --opacity-15: 0.15; + --opacity-20: 0.2; + --opacity-25: 0.25; + --opacity-30: 0.3; + --opacity-35: 0.35; + --opacity-40: 0.4; + --opacity-45: 0.45; + --opacity-50: 0.5; + --opacity-55: 0.55; + --opacity-60: 0.6; + --opacity-65: 0.65; + --opacity-70: 0.7; + --opacity-75: 0.75; + --opacity-80: 0.8; + --opacity-85: 0.85; + --opacity-90: 0.9; + --opacity-95: 0.95; + --opacity-100: 1; + --opacity-ghost: 0.3; + --opacity-disabled: 0.4; + --opacity-scrim: 0.45; + --opacity-muted: 0.65; + --opacity-overlay: 0.65; + --opacity-subtle: 0.8; + --opacity-press: 0.85; + --opacity-hover: 0.9; --opacity-full: 1; --z-index-base: 0; --z-index-raised: 1; @@ -294,7 +337,9 @@ --tracking-tight: -0.02em; --tracking-normal: 0; --tracking-wide: 0.02em; + --tracking-caps: 0.04em; --tracking-wider: 0.05em; + --tracking-widest: 0.1em; --leading-none: 1; --leading-tight: 1.15; --leading-snug: 1.3; @@ -454,14 +499,14 @@ --depth-overlay-shadow: var(--shadow-overlay); --depth-overlay-halo: inset 0 1px 0 0 color-mix(in oklab, white 7%, transparent); --depth-overlay-z: var(--z-index-popover); - --depth-overlay-blur: 10px; + --depth-overlay-blur: var(--blur-lg); --depth-modal-surface: var(--color-surface-overlay); --depth-modal-shadow: var(--shadow-overlay); --depth-modal-halo: inset 0 1px 0 0 color-mix(in oklab, white 8%, transparent); --depth-modal-z: var(--z-index-modal); - --depth-modal-blur: 16px; + --depth-modal-blur: var(--blur-xl); --depth-recessed-surface: var(--color-surface-muted); - --depth-recessed-shadow: inset 0 1px 2px color-mix(in srgb, var(--color-neutral-contrast) 12%, transparent); + --depth-recessed-shadow: var(--shadow-inset-subtle); --depth-recessed-z: var(--z-index-base); --shape-smoothing: 2; --shape-nest-gap: var(--space-2); @@ -594,18 +639,18 @@ --avatar-size-xl: 64px; --avatar-size-xxl: 96px; --avatar-font-family: var(--font-ui); - --avatar-font-size-xs: 10px; - --avatar-font-size-sm: 12px; - --avatar-font-size-md: 14px; - --avatar-font-size-lg: 16px; - --avatar-font-size-xl: 20px; - --avatar-font-size-xxl: 28px; + --avatar-font-size-xs: var(--font-size-xxs); + --avatar-font-size-sm: var(--font-size-xs); + --avatar-font-size-md: var(--font-size-sm); + --avatar-font-size-lg: var(--font-size-md); + --avatar-font-size-xl: var(--font-size-lg); + --avatar-font-size-xxl: var(--font-size-xl); --avatar-font-weight: var(--font-weight-medium); --avatar-radius-full: var(--radius-full); --avatar-radius-md: var(--radius-md); --avatar-radius-sm: var(--radius-sm); --avatar-radius-none: 0; - --avatar-border-width: 2px; + --avatar-border-width: var(--border-width-medium); --avatar-primary-solid-bg: var(--color-primary-solid); --avatar-primary-solid-fg: var(--color-primary-contrast); --avatar-primary-soft-bg: var(--color-primary-surface); @@ -660,7 +705,7 @@ --avatar-color-custom: currentColor; --avatar-color-custom-contrast: white; --avatar-ring-color-custom: currentColor; - --avatar-badge-border-width: 2px; + --avatar-badge-border-width: var(--border-width-medium); --avatar-badge-color-custom: currentColor; --avatar-badge-color-custom-contrast: white; --avatar-group-gap-xs: var(--space-1); @@ -668,7 +713,7 @@ --avatar-group-gap-md: var(--space-3); --avatar-group-gap-lg: var(--space-4); --avatar-group-overlap: -0.6em; - --avatar-group-carve-width: 2px; + --avatar-group-carve-width: var(--border-width-medium); --avatar-group-carve-color: var(--color-surface-default); --avatar-group-reverse-z-max: 20; --avatar-group-max: 99; @@ -820,15 +865,15 @@ --card-footer-gap: var(--space-3); --card-footer-padding-start: var(--space-3); --card-footer-border-color: var(--color-border-subtle); - --card-emerge-duration: 320ms; + --card-emerge-duration: var(--duration-slow); --card-emerge-ease: var(--ease-out); --card-emerge-distance: 8px; --card-hover-lift: -2px; --card-hover-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo); --card-press-scale: 0.985; --card-press-duration: 80ms; - --card-selected-ring-width: 2px; - --card-disabled-opacity: 0.6; + --card-selected-ring-width: var(--border-width-medium); + --card-disabled-opacity: var(--opacity-disabled); --card-color-custom: currentColor; --card-color-custom-contrast: white; --card-primary-solid: var(--color-primary-solid); @@ -903,7 +948,7 @@ --checkbox-border-off-hover: var(--color-border-strong); --checkbox-bg-disabled: color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) ); --checkbox-border-disabled: color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) ); - --checkbox-disabled-opacity: 0.72; + --checkbox-disabled-opacity: var(--opacity-disabled); --checkbox-indicator-color: var(--color-content-on-solid); --checkbox-indicator-color-disabled: color-mix( in srgb, var(--color-content-on-solid) 82%, var(--color-surface-default) ); --checkbox-primary-solid: var(--color-primary-solid); @@ -932,11 +977,11 @@ --collapsible-content-padding: var(--space-3); --collapsible-transition-duration: var(--duration-normal); --collapsible-transition-ease: var(--ease-default); - --collapsible-disabled-opacity: 0.5; + --collapsible-disabled-opacity: var(--opacity-disabled); --dialog-trigger-font-size: var(--font-size-sm); --dialog-overlay-bg: var(--color-overlay); --dialog-overlay-opacity: 62%; - --dialog-overlay-blur: 10px; + --dialog-overlay-blur: var(--blur-lg); --dialog-overlay-z: 70; --dialog-content-bg: var(--color-surface-overlay); --dialog-content-color: var(--color-content-primary); @@ -1047,7 +1092,7 @@ --field-error-color: var(--color-risk-text); --field-transition-duration: var(--duration-fast); --field-transition-ease: var(--ease-default); - --field-disabled-opacity: 0.56; + --field-disabled-opacity: var(--opacity-disabled); --form-gap-xs: var(--space-3); --form-gap-sm: var(--space-4); --form-gap-md: var(--space-5); @@ -1101,9 +1146,9 @@ --form-widget-border-width: var(--border-width); --form-transition-duration: var(--duration-fast); --form-transition-ease: var(--ease-default); - --form-disabled-opacity: 0.56; + --form-disabled-opacity: var(--opacity-disabled); --drawer-overlay-bg: var(--color-overlay); - --drawer-overlay-blur: 10px; + --drawer-overlay-blur: var(--blur-lg); --drawer-overlay-opacity: 62%; --drawer-overlay-z: 72; --drawer-inline-z: 64; @@ -1122,7 +1167,7 @@ --drawer-content-radius: var(--radius-xl); --drawer-content-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo); --drawer-content-shadow-dragging: var(--shadow-overlay-strong, var(--depth-overlay-shadow)), var(--depth-overlay-halo); - --drawer-content-ring-dragging-width: 2px; + --drawer-content-ring-dragging-width: var(--border-width-medium); --drawer-content-ring-dragging-color: color-mix( in srgb, var(--color-content-secondary) 32%, transparent ); --drawer-content-ring-dragging-offset: 2px; --drawer-content-inset: var(--space-3); @@ -1179,7 +1224,6 @@ --image-fade-duration: var(--duration-normal); --image-fade-ease: var(--ease-out); --image-placeholder-base: var(--color-surface-raised); - --image-placeholder-highlight: color-mix(in srgb, var(--color-surface-default) 70%, var(--color-surface-overlay)); --image-shimmer-duration: 1.8s; --image-placeholder-color-custom: var(--color-neutral-track); --image-font-family: var(--font-ui); @@ -1269,7 +1313,7 @@ --spin-field-control-disabled-opacity: 0.48; --spin-field-transition-duration: var(--duration-fast); --spin-field-transition-ease: var(--ease-default); - --spin-field-disabled-opacity: 0.56; + --spin-field-disabled-opacity: var(--opacity-disabled); --float-panel-bg: var(--color-surface-overlay); --float-panel-border: var(--color-border-default); --float-panel-border-width: var(--border-width); @@ -1388,7 +1432,7 @@ --date-field-segment-hover-bg: color-mix(in srgb, var(--color-content-primary) 6%, transparent); --date-field-transition-duration: var(--duration-fast); --date-field-transition-ease: var(--ease-default); - --date-field-disabled-opacity: 0.56; + --date-field-disabled-opacity: var(--opacity-disabled); --time-field-stack-gap: var(--space-1-5); --time-field-height-xs: var(--control-height-xs); --time-field-height-sm: var(--control-height-sm); @@ -1439,7 +1483,7 @@ --time-field-segment-focus-shadow: inset 0 0 0 var(--border-width) var(--_time-field-accent-border); --time-field-transition-duration: var(--duration-fast); --time-field-transition-ease: var(--ease-default); - --time-field-disabled-opacity: 0.56; + --time-field-disabled-opacity: var(--opacity-disabled); --color-field-stack-gap: var(--space-1-5); --color-field-height-xs: var(--control-height-xs); --color-field-height-sm: var(--control-height-sm); @@ -1499,7 +1543,7 @@ --color-field-format-select-px: var(--space-1); --color-field-transition-duration: var(--duration-fast); --color-field-transition-ease: var(--ease-default); - --color-field-disabled-opacity: 0.56; + --color-field-disabled-opacity: var(--opacity-disabled); --color-picker-content-gap: var(--space-3); --color-picker-content-padding-sm: var(--space-3); --color-picker-content-padding-md: var(--space-3-5); @@ -1533,7 +1577,7 @@ --color-picker-area-border: var(--color-border-subtle); --color-picker-area-thumb-size: 16px; --color-picker-area-thumb-radius: var(--radius-full); - --color-picker-area-thumb-border-width: 2px; + --color-picker-area-thumb-border-width: var(--border-width-medium); --color-picker-area-thumb-border: var(--color-surface-default); --color-picker-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); --color-picker-transparency-cell: var(--color-content-muted); @@ -1561,7 +1605,7 @@ --color-picker-saturation-floor: hsl(0, 0%, 50%); --color-picker-transition-duration: var(--duration-fast); --color-picker-transition-ease: var(--ease-default); - --color-picker-disabled-opacity: 0.56; + --color-picker-disabled-opacity: var(--opacity-disabled); --button-height-xs: var(--control-height-xs); --button-height-sm: var(--control-height-sm); --button-height-md: var(--control-height-md); @@ -1604,7 +1648,7 @@ --button-transition-duration: var(--duration-fast); --button-transition-ease: var(--ease-default); --button-spinner-duration: 720ms; - --button-disabled-opacity: 0.5; + --button-disabled-opacity: var(--opacity-disabled); --button-primary-track: var(--color-primary-track); --button-primary-element: var(--color-primary-element); --button-primary-border: var(--color-primary-border); @@ -1730,7 +1774,7 @@ --calendar-event-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--_calendar-accent-border); --calendar-transition-duration: var(--duration-fast); --calendar-transition-ease: var(--ease-default); - --calendar-disabled-opacity: 0.52; + --calendar-disabled-opacity: var(--opacity-disabled); --chronos-cell-min-block-sm: calc(var(--control-height-md) * 2.6); --chronos-cell-min-block-md: calc(var(--control-height-md) * 3.2); --chronos-cell-min-block-lg: calc(var(--control-height-md) * 4); @@ -1739,7 +1783,7 @@ --chronos-lane-height-md: var(--space-6); --chronos-lane-height-lg: var(--space-7); --chronos-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color); - --chronos-disabled-opacity: 0.52; + --chronos-disabled-opacity: var(--opacity-disabled); --pagination-gap-xs: var(--space-0-5); --pagination-gap-sm: var(--space-1); --pagination-gap-md: var(--space-1-5); @@ -1775,7 +1819,7 @@ --pagination-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color); --pagination-transition-duration: var(--duration-fast); --pagination-transition-ease: var(--ease-default); - --pagination-disabled-opacity: 0.56; + --pagination-disabled-opacity: var(--opacity-disabled); --popover-content-z: 75; --popover-overlay-z: 60; --popover-overlay-bg: var(--color-overlay); @@ -1905,7 +1949,7 @@ --radio-group-focus-ring: 0 0 0 var(--radio-group-focus-ring-offset) var(--radio-group-focus-ring-surface), 0 0 0 calc(var(--radio-group-focus-ring-offset) + var(--radio-group-focus-ring-width)) var(--radio-group-focus-ring-color); --radio-group-transition-duration: var(--duration-fast); --radio-group-transition-ease: var(--ease-default); - --radio-group-disabled-opacity: 0.72; + --radio-group-disabled-opacity: var(--opacity-disabled); --radio-group-primary-solid: var(--color-primary-solid); --radio-group-primary-solid-hover: var(--color-primary-solid-hover); --radio-group-neutral-solid: var(--color-neutral-solid); @@ -1969,7 +2013,7 @@ --radio-cards-focus-ring: var(--radio-group-focus-ring); --radio-cards-transition-duration: var(--duration-fast); --radio-cards-transition-ease: var(--ease-default); - --radio-cards-disabled-opacity: 0.6; + --radio-cards-disabled-opacity: var(--opacity-disabled); --rating-group-gap-xs: var(--space-0-5); --rating-group-gap-sm: var(--space-1); --rating-group-gap-md: var(--space-1-5); @@ -1995,7 +2039,7 @@ --rating-group-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color); --rating-group-transition-duration: var(--duration-fast); --rating-group-transition-ease: var(--ease-default); - --rating-group-disabled-opacity: 0.56; + --rating-group-disabled-opacity: var(--opacity-disabled); --search-field-height-xs: var(--control-height-xs); --search-field-height-sm: var(--control-height-sm); --search-field-height-md: var(--control-height-md); @@ -2046,7 +2090,7 @@ --search-field-loading-indicator-duration: 900ms; --search-field-transition-duration: var(--duration-fast); --search-field-transition-ease: var(--ease-default); - --search-field-disabled-opacity: 0.56; + --search-field-disabled-opacity: var(--opacity-disabled); --password-field-height-xs: var(--control-height-xs); --password-field-height-sm: var(--control-height-sm); --password-field-height-md: var(--control-height-md); @@ -2105,7 +2149,7 @@ --password-field-strength-radius: var(--radius-sm); --password-field-transition-duration: var(--duration-fast); --password-field-transition-ease: var(--ease-default); - --password-field-disabled-opacity: 0.56; + --password-field-disabled-opacity: var(--opacity-disabled); --select-stack-gap: var(--space-2); --select-font-family: var(--font-ui); --select-font-size-xs: var(--font-size-xs); @@ -2203,7 +2247,7 @@ --select-item-bg-highlighted: var(--color-surface-raised); --select-item-color-highlighted: var(--color-content-primary); --select-item-indicator-size: var(--icon-size-sm); - --select-item-focus-ring-width: 2px; + --select-item-focus-ring-width: var(--border-width-medium); --select-item-description-mt: 0.125rem; --select-item-description-color: var(--color-content-muted); --select-item-description-font-size: 0.85em; @@ -2276,7 +2320,7 @@ --toolbar-separator-radius: var(--radius-full); --toolbar-transition-duration: var(--duration-fast); --toolbar-transition-ease: var(--ease-default); - --toolbar-disabled-opacity: 0.48; + --toolbar-disabled-opacity: var(--opacity-disabled); --file-upload-gap-sm: var(--space-2); --file-upload-gap-md: var(--space-3); --file-upload-gap-lg: var(--space-4); @@ -2336,7 +2380,7 @@ --file-upload-progress-track-bg: var(--color-neutral-track); --file-upload-transition-duration: var(--duration-fast); --file-upload-transition-ease: var(--ease-default); - --file-upload-disabled-opacity: 0.52; + --file-upload-disabled-opacity: var(--opacity-disabled); --file-upload-primary-solid: var(--color-primary-solid); --file-upload-primary-solid-hover: var(--color-primary-solid-hover); --file-upload-primary-text: var(--color-primary-text); @@ -2422,7 +2466,7 @@ --tag-group-remove-bg-hover: color-mix(in srgb, currentColor 12%, transparent); --tag-group-transition-duration: var(--duration-fast); --tag-group-transition-ease: var(--ease-default); - --tag-group-disabled-opacity: 0.5; + --tag-group-disabled-opacity: var(--opacity-disabled); --tag-group-primary-solid-bg: var(--color-primary-solid); --tag-group-primary-solid-bg-hover: var(--color-primary-solid-hover); --tag-group-primary-soft-bg: var(--color-primary-surface); @@ -2524,7 +2568,7 @@ --editable-submit-color: var(--color-content-on-solid); --editable-transition-duration: var(--duration-fast); --editable-transition-ease: var(--ease-default); - --editable-disabled-opacity: 0.5; + --editable-disabled-opacity: var(--opacity-disabled); --editable-primary-solid: var(--color-primary-solid); --editable-secondary-solid: var(--color-secondary-solid); --editable-neutral-solid: var(--color-neutral-solid); @@ -2553,7 +2597,7 @@ --words-font-size-lg: var(--font-size-md); --words-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color); --words-invalid-border: var(--color-risk-border); - --words-disabled-opacity: 0.5; + --words-disabled-opacity: var(--opacity-disabled); --words-font-size-xs: 0.6875rem; --words-wash-faint: 6%; --words-wash-subtle: 16%; @@ -2593,7 +2637,7 @@ --words-code-py: var(--space-0-5); --words-code-px: var(--space-1); --words-quote-px: var(--space-3); - --words-quote-border-width: var(--border-width-thick); + --words-quote-border-width: var(--border-width-medium); --words-quote-color: var(--color-content-secondary); --words-list-padding-inline-start: var(--space-5); --words-transition-duration: var(--duration-fast); @@ -2688,7 +2732,7 @@ --tags-input-delete-radius: var(--radius-full); --tags-input-transition-duration: var(--duration-fast); --tags-input-transition-ease: var(--ease-default); - --tags-input-disabled-opacity: 0.52; + --tags-input-disabled-opacity: var(--opacity-disabled); --tags-input-primary-text: var(--color-primary-text); --tags-input-secondary-text: var(--color-secondary-text); --tags-input-neutral-text: var(--color-neutral-text); @@ -2743,7 +2787,7 @@ --stepper-indicator-font-weight: var(--font-weight-semibold); --stepper-complete-color: var(--color-content-on-solid); --stepper-separator-min-size: 3rem; - --stepper-separator-thickness: var(--border-width-thick); + --stepper-separator-thickness: var(--border-width-medium); --stepper-separator-radius: var(--radius-full); --stepper-separator-bg: var(--color-border-subtle); --stepper-content-padding: var(--space-4); @@ -2763,7 +2807,7 @@ --stepper-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color); --stepper-transition-duration: var(--duration-fast); --stepper-transition-ease: var(--ease-default); - --stepper-disabled-opacity: 0.45; + --stepper-disabled-opacity: var(--opacity-disabled); --stepper-primary-solid: var(--color-primary-solid); --stepper-primary-text: var(--color-primary-text); --stepper-secondary-solid: var(--color-secondary-solid); @@ -2822,7 +2866,7 @@ --slider-tick-bg-active: var(--color-primary-solid); --slider-transition-duration: var(--duration-fast); --slider-transition-ease: var(--ease-default); - --slider-disabled-opacity: 0.56; + --slider-disabled-opacity: var(--opacity-disabled); --switch-size-xs-track-width: 26px; --switch-size-xs-track-height: 15px; --switch-size-xs-thumb-size: 11px; @@ -2863,7 +2907,7 @@ --switch-track-border-disabled: color-mix( in srgb, var(--color-border-default) 70%, var(--color-border-subtle) ); --switch-thumb-bg-disabled: color-mix( in srgb, var(--color-surface-default) 88%, var(--color-surface-raised) ); --switch-thumb-border-disabled: color-mix( in srgb, var(--color-border-subtle) 88%, var(--color-surface-overlay) ); - --switch-disabled-opacity: 0.72; + --switch-disabled-opacity: var(--opacity-disabled); --switch-thumb-bg: var(--color-surface-default); --switch-thumb-border: color-mix( in srgb, var(--color-border-default) 82%, var(--color-surface-overlay) ); --switch-thumb-icon-color: var(--color-content-muted); @@ -2945,7 +2989,7 @@ --tabs-surface-content-border: color-mix( in srgb, var(--tabs-provider-border) 84%, transparent ); --tabs-surface-content-bg: color-mix( in srgb, var(--color-surface-overlay) 92%, transparent ); --tabs-pills-content-padding-block-start: var(--space-2); - --tabs-disabled-opacity: 0.5; + --tabs-disabled-opacity: var(--opacity-disabled); --tabs-indicator-transition-duration: var(--duration-normal); --tabs-indicator-transition-ease: cubic-bezier(0.32, 0.72, 0, 1); --tabs-indicator-line-thickness: 2px; @@ -2975,7 +3019,7 @@ --toast-provider-border-width: var(--border-width); --toast-provider-radius: var(--radius-lg); --toast-provider-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo); - --toast-accent-width: 3px; + --toast-accent-width: var(--border-width-thick); --toast-transition-duration: var(--duration-fast); --toast-transition-ease: var(--ease-default); --toast-closed-offset: 10px; @@ -3077,11 +3121,11 @@ --toggle-font-family: var(--font-ui); --toggle-line-height: var(--leading-ui); --toggle-letter-spacing: var(--tracking-normal); - --toggle-font-size-xs: 10px; - --toggle-font-size-sm: 12px; - --toggle-font-size-md: 14px; - --toggle-font-size-lg: 16px; - --toggle-font-size-xl: 18px; + --toggle-font-size-xs: var(--font-size-xxs); + --toggle-font-size-sm: var(--font-size-xs); + --toggle-font-size-md: var(--font-size-sm); + --toggle-font-size-lg: var(--font-size-md); + --toggle-font-size-xl: var(--font-size-lg); --toggle-font-weight-xs: var(--font-weight-medium); --toggle-font-weight-sm: var(--font-weight-medium); --toggle-font-weight-md: var(--font-weight-medium); @@ -3151,7 +3195,7 @@ --toggle-ghost-bg: transparent; --toggle-ghost-border: transparent; --toggle-ghost-hover-border: transparent; - --toggle-disabled-opacity: 0.45; + --toggle-disabled-opacity: var(--opacity-disabled); --tooltip-content-z: 76; --tooltip-max-width: 22rem; --tooltip-padding-y-sm: var(--space-1); @@ -3290,7 +3334,7 @@ --separator-min-length: var(--space-4); --toggle-group-gap: var(--space-1); --pin-input-gap: var(--space-2); - --pin-input-disabled-opacity: 0.55; + --pin-input-disabled-opacity: var(--opacity-disabled); --pin-input-cell-size-sm: 2rem; --pin-input-cell-size-md: 2.5rem; --pin-input-cell-size-lg: 3rem; @@ -3301,7 +3345,7 @@ --pin-input-cell-font-size-xl: var(--font-size-xxl); --pin-input-cell-font-weight: 500; --pin-input-cell-radius: var(--radius-md); - --pin-input-cell-border-width: 1px; + --pin-input-cell-border-width: var(--border-width); --pin-input-cell-border-color: var(--color-border-default); --pin-input-cell-bg: var(--color-surface-default); --pin-input-cell-filled-bg: var(--color-primary-element); @@ -3324,7 +3368,7 @@ --splitter-radius: var(--radius-md); --splitter-border-width: var(--border-width); --splitter-border-color: var(--color-border-default); - --splitter-disabled-opacity: 0.6; + --splitter-disabled-opacity: var(--opacity-disabled); --splitter-panel-bg: transparent; --splitter-handle-size: var(--space-2); --splitter-handle-bg: var(--color-border-subtle); @@ -3336,13 +3380,16 @@ --splitter-grip-cross: 2px; --splitter-transition-duration: var(--duration-fast); --splitter-transition-ease: var(--ease-default); + --combobox-content-z: 80; + --drag-drop-preview-z: 99; + --navigation-menu-content-z: 50; --navigation-menu-indicator-w: 0px; --navigation-menu-indicator-x: 0px; --dropdown-menu-content-z: 80; --dropdown-menu-content-min-width: 12rem; --dropdown-menu-content-max-height: min(60vh, 22rem); --dropdown-menu-content-padding: var(--space-1); - --dropdown-menu-content-border-width: 1px; + --dropdown-menu-content-border-width: var(--border-width); --dropdown-menu-content-border-color: var(--color-border-default); --dropdown-menu-content-radius: var(--radius-md); --dropdown-menu-content-bg: var(--color-surface-overlay); @@ -3364,7 +3411,7 @@ --context-menu-content-min-width: 12rem; --context-menu-content-max-height: min(60vh, 22rem); --context-menu-content-padding: var(--space-1); - --context-menu-content-border-width: 1px; + --context-menu-content-border-width: var(--border-width); --context-menu-content-border-color: var(--color-border-default); --context-menu-content-radius: var(--radius-md); --context-menu-content-bg: var(--color-surface-overlay); @@ -5246,6 +5293,10 @@ } } +[data-container] { + container-type: inline-size; +} + :root, [data-theme='base-light'], [data-theme='light'] { @@ -5914,6 +5965,8 @@ --shadow-subtle: var(--shadow-1); --shadow-raised: var(--shadow-3); --shadow-overlay: var(--shadow-5); + --shadow-inset-subtle: inset 0 1px 2px rgb(15 23 42 / 0.08); + --shadow-inset-deep: inset 0 2px 4px rgb(15 23 42 / 0.12); } [data-theme='base-dark'], @@ -6583,4 +6636,6 @@ --shadow-subtle: var(--shadow-1); --shadow-raised: var(--shadow-3); --shadow-overlay: var(--shadow-5); + --shadow-inset-subtle: inset 0 1px 2px rgb(0 0 0 / 0.30); + --shadow-inset-deep: inset 0 2px 4px rgb(0 0 0 / 0.45); } diff --git a/src/uix/eidos/lib/config-types.ts b/src/uix/eidos/lib/config-types.ts index 49b0de3c7..60722e433 100644 --- a/src/uix/eidos/lib/config-types.ts +++ b/src/uix/eidos/lib/config-types.ts @@ -1,6 +1,7 @@ import type { ResponsiveProp, Size, SizePrimitiveKey } from './types'; import type { MotionConfig } from '$motion'; import { DENSITIES, type Density } from '$libs/density'; +import type { Breakpoint, Breakpoints } from '$libs/dom'; import { INTENTS, type Intent } from '$uix/intent'; export { SIZE_PRIMITIVE_KEYS, SIZES } from './types'; @@ -150,6 +151,25 @@ export type ControlHeightKey = Exclude; export const RADIUS_KEYS = ['none', 'sm', 'md', 'lg', 'xl', 'xxl', 'full'] as const; export type RadiusKey = (typeof RADIUS_KEYS)[number]; +// Blur scale — backdrop / frost radii. Numeric primitive (Tailwind-aligned values); +// the depth planes consume it (`--depth-{plane}-blur` → `var(--blur-*)`). No `full`. +export const BLUR_KEYS = ['none', 'sm', 'md', 'lg', 'xl', 'xxl'] as const; +export type BlurKey = (typeof BLUR_KEYS)[number]; + +// Gradient direction tokens (Tailwind's 8 compass directions, as CSS angles where +// 0deg = "to top"). The raw axis; named gradients (`gradients`) compose these. +export const GRADIENT_ANGLE_KEYS = [ + 'to-t', + 'to-tr', + 'to-r', + 'to-br', + 'to-b', + 'to-bl', + 'to-l', + 'to-tl' +] as const; +export type GradientAngleKey = (typeof GRADIENT_ANGLE_KEYS)[number]; + export const CONTAINER_WIDTH_KEYS = ['sm', 'md', 'lg', 'xl', 'xxl', 'full'] as const; export const CONTENT_WIDTH_KEYS = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl', 'prose'] as const; export const ASPECT_RATIO_KEYS = [ @@ -203,7 +223,11 @@ export interface SizePrimitive { export type SizePrimitiveSet = Record; -export const BORDER_WIDTH_KEYS = ['none', 'hairline', 'thin', 'medium', 'thick'] as const; +// Linear width ladder (Bootstrap-style 1/2/3/4 — the only reference scale that has +// the 3px components actually use; Tailwind's 1/2/4/8 skips it). The dead `hairline` +// (0.5) + old `medium` (1.5) were pruned (0 consumers); `medium` retuned to 2, `thick` +// to 3, `heavy` (4) added for strong dividers/accents. +export const BORDER_WIDTH_KEYS = ['none', 'thin', 'medium', 'thick', 'heavy'] as const; export const BORDER_STYLE_KEYS = ['solid', 'dashed', 'dotted'] as const; export type BorderWidthKey = (typeof BORDER_WIDTH_KEYS)[number]; @@ -225,23 +249,63 @@ export interface FocusRingPrimitiveSet { export const SHADOW_PHYSICAL_KEYS = ['1', '2', '3', '4', '5', '6'] as const; export const SHADOW_SEMANTIC_KEYS = ['none', 'subtle', 'raised', 'overlay'] as const; -export const SHADOW_KEYS = [...SHADOW_SEMANTIC_KEYS, ...SHADOW_PHYSICAL_KEYS] as const; +// Inner-shadow (recessed/well) tier — soft, mode-aware, drawn `inset`. Distinct +// from the outer drop shadows above and from inset-RINGS (crisp `inset 0 0 0 Npx`, +// a separate axis). Consumed by the `recessed` depth plane + pressed/well states. +export const SHADOW_INSET_KEYS = ['inset-subtle', 'inset-deep'] as const; +export const SHADOW_KEYS = [ + ...SHADOW_SEMANTIC_KEYS, + ...SHADOW_PHYSICAL_KEYS, + ...SHADOW_INSET_KEYS +] as const; export type ShadowPhysicalKey = (typeof SHADOW_PHYSICAL_KEYS)[number]; export type ShadowSemanticKey = (typeof SHADOW_SEMANTIC_KEYS)[number]; +export type ShadowInsetKey = (typeof SHADOW_INSET_KEYS)[number]; export type ShadowKey = (typeof SHADOW_KEYS)[number]; export type ShadowScale = Record; -export const OPACITY_KEYS = [ +// Two-tier scale (mirrors shadow: numeric `--shadow-1..6` + semantic +// `--shadow-{subtle,raised,overlay}`). The numeric tier (Tailwind step-5) gives +// the fine low-end granularity ethereal / glass interfaces need for layered +// translucency; the semantic tier names the perceptual roles consumed by recipes. +export const OPACITY_NUMERIC_KEYS = [ '0', - 'muted', + '5', + '10', + '15', + '20', + '25', + '30', + '35', + '40', + '45', + '50', + '55', + '60', + '65', + '70', + '75', + '80', + '85', + '90', + '95', + '100' +] as const; +export const OPACITY_SEMANTIC_KEYS = [ + 'ghost', 'disabled', 'scrim', + 'muted', 'overlay', - 'hover', + 'subtle', 'press', + 'hover', 'full' ] as const; +export const OPACITY_KEYS = [...OPACITY_NUMERIC_KEYS, ...OPACITY_SEMANTIC_KEYS] as const; +export type OpacityNumericKey = (typeof OPACITY_NUMERIC_KEYS)[number]; +export type OpacitySemanticKey = (typeof OPACITY_SEMANTIC_KEYS)[number]; export type OpacityKey = (typeof OPACITY_KEYS)[number]; export type OpacityPrimitiveSet = Record; @@ -475,9 +539,25 @@ export interface RecipeComposition { * and `tokenEntries` in the recipe contract test). */ type RecipeCompositionMap = Readonly>; + +/** + * Container-query overrides — per-breakpoint token overrides emitted inside + * `@container (min-width: px) { [data-{component}] { … } }`. The + * breakpoints are the SAME configured source as `@media` (`EidosConfig.breakpoints`), + * so a container-relative override tracks the runtime `ResponsiveProp` scale. + * CSS forbids `var()` in `@container` conditions, so the generator emits literal + * px — this is the only way to keep the condition synced with the configured set. + * Reserved sibling key `container`, like `composition`. The element opts in to + * being a query container via `data-container` (`container-type: inline-size`). + */ +export type ContainerBreakpoint = Exclude; +export type RecipeContainerQueries = Partial< + Record>> +>; export type RecipeTokenMap = Readonly<{ - [name: string]: RecipeTokenValue | RecipeCompositionMap | undefined; + [name: string]: RecipeTokenValue | RecipeCompositionMap | RecipeContainerQueries | undefined; composition?: RecipeCompositionMap; + container?: RecipeContainerQueries; }>; export type RecipeTokenSet = Readonly>; @@ -722,6 +802,12 @@ export interface PrimitiveSet { readonly space?: ScaleMap; readonly controlHeight?: Record; readonly radius?: Record; + readonly blur?: Record; + // Gradient direction tokens (`--gradient-angle-*`) + named gradients + // (`--gradient-*`). Named gradients compose role/surface color tokens, so they + // retint per theme + mode. Extensible via config (the "open cage"). + readonly gradientAngle?: Record; + readonly gradients?: Record; readonly border?: BorderPrimitiveSet; readonly focusRing?: FocusRingPrimitiveSet; readonly layout?: LayoutPrimitiveSet; @@ -762,6 +848,15 @@ export interface EidosConfig { readonly semantics: SemanticSet; readonly themes?: ThemeMap; readonly recipes?: RecipeTokenSet; + /** + * Responsive breakpoints (px), the SINGLE source of truth shared with the + * runtime responsive service. The developer configures them on + * `ActiveDom`/`ActiveUix`; `ActiveEidos` threads `dom.breakpoints.current` + * here so the generated `@media` blocks + `--breakpoint-*` tokens match the + * runtime `ResponsiveProp` resolution. Absent (build-time / no service) → + * the generator falls back to the canonical defaults. + */ + readonly breakpoints?: Breakpoints; /** * Motion system: named `@keyframes` + presets (the composition layer). * Distinct from `primitives.motion` (duration/ease/distance/scale tokens). diff --git a/src/uix/eidos/lib/contract.ts b/src/uix/eidos/lib/contract.ts index 45633197e..86b287ea3 100644 --- a/src/uix/eidos/lib/contract.ts +++ b/src/uix/eidos/lib/contract.ts @@ -515,11 +515,11 @@ function appendRecipeContractTokens( ): void { for (const [component, recipeTokens] of Object.entries(recipes ?? {})) { for (const name of Object.keys(recipeTokens)) { - // Reserved TSC v2.2 sibling keys (currently `composition`) are - // not tokens — they're structural blocks describing cross-recipe - // overrides. Skip them so they don't leak into the public CSS - // contract as fake `--{component}-composition` knobs. - if (name === 'composition') continue + // Reserved sibling keys (`composition` — cross-recipe overrides; + // `container` — container-query overrides) are not tokens but + // structural blocks. Skip them so they don't leak into the public + // CSS contract as fake `--{component}-{composition,container}` knobs. + if (name === 'composition' || name === 'container') continue // Private tokens (key starts with '_') are recipe-internal // machinery emitted as `--_{c}-{rest}` — not part of the // public CSS contract. External consumers should not see diff --git a/src/uix/eidos/lib/primitives/static.ts b/src/uix/eidos/lib/primitives/static.ts index 43df8a171..444867945 100644 --- a/src/uix/eidos/lib/primitives/static.ts +++ b/src/uix/eidos/lib/primitives/static.ts @@ -47,13 +47,49 @@ export const STATIC_RADIUS = { full: '9999px' } as const +// Blur scale — backdrop / frost radii. Numeric primitive, values aligned to +// Tailwind's blur scale (xs→sm 4 · sm→md 8 · md→lg 12 · lg→xl 16 · xl→xxl 24). +// The depth planes consume it (`--depth-{plane}-blur` → `var(--blur-*)`); a +// "cristal" theme retunes blur+alpha per plane on top. Density-scaled (× --scaling) +// at generation, like the icon scale. See THEMING.md "Escala de blur". +export const STATIC_BLUR = { + none: '0px', + sm: '4px', + md: '8px', + lg: '12px', + xl: '16px', + xxl: '24px' +} as const + +// Gradient direction tokens — Tailwind's 8 compass directions as CSS angles +// (0deg = "to top"). The raw axis; named gradients compose these. +export const STATIC_GRADIENT_ANGLE = { + 'to-t': '0deg', + 'to-tr': '45deg', + 'to-r': '90deg', + 'to-br': '135deg', + 'to-b': '180deg', + 'to-bl': '225deg', + 'to-l': '270deg', + 'to-tl': '315deg' +} as const + +// Named gradients — the semantic layer. Compose direction tokens + role/surface +// color tokens, so they retint per theme + mode. The base ships ONE (`shimmer`, +// the loading sweep consumed by `image`); apps/themes add more via config (open +// cage), like Tailwind ships directions but no named gradients of its own. +export const STATIC_GRADIENTS = { + shimmer: + 'linear-gradient(var(--gradient-angle-to-r), var(--color-surface-raised) 0%, color-mix(in srgb, var(--color-surface-default) 70%, var(--color-surface-overlay)) 50%, var(--color-surface-raised) 100%)' +} as const + export const STATIC_BORDER = { width: { none: '0px', - hairline: '0.5px', thin: '1px', - medium: '1.5px', - thick: '2px' + medium: '2px', + thick: '3px', + heavy: '4px' }, style: { solid: 'solid', @@ -205,13 +241,38 @@ export const STATIC_ICON = { } as const export const STATIC_OPACITY = { + // Numeric tier (Tailwind step-5) — fine granularity for ethereal / glass layering. '0': 0, - muted: 0.72, - disabled: 0.48, - scrim: 0.42, - overlay: 0.66, - hover: 0.92, - press: 0.84, + '5': 0.05, + '10': 0.1, + '15': 0.15, + '20': 0.2, + '25': 0.25, + '30': 0.3, + '35': 0.35, + '40': 0.4, + '45': 0.45, + '50': 0.5, + '55': 0.55, + '60': 0.6, + '65': 0.65, + '70': 0.7, + '75': 0.75, + '80': 0.8, + '85': 0.85, + '90': 0.9, + '95': 0.95, + '100': 1, + // Semantic tier — the perceptual roles recipes consume (de-emphasis ladder + + // veils + interaction). Disabled = 0.4 (modern standard, ≈ Material 38%). + ghost: 0.3, + disabled: 0.4, + scrim: 0.45, + muted: 0.65, + overlay: 0.65, + subtle: 0.8, + press: 0.85, + hover: 0.9, full: 1 } as const @@ -242,19 +303,19 @@ export const STATIC_DEPTH: DepthPrimitiveSet = { surface: 'var(--color-surface-overlay)', shadow: 'var(--shadow-overlay)', halo: 'inset 0 1px 0 0 color-mix(in oklab, white 7%, transparent)', - blur: '10px', + blur: 'var(--blur-lg)', z: 'var(--z-index-popover)' }, modal: { surface: 'var(--color-surface-overlay)', shadow: 'var(--shadow-overlay)', halo: 'inset 0 1px 0 0 color-mix(in oklab, white 8%, transparent)', - blur: '16px', + blur: 'var(--blur-xl)', z: 'var(--z-index-modal)' }, recessed: { surface: 'var(--color-surface-muted)', - shadow: 'inset 0 1px 2px color-mix(in srgb, var(--color-neutral-contrast) 12%, transparent)', + shadow: 'var(--shadow-inset-subtle)', z: 'var(--z-index-base)' } } @@ -347,6 +408,9 @@ export const STATIC_PRIMITIVES: Pick< | 'space' | 'controlHeight' | 'radius' + | 'blur' + | 'gradientAngle' + | 'gradients' | 'border' | 'focusRing' | 'layout' @@ -363,6 +427,9 @@ export const STATIC_PRIMITIVES: Pick< space: STATIC_SPACE, controlHeight: STATIC_CONTROL_HEIGHT, radius: STATIC_RADIUS, + blur: STATIC_BLUR, + gradientAngle: STATIC_GRADIENT_ANGLE, + gradients: STATIC_GRADIENTS, border: STATIC_BORDER, focusRing: STATIC_FOCUS_RING, layout: STATIC_LAYOUT, diff --git a/src/uix/eidos/lib/primitives/typography.ts b/src/uix/eidos/lib/primitives/typography.ts index 220abb840..69d4ff762 100644 --- a/src/uix/eidos/lib/primitives/typography.ts +++ b/src/uix/eidos/lib/primitives/typography.ts @@ -172,12 +172,16 @@ export const STATIC_TYPOGRAPHY: TypographyPrimitiveSet = { } }, // Tracking scale (letter-spacing) → --tracking-{key}. (Fase 3) + // `caps` (0.04em) is the canonical micro-tracking for UPPERCASE labels (the + // dominant raw-value pattern across menus/headings); `widest` (0.1em) for wide caps. tracking: { tighter: '-0.04em', tight: '-0.02em', normal: '0', wide: '0.02em', - wider: '0.05em' + caps: '0.04em', + wider: '0.05em', + widest: '0.1em' }, // Leading scale (line-height) → --leading-{key}. leading: { diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index a7d3c3c64..e35bef7de 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -144,18 +144,18 @@ export const THEME_BASE_RECIPE_TOKENS = { 'size-xl': '64px', 'size-xxl': '96px', 'font-family': 'var(--font-ui)', - 'font-size-xs': '10px', - 'font-size-sm': '12px', - 'font-size-md': '14px', - 'font-size-lg': '16px', - 'font-size-xl': '20px', - 'font-size-xxl': '28px', + 'font-size-xs': 'var(--font-size-xxs)', + 'font-size-sm': 'var(--font-size-xs)', + 'font-size-md': 'var(--font-size-sm)', + 'font-size-lg': 'var(--font-size-md)', + 'font-size-xl': 'var(--font-size-lg)', + 'font-size-xxl': 'var(--font-size-xl)', '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': '2px', + '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)', @@ -215,7 +215,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'color-custom-contrast': 'white', 'ring-color-custom': 'currentColor', // ── Badge ───────────────────────────────────────────────────────── - 'badge-border-width': '2px', + 'badge-border-width': 'var(--border-width-medium)', 'badge-color-custom': 'currentColor', 'badge-color-custom-contrast': 'white', // ── AvatarGroup ────────────────────────────────────────────────── @@ -224,7 +224,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'group-gap-md': 'var(--space-3)', 'group-gap-lg': 'var(--space-4)', 'group-overlap': '-0.6em', - 'group-carve-width': '2px', + 'group-carve-width': 'var(--border-width-medium)', 'group-carve-color': 'var(--color-surface-default)', 'group-reverse-z-max': '20', 'group-max': '99', @@ -518,15 +518,15 @@ export const THEME_BASE_RECIPE_TOKENS = { 'footer-padding-start': 'var(--space-3)', 'footer-border-color': 'var(--color-border-subtle)', // ── Animations ──────────────────────────────────────────────────── - 'emerge-duration': '320ms', + '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': '0.985', 'press-duration': '80ms', - 'selected-ring-width': '2px', - 'disabled-opacity': '0.6', + 'selected-ring-width': 'var(--border-width-medium)', + 'disabled-opacity': 'var(--opacity-disabled)', // ── Custom color (set inline by consumer via style) ─────────────── // The Svelte file emits `style="--card-color-custom: ;"` // when the consumer passes a non-canonical CSS color. Declared @@ -676,7 +676,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) )', 'border-disabled': 'color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) )', - 'disabled-opacity': '0.72', + 'disabled-opacity': 'var(--opacity-disabled)', 'indicator-color': 'var(--color-content-on-solid)', 'indicator-color-disabled': 'color-mix( in srgb, var(--color-content-on-solid) 82%, var(--color-surface-default) )', @@ -729,13 +729,13 @@ export const THEME_BASE_RECIPE_TOKENS = { 'content-padding': 'var(--space-3)', 'transition-duration': 'var(--duration-normal)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.5' + 'disabled-opacity': 'var(--opacity-disabled)' }, dialog: { 'trigger-font-size': 'var(--font-size-sm)', 'overlay-bg': 'var(--color-overlay)', 'overlay-opacity': '62%', - 'overlay-blur': '10px', + 'overlay-blur': 'var(--blur-lg)', 'overlay-z': '70', 'content-bg': 'var(--color-surface-overlay)', 'content-color': 'var(--color-content-primary)', @@ -849,7 +849,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'error-color': 'var(--color-risk-text)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, form: { 'gap-xs': 'var(--space-3)', @@ -905,11 +905,11 @@ export const THEME_BASE_RECIPE_TOKENS = { 'widget-border-width': 'var(--border-width)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, drawer: { 'overlay-bg': 'var(--color-overlay)', - 'overlay-blur': '10px', + 'overlay-blur': 'var(--blur-lg)', 'overlay-opacity': '62%', 'overlay-z': '72', 'inline-z': '64', @@ -929,7 +929,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'content-shadow': 'var(--depth-overlay-shadow), var(--depth-overlay-halo)', 'content-shadow-dragging': 'var(--shadow-overlay-strong, var(--depth-overlay-shadow)), var(--depth-overlay-halo)', - 'content-ring-dragging-width': '2px', + 'content-ring-dragging-width': 'var(--border-width-medium)', 'content-ring-dragging-color': 'color-mix( in srgb, var(--color-content-secondary) 32%, transparent )', 'content-ring-dragging-offset': '2px', @@ -995,8 +995,6 @@ export const THEME_BASE_RECIPE_TOKENS = { 'fade-ease': 'var(--ease-out)', // ── Skeleton placeholder shimmer ────────────────────────────────── 'placeholder-base': 'var(--color-surface-raised)', - 'placeholder-highlight': - 'color-mix(in srgb, var(--color-surface-default) 70%, var(--color-surface-overlay))', 'shimmer-duration': '1.8s', // ── Custom placeholder color (set inline by Svelte file) ───────── 'placeholder-color-custom': 'var(--color-neutral-track)', @@ -1108,7 +1106,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'control-disabled-opacity': '0.48', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, 'float-panel': { bg: 'var(--color-surface-overlay)', @@ -1258,7 +1256,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'segment-hover-bg': 'color-mix(in srgb, var(--color-content-primary) 6%, transparent)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, 'time-field': { 'stack-gap': 'var(--space-1-5)', @@ -1312,7 +1310,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'segment-focus-shadow': 'inset 0 0 0 var(--border-width) var(--_time-field-accent-border)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, 'color-field': { 'stack-gap': 'var(--space-1-5)', @@ -1375,7 +1373,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'format-select-px': 'var(--space-1)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, 'color-picker': { // Popover content shell @@ -1417,7 +1415,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'area-border': 'var(--color-border-subtle)', 'area-thumb-size': '16px', 'area-thumb-radius': 'var(--radius-full)', - 'area-thumb-border-width': '2px', + '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)', @@ -1465,7 +1463,7 @@ export const THEME_BASE_RECIPE_TOKENS = { // Misc 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, button: { // Size tokens @@ -1515,7 +1513,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', 'spinner-duration': '720ms', - 'disabled-opacity': '0.5', + '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 @@ -1738,7 +1736,22 @@ export const THEME_BASE_RECIPE_TOKENS = { '0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--_calendar-accent-border)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.52' + '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(--control-height-md) * 2.6)', + 'cell-min-block-md': 'calc(var(--control-height-md) * 3.2)', + 'cell-min-block-lg': 'calc(var(--control-height-md) * 4)', + 'num-row': 'var(--space-7)', + 'lane-height-sm': 'var(--space-5)', + 'lane-height-md': 'var(--space-6)', + 'lane-height-lg': 'var(--space-7)', + 'focus-shadow': + 'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)', + 'disabled-opacity': 'var(--opacity-disabled)' }, pagination: { 'gap-xs': 'var(--space-0-5)', @@ -1777,7 +1790,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, popover: { 'content-z': '75', @@ -1920,7 +1933,7 @@ export const THEME_BASE_RECIPE_TOKENS = { '0 0 0 var(--radio-group-focus-ring-offset) var(--radio-group-focus-ring-surface), 0 0 0 calc(var(--radio-group-focus-ring-offset) + var(--radio-group-focus-ring-width)) var(--radio-group-focus-ring-color)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.72', + '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)', @@ -1969,6 +1982,8 @@ export const THEME_BASE_RECIPE_TOKENS = { '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)' }, @@ -2028,7 +2043,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'focus-ring': 'var(--radio-group-focus-ring)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.6' + 'disabled-opacity': 'var(--opacity-disabled)' }, 'rating-group': { 'gap-xs': 'var(--space-0-5)', @@ -2057,7 +2072,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, 'search-field': { 'height-xs': 'var(--control-height-xs)', @@ -2111,7 +2126,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'loading-indicator-duration': '900ms', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, 'password-field': { 'height-xs': 'var(--control-height-xs)', @@ -2174,7 +2189,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'strength-radius': 'var(--radius-sm)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, select: { 'stack-gap': 'var(--space-2)', @@ -2279,7 +2294,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'item-bg-highlighted': 'var(--color-surface-raised)', 'item-color-highlighted': 'var(--color-content-primary)', 'item-indicator-size': 'var(--icon-size-sm)', - 'item-focus-ring-width': '2px', + 'item-focus-ring-width': 'var(--border-width-medium)', 'item-description-mt': '0.125rem', 'item-description-color': 'var(--color-content-muted)', 'item-description-font-size': '0.85em', @@ -2408,7 +2423,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'separator-radius': 'var(--radius-full)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.48' + 'disabled-opacity': 'var(--opacity-disabled)' }, 'file-upload': { 'gap-sm': 'var(--space-2)', @@ -2471,7 +2486,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'progress-track-bg': 'var(--color-neutral-track)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.52', + '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)', @@ -2622,7 +2637,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'remove-bg-hover': 'color-mix(in srgb, currentColor 12%, transparent)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.5', + 'disabled-opacity': 'var(--opacity-disabled)', 'primary-solid-bg': 'var(--color-primary-solid)', 'primary-solid-bg-hover': 'var(--color-primary-solid-hover)', 'primary-soft-bg': 'var(--color-primary-surface)', @@ -2816,7 +2831,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'submit-color': 'var(--color-content-on-solid)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.5', + 'disabled-opacity': 'var(--opacity-disabled)', 'primary-solid': 'var(--color-primary-solid)', 'secondary-solid': 'var(--color-secondary-solid)', 'neutral-solid': 'var(--color-neutral-solid)', @@ -2884,7 +2899,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'focus-shadow': 'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)', 'invalid-border': 'var(--color-risk-border)', - 'disabled-opacity': '0.5', + 'disabled-opacity': 'var(--opacity-disabled)', // Smallest doc-chrome size (breadcrumb, cell labels) — one step under `sm`. 'font-size-xs': '0.6875rem', // Wash scale: ONE canonical set of color-mix opacities for the editor's @@ -2944,7 +2959,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'code-py': 'var(--space-0-5)', 'code-px': 'var(--space-1)', 'quote-px': 'var(--space-3)', - 'quote-border-width': 'var(--border-width-thick)', + 'quote-border-width': 'var(--border-width-medium)', 'quote-color': 'var(--color-content-secondary)', 'list-padding-inline-start': 'var(--space-5)', 'transition-duration': 'var(--duration-fast)', @@ -3044,7 +3059,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'delete-radius': 'var(--radius-full)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.52', + 'disabled-opacity': 'var(--opacity-disabled)', 'primary-text': 'var(--color-primary-text)', 'secondary-text': 'var(--color-secondary-text)', 'neutral-text': 'var(--color-neutral-text)', @@ -3141,7 +3156,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'indicator-font-weight': 'var(--font-weight-semibold)', 'complete-color': 'var(--color-content-on-solid)', 'separator-min-size': '3rem', - 'separator-thickness': 'var(--border-width-thick)', + 'separator-thickness': 'var(--border-width-medium)', 'separator-radius': 'var(--radius-full)', 'separator-bg': 'var(--color-border-subtle)', 'content-padding': 'var(--space-4)', @@ -3162,7 +3177,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.45', + 'disabled-opacity': 'var(--opacity-disabled)', 'primary-solid': 'var(--color-primary-solid)', 'primary-text': 'var(--color-primary-text)', 'secondary-solid': 'var(--color-secondary-solid)', @@ -3270,7 +3285,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'tick-bg-active': 'var(--color-primary-solid)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', - 'disabled-opacity': '0.56' + 'disabled-opacity': 'var(--opacity-disabled)' }, switch: { 'size-xs-track-width': '26px', @@ -3318,7 +3333,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'color-mix( in srgb, var(--color-surface-default) 88%, var(--color-surface-raised) )', 'thumb-border-disabled': 'color-mix( in srgb, var(--color-border-subtle) 88%, var(--color-surface-overlay) )', - 'disabled-opacity': '0.72', + '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) )', @@ -3429,7 +3444,7 @@ export const THEME_BASE_RECIPE_TOKENS = { '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': '0.5', + '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', @@ -3465,7 +3480,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'provider-border-width': 'var(--border-width)', 'provider-radius': 'var(--radius-lg)', 'provider-shadow': 'var(--depth-overlay-shadow), var(--depth-overlay-halo)', - 'accent-width': '3px', + 'accent-width': 'var(--border-width-thick)', 'transition-duration': 'var(--duration-fast)', 'transition-ease': 'var(--ease-default)', 'closed-offset': '10px', @@ -3581,11 +3596,11 @@ export const THEME_BASE_RECIPE_TOKENS = { 'font-family': 'var(--font-ui)', 'line-height': 'var(--leading-ui)', 'letter-spacing': 'var(--tracking-normal)', - 'font-size-xs': '10px', - 'font-size-sm': '12px', - 'font-size-md': '14px', - 'font-size-lg': '16px', - 'font-size-xl': '18px', + 'font-size-xs': 'var(--font-size-xxs)', + 'font-size-sm': 'var(--font-size-xs)', + 'font-size-md': 'var(--font-size-sm)', + 'font-size-lg': 'var(--font-size-md)', + 'font-size-xl': 'var(--font-size-lg)', 'font-weight-xs': 'var(--font-weight-medium)', 'font-weight-sm': 'var(--font-weight-medium)', 'font-weight-md': 'var(--font-weight-medium)', @@ -3806,7 +3821,7 @@ export const THEME_BASE_RECIPE_TOKENS = { value: 'color-mix( in srgb, var(--toggle-palette-border) 88%, transparent )', scope: 'host' }, - 'disabled-opacity': '0.45' + 'disabled-opacity': 'var(--opacity-disabled)' }, tooltip: { 'content-z': '76', @@ -4097,7 +4112,7 @@ export const THEME_BASE_RECIPE_TOKENS = { // ───────────────────────────────────────────────────────────────────── 'pin-input': { gap: 'var(--space-2)', - 'disabled-opacity': '0.55', + 'disabled-opacity': 'var(--opacity-disabled)', 'cell-size-sm': '2rem', 'cell-size-md': '2.5rem', 'cell-size-lg': '3rem', @@ -4108,7 +4123,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'cell-font-size-xl': 'var(--font-size-xxl)', 'cell-font-weight': '500', 'cell-radius': 'var(--radius-md)', - 'cell-border-width': '1px', + 'cell-border-width': 'var(--border-width)', 'cell-border-color': 'var(--color-border-default)', 'cell-bg': 'var(--color-surface-default)', 'cell-filled-bg': 'var(--color-primary-element)', @@ -4143,7 +4158,7 @@ export const THEME_BASE_RECIPE_TOKENS = { radius: 'var(--radius-md)', 'border-width': 'var(--border-width)', 'border-color': 'var(--color-border-default)', - 'disabled-opacity': '0.6', + 'disabled-opacity': 'var(--opacity-disabled)', 'panel-bg': 'transparent', 'handle-size': 'var(--space-2)', 'handle-bg': 'var(--color-border-subtle)', @@ -4161,7 +4176,23 @@ export const THEME_BASE_RECIPE_TOKENS = { // NavigationMenu — soma writes the active-trigger rect into these // indicator vars; the recipe paints the underline. // ───────────────────────────────────────────────────────────────────── + // Combobox — floating result list. Same overlay micro-band as `select` / + // `dropdown-menu` (the soma floating layer mirrors this computed z-index onto + // the positioner wrapper, so it must stay a low, themeable value — not the + // global `--z-index-*` scale). + combobox: { + 'content-z': '80' + }, + + // DragDrop — the drag preview/ghost that tracks the cursor. Floats above + // local content during an active drag. + 'drag-drop': { + 'preview-z': '99' + }, + 'navigation-menu': { + // Inline (non-portaled) dropdown panel: stacks above normal page flow. + 'content-z': '50', 'indicator-w': '0px', 'indicator-x': '0px' }, @@ -4179,7 +4210,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'content-min-width': '12rem', 'content-max-height': 'min(60vh, 22rem)', 'content-padding': 'var(--space-1)', - 'content-border-width': '1px', + 'content-border-width': 'var(--border-width)', 'content-border-color': 'var(--color-border-default)', 'content-radius': 'var(--radius-md)', 'content-bg': 'var(--color-surface-overlay)', @@ -4207,7 +4238,7 @@ export const THEME_BASE_RECIPE_TOKENS = { 'content-min-width': '12rem', 'content-max-height': 'min(60vh, 22rem)', 'content-padding': 'var(--space-1)', - 'content-border-width': '1px', + 'content-border-width': 'var(--border-width)', 'content-border-color': 'var(--color-border-default)', 'content-radius': 'var(--radius-md)', 'content-bg': 'var(--color-surface-overlay)', diff --git a/src/uix/eidos/lib/render-css.ts b/src/uix/eidos/lib/render-css.ts index e2d33c832..af76767a4 100644 --- a/src/uix/eidos/lib/render-css.ts +++ b/src/uix/eidos/lib/render-css.ts @@ -32,6 +32,7 @@ import { type LayoutPrimitiveSet, type PrimitiveSet, type RecipeComposition, + type RecipeContainerQueries, type RecipeTokenSet, type RecipeTokenValue, type RecipeTokenDeclaration, @@ -144,6 +145,16 @@ export function renderStaticCss(options: EidosConfig): string { const { primitives } = options const declarations: string[] = [] + // Responsive breakpoints — the configured set (threaded from the runtime + // service via `ActiveEidos`, which reads `dom.breakpoints.current`) merged over + // the fallback defaults. Emitted as `--breakpoint-*` tokens AND used for the + // responsive `@media` blocks below, so both track ONE source instead of a + // frozen const drifting from the developer-configured breakpoints. + const breakpoints = { ...STYLE_BREAKPOINT_WIDTHS, ...options.breakpoints } + for (const bp of STYLE_BREAKPOINT_ORDER) { + declarations.push(cssVar(`breakpoint-${bp}`, `${breakpoints[bp]}px`)) + } + // Spacing + control heights scale with BOTH the active density and the global // `--scaling` zoom (multiplicative): `calc(value * density-scale * scaling)`. // The `[data-density]` / `[data-scaling]` blocks rebind those vars. At @@ -213,6 +224,19 @@ export function renderStaticCss(options: EidosConfig): string { appendRecordDeclarations(declarations, 'icon-stroke-width', primitives.icon.strokeWidth) } + if (primitives.blur) { + // Blur radii scale with --scaling (zoom-consistent), like icon-size. + appendScaledMetricDeclarations(declarations, 'blur', primitives.blur) + } + + if (primitives.gradientAngle) { + appendRecordDeclarations(declarations, 'gradient-angle', primitives.gradientAngle) + } + if (primitives.gradients) { + // Named gradients compose role/surface tokens → mode-aware via the references. + appendRecordDeclarations(declarations, 'gradient', primitives.gradients) + } + appendRecordDeclarations(declarations, 'opacity', primitives.opacity) appendRecordDeclarations(declarations, 'z-index', primitives.zIndex) @@ -238,7 +262,7 @@ export function renderStaticCss(options: EidosConfig): string { } appendTransitionAliasDeclarations(declarations, options) - const scopedRecipeBlocks = appendRecipeDeclarations(declarations, options.recipes) + const scopedRecipeBlocks = appendRecipeDeclarations(declarations, options.recipes, breakpoints) const depthBlocks = primitives.depth ? renderDepthBlocks(primitives.depth) : [] const shapeBlocks = primitives.shape ? renderShapeBlocks(primitives.shape) : [] @@ -261,7 +285,7 @@ export function renderStaticCss(options: EidosConfig): string { for (const bp of STYLE_BREAKPOINT_ORDER) { const lines = styleResponsive[bp] if (!lines || lines.length === 0) continue - const width = STYLE_BREAKPOINT_WIDTHS[bp] + const width = breakpoints[bp] const rootBlock = renderBlock(':root', lines) blocks.push(`@media (min-width: ${width}px) {\n${indentBlock(rootBlock)}\n}`) } @@ -291,6 +315,11 @@ export function renderStaticCss(options: EidosConfig): string { blocks.push(renderForcedColorsBlock()) blocks.push(renderPrefersContrastBlock()) + // Container-query opt-in: marking an element `data-container` makes it a query + // container, so its descendants' recipe `@container` overrides resolve against + // its inline size. (The `@container` blocks themselves are emitted per-recipe.) + blocks.push(renderBlock('[data-container]', ['container-type: inline-size;'])) + return blocks.join('\n\n') } @@ -565,6 +594,13 @@ function appendBorderDeclarations(declarations: string[], border: BorderPrimitiv declarations.push( cssVar('border', 'var(--border-width) var(--border-style) var(--color-border-default)') ) + // Inset-ring width — default for crisp inner rings drawn as `box-shadow` + // (`inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color)`). Defaults to the + // `medium` (2px) border step; a theme retunes all inset rings here, a component overrides + // per-use. The full expression lives at the use site (CSS resolves nested var() in + // the declaring scope, so a single pre-baked `--ring-inset` shorthand can't pick up + // per-element color/width). Distinct from the soft `--shadow-inset-*` tier. + declarations.push(cssVar('ring-inset-width', 'var(--border-width-medium)')) } function appendLayoutDeclarations(declarations: string[], layout: LayoutPrimitiveSet): void { @@ -980,9 +1016,12 @@ function renderShapeBlocks(shape: ShapePrimitiveSet): string[] { } /** - * Breakpoint min-width thresholds matching `$libs/dom/responsive`. - * `base` is the implicit 0 fallback — emitted into `:root`, not as a - * media query. + * Fallback breakpoint min-width thresholds (mirrors `$libs/dom/responsive` + * `BREAKPOINTS_DEFAULT`). Used only when `options.breakpoints` is absent + * (build-time / no runtime service). At runtime `ActiveEidos` passes the + * developer-configured `dom.breakpoints.current`, which overrides these — so the + * emitted `--breakpoint-*` tokens + `@media` blocks track the live source. + * `base` is the implicit 0 fallback — not emitted as a media query. */ const STYLE_BREAKPOINT_WIDTHS: Record = { sm: 480, @@ -1216,7 +1255,8 @@ function appendTransitionAliasDeclarations( */ function appendRecipeDeclarations( declarations: string[], - recipes: RecipeTokenSet | undefined + recipes: RecipeTokenSet | undefined, + breakpoints: Record ): string[] { if (!recipes) return [] @@ -1230,6 +1270,9 @@ function appendRecipeDeclarations( // this iteration so it composes onto the same `blocks` array. const composition = (tokens as { composition?: Readonly> }) .composition + // `container` is the reserved sibling key for container-query overrides + // (emitted inside `@container`); stripped before normalizing, like `composition`. + const container = (tokens as { container?: RecipeContainerQueries }).container const tokensOnly = stripCompositionKey(tokens) // Phase 1: normalize every token to a list of declarations. @@ -1294,6 +1337,13 @@ function appendRecipeDeclarations( if (composition) { blocks.push(...emitComposition(component, composition)) } + + // Phase 6: container queries. Per-breakpoint token overrides emitted + // inside `@container (min-width: px)`, scoped to the + // component. Uses the SAME configured breakpoints as `@media`. + if (container) { + blocks.push(...emitContainerQueries(component, container, breakpoints)) + } } if (allViolations.length) { @@ -1315,12 +1365,44 @@ function stripCompositionKey( ): Readonly> { const out: Record = {} for (const [key, value] of Object.entries(tokens)) { - if (key === 'composition') continue + if (key === 'composition' || key === 'container') continue out[key] = value as RecipeTokenValue } return out } +/** + * Emit `@container` blocks for a recipe's container-query overrides. + * + * For each breakpoint with token entries, emits + * `@container (min-width: px) { [data-{c}] { --{c}-{token}: value } }`. + * The breakpoints are the SAME configured source as `@media` (threaded from + * `EidosConfig.breakpoints`), so a container override tracks the runtime scale. + * CSS forbids `var()` in `@container` conditions, so the px is emitted literally — + * the generator is the only place this can stay synced with the configured set. + * + * The queried element (`[data-{c}]`) responds to its nearest ANCESTOR query + * container — an ancestor opts in via `data-container` (`container-type: inline-size`). + */ +function emitContainerQueries( + component: string, + container: RecipeContainerQueries, + breakpoints: Record +): string[] { + const blocks: string[] = [] + for (const bp of STYLE_BREAKPOINT_ORDER) { + const tokens = container[bp] + if (!tokens) continue + const lines = Object.entries(tokens).map(([name, value]) => + cssVar(name.startsWith('_') ? `_${component}-${name.slice(1)}` : `${component}-${name}`, value) + ) + if (!lines.length) continue + const inner = renderBlock(`[data-${component}]`, lines) + blocks.push(`@container (min-width: ${breakpoints[bp]}px) {\n${indentBlock(inner)}\n}`) + } + return blocks +} + /** * Emit blocks for a recipe's cross-recipe composition (TSC v2.2). * diff --git a/src/uix/eidos/lib/themes/base.ts b/src/uix/eidos/lib/themes/base.ts index d3c027302..078f2ae58 100644 --- a/src/uix/eidos/lib/themes/base.ts +++ b/src/uix/eidos/lib/themes/base.ts @@ -27,6 +27,9 @@ export const THEME_BASE_COLOR_ROLES: ColorRoleMap = { // and the gray neutral. Indigo is purple's cool ~60° neighbor (Material 3's // tertiary-hue rule) and sits in a hue band no intent occupies, so it reads as // hierarchy, not a signal. (Was `gray` — collided with neutral.) + // RESERVED (2026-06-15 audit): defined + emitted (`--color-tertiary-*`) but no + // component consumes it yet — a deliberate third-hierarchy slot apps/themes can + // reach for. Not dead code; not to be removed. tertiary: 'indigo', // `gray-9` (el sólido neutral) es un gris medio: ni blanco ni negro // contrastan por defecto. El slot `contrast` se fija al step 12 — casi-negro @@ -459,7 +462,10 @@ export const THEME_BASE_LIGHT_SHADOW: ShadowScale = { '3': '0 8px 24px rgb(15 23 42 / 0.12)', '4': '0 14px 36px rgb(15 23 42 / 0.15)', '5': '0 18px 48px rgb(15 23 42 / 0.18)', - '6': '0 28px 72px rgb(15 23 42 / 0.22)' + '6': '0 28px 72px rgb(15 23 42 / 0.22)', + // Inner-shadow tier (recessed wells). Same slate base as the drop shadows; soft + inset. + 'inset-subtle': 'inset 0 1px 2px rgb(15 23 42 / 0.08)', + 'inset-deep': 'inset 0 2px 4px rgb(15 23 42 / 0.12)' } export const THEME_BASE_DARK_SHADOW: ShadowScale = { @@ -472,7 +478,11 @@ export const THEME_BASE_DARK_SHADOW: ShadowScale = { '3': '0 10px 30px rgb(0 0 0 / 0.42)', '4': '0 16px 44px rgb(0 0 0 / 0.48)', '5': '0 22px 60px rgb(0 0 0 / 0.55)', - '6': '0 34px 88px rgb(0 0 0 / 0.62)' + '6': '0 34px 88px rgb(0 0 0 / 0.62)', + // Inner-shadow tier (recessed wells). Dark wells read as a darker inset (not a + // light rim) — black base, deeper alpha, mode-correct unlike a contrast-mix. + 'inset-subtle': 'inset 0 1px 2px rgb(0 0 0 / 0.30)', + 'inset-deep': 'inset 0 2px 4px rgb(0 0 0 / 0.45)' } export const THEME_BASE_OPTIONS: EidosConfig = defineEidosConfig({ diff --git a/src/uix/eidos/recipe-css-contract.test.ts b/src/uix/eidos/recipe-css-contract.test.ts index 9012cad7f..785975c9d 100644 --- a/src/uix/eidos/recipe-css-contract.test.ts +++ b/src/uix/eidos/recipe-css-contract.test.ts @@ -196,6 +196,33 @@ describe('Eidos recipe CSS contract', () => { expect(violations).toEqual([]) }) + // Coherence guard (Fase 7 — theming audit): the type/size canon is only worth + // having if components CONSUME it. Recipe `font-size-*` / `icon-size-*` tokens + // MUST reference the `--font-size-*` / `--icon-size-*` scale (or another token), + // never a raw `px`/`rem` literal — else the text/icon stops responding to the + // type scale, `--scaling` and `applyTypeScale()`. The earlier CSS-only guard + // missed this (recipe-token values become custom properties, not `font-size:`). + // `words` / `palabras` / `chronos` are excluded (active dev tracks, per the audit). + it('keeps raw px/rem literals out of recipe font-size / icon-size tokens', () => { + const EXCLUDED = new Set(['words', 'palabras', 'chronos']) + const RAW_LENGTH = /^-?[0-9.]+(px|rem)$/ + const violations: string[] = [] + + for (const [component, tokens] of Object.entries(RECIPE_TOKENS)) { + if (EXCLUDED.has(component)) continue + for (const [key, value] of tokenEntries(tokens)) { + if (!/(^|-)(font-size|icon-size)(-|$)/.test(key)) continue + for (const resolved of entryValues(value)) { + if (typeof resolved === 'string' && RAW_LENGTH.test(resolved.trim())) { + violations.push(`${component}.${key}: ${resolved}`) + } + } + } + } + + expect(violations).toEqual([]) + }) + it('does not leave declared public recipe variables orphaned', () => { const recipeValueReferences = readRecipeValueReferences() const orphaned: string[] = []