chore(eidos): close theming-engine backlog — P3-5 guard + P3-8 API tidy + triage

Finishes the THEMING_AUDIT P3 backlog.

P3-5 (fixed) · appendScaledMetricDeclarations: the `parseFloat(raw) === 0` guard let
non-numeric values (auto / var() / calc()) fall into `calc(x * …)` = invalid CSS. Now
only finite, non-zero numbers are scaled; zero + non-numeric emit verbatim. No change
to the base config output (all values numeric) — pure robustness.

P3-8 (fixed) · index.ts no longer re-exports the raw render-* fns. The public render
API is the ActiveEidos class (gated by assertValid() + active config); ./lib/render-css
stays reachable for internal/tooling use. Redirected the one internal consumer
(active-eidos-config.test.ts) to import renderThemeCss from the module.

Triaged the rest with rationale (audit updated):
- P3-4 deferred · density wins by deterministic source order (stable); the :where(:root)
  restructure to also support scoped density is high-cost for a theoretical nit.
- P3-6 already resolved · dispose() routes documentElement via dom (no direct access).
- P3-7 deferred · ActiveEidos reactivity is callback-driven (apply() on pref change) by
  design; a full runes conversion is a risky refactor with no bug to justify it.
- P3-9 deferred · orphan _accent forwarders — low-value recipe surgery with cascade risk.

All P3 now fixed-or-decided; only the P2 secondary halves (contract pruning + bare
identifier color validation, both edge-case) remain, deferred as low-value.

check 0 errors · eidos suite green (3 pre-existing words-track failures unrelated).

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

@ -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%).

@ -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,

@ -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,

@ -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)
)
}
}

Loading…
Cancel
Save

Powered by TurnKey Linux.