diff --git a/src/uix/eidos/THEMING.md b/src/uix/eidos/THEMING.md index 6d3f5488f..cc6b72d3d 100644 --- a/src/uix/eidos/THEMING.md +++ b/src/uix/eidos/THEMING.md @@ -2231,8 +2231,17 @@ box-shadow, así que el foco **desaparecía**. Fix: la foundation emite siempre Los componentes que ya enfocan con `outline` (p. ej. Button) conservan el suyo por especificidad; este es el fallback para los de box-shadow. `renderForcedColorsBlock` -en `render-css.ts`. Pendiente futuro: `prefers-contrast: more` (variante high-contrast, -distinta de forced-colors). +en `render-css.ts`. + +**`prefers-contrast: more`** (macOS "Aumentar contraste", etc.) — bloque aparte que +**refuerza el chrome neutral** para quien pide más contraste: bordes a pasos más +fuertes (`subtle/default/strong` → neutral 7/8/9) + texto de-enfatizado más legible +(`secondary` → 12, `muted` → 11). Sólidos + texto primario ya son alto-contraste. Usa +`:root:root` (especificidad 0,2,0) para ganar al `:root` del tema sin depender del orden; +referencia `--primitive-neutral-*` (resuelven del cascade; si un tema los omite, la +declaración se ignora — degrada con gracia). Estrictamente aditivo (gated por el media +query) y estrictamente MÁS fuerte, así que no puede regresar el look por defecto. +`renderPrefersContrastBlock` en `render-css.ts`. **Ramp de bordes** — el slot de rol `border` pasó de **step 6 → step 7**. En la escala funcional de Radix el 6 es un *separador sutil* y el 7 es el *UI element border*; el 6 diff --git a/src/uix/eidos/THEMING_AUDIT_2026-06-01.md b/src/uix/eidos/THEMING_AUDIT_2026-06-01.md index 04902185c..a87913fc7 100644 --- a/src/uix/eidos/THEMING_AUDIT_2026-06-01.md +++ b/src/uix/eidos/THEMING_AUDIT_2026-06-01.md @@ -105,11 +105,11 @@ definiciones vs referencias). Orden de prioridad para empezar a resolver. ## P3 — Menores / backlog - **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, parcial)_ `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`. Pendiente futuro: `prefers-contrast: more` (variante high-contrast) — no es lo mismo que forced-colors. +- **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-6** · `dispose()` lee `documentElement` con DOM directo en vez de vía `dom` (`active-eidos.svelte.ts:402-416`). +- **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-*`. diff --git a/src/uix/eidos/active-eidos.test.ts b/src/uix/eidos/active-eidos.test.ts index f62f6ccb6..9934fbef2 100644 --- a/src/uix/eidos/active-eidos.test.ts +++ b/src/uix/eidos/active-eidos.test.ts @@ -491,4 +491,14 @@ describe('ActiveEidos forced-colors (a11y)', () => { expect(css).toContain(':focus-visible'); expect(css).toContain('outline: 2px solid Highlight'); }); + + it('emits a prefers-contrast: more block that strengthens neutral chrome', () => { + const { preferences } = preferencesHarness(); + const eidos = createActiveEidos({ preferences, applyDom: false }); + const css = eidos.renderStaticCss(); + expect(css).toContain('@media (prefers-contrast: more)'); + // Borders bumped to stronger neutral steps; won via :root:root specificity. + expect(css).toContain('--color-border-default: var(--primitive-neutral-8)'); + expect(css).toContain(':root:root'); + }); }); diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 31f66673e..ab0d192d6 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -4926,6 +4926,16 @@ } } +@media (prefers-contrast: more) { + :root:root { + --color-border-subtle: var(--primitive-neutral-7); + --color-border-default: var(--primitive-neutral-8); + --color-border-strong: var(--primitive-neutral-9); + --color-content-secondary: var(--primitive-neutral-12); + --color-content-muted: var(--primitive-neutral-11); + } +} + :root, [data-theme='base-light'], [data-theme='light'] { diff --git a/src/uix/eidos/lib/render-css.ts b/src/uix/eidos/lib/render-css.ts index cfacacd6f..5d31a1f2f 100644 --- a/src/uix/eidos/lib/render-css.ts +++ b/src/uix/eidos/lib/render-css.ts @@ -241,6 +241,7 @@ export function renderStaticCss(options: EidosConfig): string { if (options.motion) blocks.push(renderMotionBlocks(options.motion)) blocks.push(renderForcedColorsBlock()) + blocks.push(renderPrefersContrastBlock()) return blocks.join('\n\n') } @@ -265,6 +266,32 @@ function renderForcedColorsBlock(): string { ].join('\n') } +/** + * Accessibility — `prefers-contrast: more` (macOS "Increase contrast", Windows, etc.). + * Strengthen the neutral CHROME for users who ask for more contrast: darker/stronger + * borders + higher-contrast de-emphasized text. Solid fills + primary text are already + * high-contrast, so they stay. Strictly additive (only active under the media query) + * and strictly STRONGER (never weaker), so it can't regress the default look. + * + * `:root:root` (specificity 0,2,0) so these win over the theme's `:root` (0,1,0) + * regardless of stylesheet order. Values reference the theme's `--primitive-neutral-*`, + * which resolve from the cascade; if a theme omits them the declaration is simply + * ignored (graceful — keeps the theme's own chrome). + */ +function renderPrefersContrastBlock(): string { + return [ + '@media (prefers-contrast: more) {', + '\t:root:root {', + '\t\t--color-border-subtle: var(--primitive-neutral-7);', + '\t\t--color-border-default: var(--primitive-neutral-8);', + '\t\t--color-border-strong: var(--primitive-neutral-9);', + '\t\t--color-content-secondary: var(--primitive-neutral-12);', + '\t\t--color-content-muted: var(--primitive-neutral-11);', + '\t}', + '}' + ].join('\n') +} + function indentBlock(block: string): string { return block .split('\n')