diff --git a/src/uix/eidos/THEMING_AUDIT_2026-06-01.md b/src/uix/eidos/THEMING_AUDIT_2026-06-01.md index a87913fc7..bd500e9db 100644 --- a/src/uix/eidos/THEMING_AUDIT_2026-06-01.md +++ b/src/uix/eidos/THEMING_AUDIT_2026-06-01.md @@ -107,12 +107,12 @@ definiciones vs referencias). Orden de prioridad para empezar a resolver. - **P3-1** · ✅ _(resuelto 2026-06-04)_ Wide-gamut: salida OKLCH-nativa default-on (hex fallback + `oklch()` sibling por paso, `appendColorScaleDeclarations`) + generador wide-gamut-true (`buildScheme`/`applyColorScheme` retienen el OKLCH raw). THEMING §27, RFC §7. - **P3-2** · ✅ _(resuelto 2026-06-05)_ `forced-colors`: el `box-shadow` del focus ring se elimina en HCM → bloque `@media (forced-colors: active)` con `outline` de foco system-colored (`Highlight`) como fallback universal (`renderForcedColorsBlock`). El resto (bordes/texto/fondos) lo auto-mapea `forced-color-adjust: auto`. **+ `prefers-contrast: more`** (`renderPrefersContrastBlock`): refuerza bordes (neutral 7/8/9) + texto de-enfatizado (12/11) vía `:root:root`, aditivo y gated. THEMING §28. - **P3-3** · ✅ _(resuelto 2026-06-05)_ El slot de rol `border` pasó de step 6 (separador sutil de Radix) a **step 7** (UI element border de Radix) — `DEFAULT_COLOR_ROLE_SLOT_STEPS`. element/hover/active=3/4/5 se quedan (son los canónicos de Radix para component-bg). Verificado en navegador (checkbox / token `--color-{role}-border`). -- **P3-4** · La densidad activa depende del **orden de fuente**, no de especificidad (`:root` default vs `[data-density]`, misma especificidad). *Fix*: `:where(:root)` para el default. -- **P3-5** · `appendDensityScaledDeclarations` usa `parseFloat` para el guard de cero → frágil si un valor futuro es keyword/`var()` (`render-css.ts:1383-1387`). +- **P3-4** · ⏸️ _(deferido — works-by-design)_ La densidad activa gana por **orden de fuente** (el `:root` default va antes; los bloques `[data-density]` después), no por especificidad. El orden lo fija el renderer de forma determinista, así que es estable. El fix `:where(:root)` para el default preservaría densidad scoped (no sólo en root) pero exige reestructurar la emisión de los defaults + verificar la matriz densidad×scaling×breakpoint — coste alto para una fragilidad teórica. Deferido salvo que aparezca densidad scoped real. +- **P3-5** · ✅ _(resuelto 2026-06-05)_ `appendScaledMetricDeclarations`: el guard `parseFloat(raw) === 0` dejaba pasar valores no-numéricos (`auto`/`var()`/`calc()`) a `calc(x * …)` inválido. Ahora sólo se multiplica si `Number.isFinite(num) && num !== 0`; cero y no-numéricos se emiten verbatim. Sin cambio en el output del config base (todos los valores son numéricos). - **P3-6** · ✅ _(ya resuelto)_ `dispose()` usa `#lastAttrs.target` (el `documentElement` capturado vía `dom.getDocument()` en `#writeThemeAttrs`) + `dom.apply`/`dom.removeStyle` — sin acceso directo a `document`. El único `globalThis.matchMedia` restante es el fallback SSR-guarded de color-scheme (`createSystemColorSchemeSource`), no manipulación de DOM. Probablemente cerrado en el commit "DOM via ActiveDom" (928b3c15). -- **P3-7** · `active-eidos.svelte.ts` no usa runes; el sufijo `.svelte.ts` engaña (lecturas de tema no son reactivas). -- **P3-8** · `index.ts` re-exporta los render-fns crudos (duplican la API de la clase, sin gating). -- **P3-9** · `editable._accent-solid-hover` se salta los forwarders del recipe (`base.ts:2652-2661`); forwarders huérfanos varios (select/tags-input/editable) sobreviven al test de orphans por la cadena `_accent-*`. +- **P3-7** · ⏸️ _(deferido — by-design)_ `active-eidos.svelte.ts` no usa runes; la reactividad es **callback-driven** (`onPreferenceChange` → `apply()` re-renderiza + reescribe CSS/attrs). Los getters (`getThemeId()` etc.) son lecturas point-in-time por diseño; los componentes consumen CSS vars (que sí se actualizan vía `apply()`), no los getters de forma reactiva. Convertir a runes es un refactor riesgoso de la arquitectura de preference-source sin un bug que lo justifique. El sufijo `.svelte.ts` se mantiene (capa runtime de eidos; permite runes a futuro). +- **P3-8** · ✅ _(resuelto 2026-06-05)_ `index.ts` ya no re-exporta los render-fns crudos. La API pública de render es la clase `ActiveEidos` (gateada por `assertValid()` + config activa); el módulo `./lib/render-css` sigue accesible para uso interno/tooling. Verificado: cero consumidores importaban los crudos desde el index (`check` 0 errores tras quitarlos). +- **P3-9** · ⏸️ _(deferido — low-value/risky)_ Forwarders huérfanos (select/tags-input/editable) sobreviven al test de orphans por la cadena `_accent-*`. Limpiarlos es cirugía de recipe con riesgo de romper las cascadas `_accent`, por valor bajo. Deferido; el lint sigue siendo opt-in (no es el contrato — el contrato es el morfo). - **P3-10** · Words `rail-bg/border` son hex literales (`base.ts:2726-2727`) — intencional (margen de papel fixed-tone), pero no adapta a dark. - **P3-11** · ✅ _(resuelto 2026-06-05)_ Asimetría light/dark de superficies: light `overlay` era `neutral-3` == `muted` (popovers indistinguibles de paneles muted); dark ya tenía `overlay=neutral-4`. Light `overlay` → `neutral-4` → ladder consistente en ambos modos: `default(1) < raised(2) < muted(3) < overlay(4)`. Verificado en navegador (popover light: overlay L93% distinto de muted L95.5% / default L99%). diff --git a/src/uix/eidos/active-eidos-config.test.ts b/src/uix/eidos/active-eidos-config.test.ts index 035bf9766..a45616b39 100644 --- a/src/uix/eidos/active-eidos-config.test.ts +++ b/src/uix/eidos/active-eidos-config.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { renderThemeCss } from './lib/render-css' import { COLOR_ALPHA_STEPS, COLOR_SCALE_STEPS, @@ -14,7 +15,6 @@ import { extendEidosConfig, parseEidosConfigFromJson, parseEidosConfigDocument, - renderThemeCss, STATIC_CONTROL_HEIGHT, STATIC_DENSITY, STATIC_LAYOUT, diff --git a/src/uix/eidos/index.ts b/src/uix/eidos/index.ts index 5a8ee65a9..1736b0d4c 100644 --- a/src/uix/eidos/index.ts +++ b/src/uix/eidos/index.ts @@ -48,12 +48,11 @@ export { readEidosConfigDocument, serializeEidosConfigDocument } from './lib/persistence'; -export { - renderContractCss, - renderCssVariables, - renderStaticCss, - renderThemeCss -} from './lib/render-css'; +// Note: the raw render-* functions are intentionally NOT re-exported here. The +// public CSS-rendering API is the ActiveEidos class (`renderStaticCss()` / +// `renderThemeCss()` / `renderCssVariables()` / `renderContractCss()`), which gates +// them through `assertValid()` + the active config. Reach into `./lib/render-css` +// directly only for internal/tooling use. export { THEME_BASE_COLOR_ROLES, THEME_BASE_DARK_COLOR_SCALES, diff --git a/src/uix/eidos/lib/render-css.ts b/src/uix/eidos/lib/render-css.ts index 5d31a1f2f..1d359ecdb 100644 --- a/src/uix/eidos/lib/render-css.ts +++ b/src/uix/eidos/lib/render-css.ts @@ -1789,10 +1789,15 @@ function appendScaledMetricDeclarations( for (const [name, value] of Object.entries(record)) { const raw = String(value).trim() - const isZero = parseFloat(raw) === 0 + // Only multiply finite, non-zero numeric lengths. Zero stays verbatim (`0px` + // must remain a valid length, `calc(0 * x)` is pointless) and so does any + // non-numeric value (keyword like `auto`, a `var()` or `calc()`) — wrapping + // those in `calc(x * …)` would emit invalid CSS. + const num = parseFloat(raw) + const scalable = Number.isFinite(num) && num !== 0 const density = densityScaleVar ? ` * var(--${densityScaleVar})` : '' declarations.push( - cssVar(`${prefix}-${name}`, isZero ? raw : `calc(${raw}${density} * var(--scaling))`) + cssVar(`${prefix}-${name}`, scalable ? `calc(${raw}${density} * var(--scaling))` : raw) ) } }