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