feat(eidos): forced-colors focus a11y (P3-2) + role border ramp 6->7 (P3-3)

Closes the two color-quality items from THEMING_AUDIT P3.

P3-2 · forced-colors (Windows High Contrast): under @media (forced-colors: active) the
browser auto-maps borders/text/backgrounds to system colors BUT drops box-shadow — so
the box-shadow focus ring (--focus-ring) vanishes and keyboard focus disappears. The
foundation now always emits a system-colored outline fallback:

  @media (forced-colors: active) {
    :focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
  }

Components that already focus via outline (e.g. Button) keep theirs by specificity; this
is the fallback for the box-shadow ones. renderForcedColorsBlock in render-css.ts.

P3-3 · role border ramp: the per-role `border` slot moved step 6 -> 7. In Radix's
functional scale 6 is a subtle separator and 7 is the UI element border; step 6 read
washed-out on real element borders (outline/surface/controls). element/hover/active
(3/4/5) stay — Radix-canonical for component bg. DEFAULT_COLOR_ROLE_SLOT_STEPS.

- generated/base.css regenerated (forced-colors block + --color-{role}-border -> step 7).
- Verified in browser: --color-primary-border now resolves to primitive-7 (oklch 0.80
  0.092 vs the softer step-6 0.86 0.072); checkbox borders render defined, not broken.
- test: renderStaticCss emits the forced-colors outline block.
- docs: THEMING §28 + audit P3-1/2/3 marked resolved + README ref row.

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

@ -578,6 +578,7 @@ Resumen rápido de lo que cubre, para no duplicar aquí:
| **Modelo de color: paleta (31 escalas) + roles (alias) + intents (auto-derivados)** | §25 | | **Modelo de color: paleta (31 escalas) + roles (alias) + intents (auto-derivados)** | §25 |
| **Theme builder en runtime: `eidos.applyColorScheme(seed)` (motor `uix.color`)** | §26 | | **Theme builder en runtime: `eidos.applyColorScheme(seed)` (motor `uix.color`)** | §26 |
| **Salida wide-gamut OKLCH default-on (hex fallback + `oklch()` sibling)** | §27 | | **Salida wide-gamut OKLCH default-on (hex fallback + `oklch()` sibling)** | §27 |
| **a11y forced-colors (focus outline fallback) + ramp de bordes (slot 6→7)** | §28 |
Lo que sigue en este README son las decisiones operativas de la **capa Lo que sigue en este README son las decisiones operativas de la **capa
visual como módulo** (typography sourcing, picker patterns, API visual como módulo** (typography sourcing, picker patterns, API

@ -56,6 +56,7 @@
25. [Modelo de color — paleta + roles/intents derivados](#25-modelo-de-color--paleta--rolesintents-derivados-2026-06-02) 25. [Modelo de color — paleta + roles/intents derivados](#25-modelo-de-color--paleta--rolesintents-derivados-2026-06-02)
26. [Theme builder en runtime — `eidos.applyColorScheme`](#26-theme-builder-en-runtime--eidosapplycolorscheme-2026-06-04) 26. [Theme builder en runtime — `eidos.applyColorScheme`](#26-theme-builder-en-runtime--eidosapplycolorscheme-2026-06-04)
27. [Salida wide-gamut OKLCH (default-on)](#27-salida-wide-gamut-oklch-default-on-2026-06-04) 27. [Salida wide-gamut OKLCH (default-on)](#27-salida-wide-gamut-oklch-default-on-2026-06-04)
28. [Accesibilidad forced-colors + ramp de bordes](#28-accesibilidad-forced-colors--ramp-de-bordes-2026-06-05)
--- ---
@ -2215,6 +2216,33 @@ navegador lo soporta (Chrome 111+ / Safari 15.4+ / Firefox 113+).
--- ---
**Última revisión**: 2026-06-04. Si algo en este doc no coincide con ## 28. Accesibilidad forced-colors + ramp de bordes (2026-06-05)
**Forced-colors (Windows High Contrast)** — bajo `@media (forced-colors: active)` el
navegador auto-mapea bordes / texto / fondos a system colors (`forced-color-adjust:
auto`), PERO **elimina `box-shadow`** — y el focus ring de eidos (`--focus-ring`) es un
box-shadow, así que el foco **desaparecía**. Fix: la foundation emite siempre
```css
@media (forced-colors: active) {
:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
}
```
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).
**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
se leía lavado en bordes reales (outline / surface / controles). `element` / `hover` /
`active` (3 / 4 / 5) se mantienen (canónicos de Radix para component-bg).
`DEFAULT_COLOR_ROLE_SLOT_STEPS`. Verificado en navegador (checkbox + token
`--color-{role}-border` → step 7).
---
**Última revisión**: 2026-06-05. Si algo en este doc no coincide con
el código, el código gana — pero abre un issue para que actualicemos el código, el código gana — pero abre un issue para que actualicemos
el doc. el doc.

@ -104,9 +104,9 @@ definiciones vs referencias). Orden de prioridad para empezar a resolver.
## P3 — Menores / backlog ## P3 — Menores / backlog
- **P3-1** · Sin P3/wide-gamut (`color(display-p3 …)` + `@media (color-gamut: p3)`) en ningún sitio. (No está ni en 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** · Sin soporte high-contrast / `forced-colors` / `prefers-contrast`. (No está en backlog.) - **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-3** · Mapa slot→step apretado abajo (element=3/hover=4/active=5 adyacentes) e infrautiliza 7–8; border=6 más suave que el 7/8 de Radix. (`render-css.ts:58-68`.) - **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-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-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** · `dispose()` lee `documentElement` con DOM directo en vez de vía `dom` (`active-eidos.svelte.ts:402-416`).

@ -480,3 +480,15 @@ describe('ActiveEidos color scheme', () => {
dom.dispose(); dom.dispose();
}); });
}); });
describe('ActiveEidos forced-colors (a11y)', () => {
it('emits a system-colored focus outline fallback for forced-colors mode', () => {
const { preferences } = preferencesHarness();
const eidos = createActiveEidos({ preferences, applyDom: false });
const css = eidos.renderStaticCss();
// box-shadow focus rings vanish under forced-colors; the outline survives.
expect(css).toContain('@media (forced-colors: active)');
expect(css).toContain(':focus-visible');
expect(css).toContain('outline: 2px solid Highlight');
});
});

@ -4919,6 +4919,13 @@
} }
} }
@media (forced-colors: active) {
:focus-visible {
outline: 2px solid Highlight;
outline-offset: 2px;
}
}
:root, :root,
[data-theme='base-light'], [data-theme='base-light'],
[data-theme='light'] { [data-theme='light'] {
@ -6066,7 +6073,7 @@
--color-primary-element: var(--primitive-primary-3); --color-primary-element: var(--primitive-primary-3);
--color-primary-hover: var(--primitive-primary-4); --color-primary-hover: var(--primitive-primary-4);
--color-primary-active: var(--primitive-primary-5); --color-primary-active: var(--primitive-primary-5);
--color-primary-border: var(--primitive-primary-6); --color-primary-border: var(--primitive-primary-7);
--color-primary-solid: var(--primitive-primary-9); --color-primary-solid: var(--primitive-primary-9);
--color-primary-solid-hover: var(--primitive-primary-10); --color-primary-solid-hover: var(--primitive-primary-10);
--color-primary-text: var(--primitive-primary-11); --color-primary-text: var(--primitive-primary-11);
@ -6101,7 +6108,7 @@
--color-secondary-element: var(--primitive-secondary-3); --color-secondary-element: var(--primitive-secondary-3);
--color-secondary-hover: var(--primitive-secondary-4); --color-secondary-hover: var(--primitive-secondary-4);
--color-secondary-active: var(--primitive-secondary-5); --color-secondary-active: var(--primitive-secondary-5);
--color-secondary-border: var(--primitive-secondary-6); --color-secondary-border: var(--primitive-secondary-7);
--color-secondary-solid: var(--primitive-secondary-9); --color-secondary-solid: var(--primitive-secondary-9);
--color-secondary-solid-hover: var(--primitive-secondary-10); --color-secondary-solid-hover: var(--primitive-secondary-10);
--color-secondary-text: var(--primitive-secondary-11); --color-secondary-text: var(--primitive-secondary-11);
@ -6136,7 +6143,7 @@
--color-tertiary-element: var(--primitive-tertiary-3); --color-tertiary-element: var(--primitive-tertiary-3);
--color-tertiary-hover: var(--primitive-tertiary-4); --color-tertiary-hover: var(--primitive-tertiary-4);
--color-tertiary-active: var(--primitive-tertiary-5); --color-tertiary-active: var(--primitive-tertiary-5);
--color-tertiary-border: var(--primitive-tertiary-6); --color-tertiary-border: var(--primitive-tertiary-7);
--color-tertiary-solid: var(--primitive-tertiary-9); --color-tertiary-solid: var(--primitive-tertiary-9);
--color-tertiary-solid-hover: var(--primitive-tertiary-10); --color-tertiary-solid-hover: var(--primitive-tertiary-10);
--color-tertiary-text: var(--primitive-tertiary-11); --color-tertiary-text: var(--primitive-tertiary-11);
@ -6171,7 +6178,7 @@
--color-neutral-element: var(--primitive-neutral-3); --color-neutral-element: var(--primitive-neutral-3);
--color-neutral-hover: var(--primitive-neutral-4); --color-neutral-hover: var(--primitive-neutral-4);
--color-neutral-active: var(--primitive-neutral-5); --color-neutral-active: var(--primitive-neutral-5);
--color-neutral-border: var(--primitive-neutral-6); --color-neutral-border: var(--primitive-neutral-7);
--color-neutral-solid: var(--primitive-neutral-9); --color-neutral-solid: var(--primitive-neutral-9);
--color-neutral-solid-hover: var(--primitive-neutral-10); --color-neutral-solid-hover: var(--primitive-neutral-10);
--color-neutral-text: var(--primitive-neutral-11); --color-neutral-text: var(--primitive-neutral-11);
@ -6206,7 +6213,7 @@
--color-affirm-element: var(--primitive-affirm-3); --color-affirm-element: var(--primitive-affirm-3);
--color-affirm-hover: var(--primitive-affirm-4); --color-affirm-hover: var(--primitive-affirm-4);
--color-affirm-active: var(--primitive-affirm-5); --color-affirm-active: var(--primitive-affirm-5);
--color-affirm-border: var(--primitive-affirm-6); --color-affirm-border: var(--primitive-affirm-7);
--color-affirm-solid: var(--primitive-affirm-9); --color-affirm-solid: var(--primitive-affirm-9);
--color-affirm-solid-hover: var(--primitive-affirm-10); --color-affirm-solid-hover: var(--primitive-affirm-10);
--color-affirm-text: var(--primitive-affirm-11); --color-affirm-text: var(--primitive-affirm-11);
@ -6241,7 +6248,7 @@
--color-fulfill-element: var(--primitive-fulfill-3); --color-fulfill-element: var(--primitive-fulfill-3);
--color-fulfill-hover: var(--primitive-fulfill-4); --color-fulfill-hover: var(--primitive-fulfill-4);
--color-fulfill-active: var(--primitive-fulfill-5); --color-fulfill-active: var(--primitive-fulfill-5);
--color-fulfill-border: var(--primitive-fulfill-6); --color-fulfill-border: var(--primitive-fulfill-7);
--color-fulfill-solid: var(--primitive-fulfill-9); --color-fulfill-solid: var(--primitive-fulfill-9);
--color-fulfill-solid-hover: var(--primitive-fulfill-10); --color-fulfill-solid-hover: var(--primitive-fulfill-10);
--color-fulfill-text: var(--primitive-fulfill-11); --color-fulfill-text: var(--primitive-fulfill-11);
@ -6276,7 +6283,7 @@
--color-risk-element: var(--primitive-risk-3); --color-risk-element: var(--primitive-risk-3);
--color-risk-hover: var(--primitive-risk-4); --color-risk-hover: var(--primitive-risk-4);
--color-risk-active: var(--primitive-risk-5); --color-risk-active: var(--primitive-risk-5);
--color-risk-border: var(--primitive-risk-6); --color-risk-border: var(--primitive-risk-7);
--color-risk-solid: var(--primitive-risk-9); --color-risk-solid: var(--primitive-risk-9);
--color-risk-solid-hover: var(--primitive-risk-10); --color-risk-solid-hover: var(--primitive-risk-10);
--color-risk-text: var(--primitive-risk-11); --color-risk-text: var(--primitive-risk-11);
@ -6311,7 +6318,7 @@
--color-threat-element: var(--primitive-threat-3); --color-threat-element: var(--primitive-threat-3);
--color-threat-hover: var(--primitive-threat-4); --color-threat-hover: var(--primitive-threat-4);
--color-threat-active: var(--primitive-threat-5); --color-threat-active: var(--primitive-threat-5);
--color-threat-border: var(--primitive-threat-6); --color-threat-border: var(--primitive-threat-7);
--color-threat-solid: var(--primitive-threat-9); --color-threat-solid: var(--primitive-threat-9);
--color-threat-solid-hover: var(--primitive-threat-10); --color-threat-solid-hover: var(--primitive-threat-10);
--color-threat-text: var(--primitive-threat-11); --color-threat-text: var(--primitive-threat-11);
@ -6346,7 +6353,7 @@
--color-loss-element: var(--primitive-loss-3); --color-loss-element: var(--primitive-loss-3);
--color-loss-hover: var(--primitive-loss-4); --color-loss-hover: var(--primitive-loss-4);
--color-loss-active: var(--primitive-loss-5); --color-loss-active: var(--primitive-loss-5);
--color-loss-border: var(--primitive-loss-6); --color-loss-border: var(--primitive-loss-7);
--color-loss-solid: var(--primitive-loss-9); --color-loss-solid: var(--primitive-loss-9);
--color-loss-solid-hover: var(--primitive-loss-10); --color-loss-solid-hover: var(--primitive-loss-10);
--color-loss-text: var(--primitive-loss-11); --color-loss-text: var(--primitive-loss-11);
@ -7527,7 +7534,7 @@
--color-primary-element: var(--primitive-primary-3); --color-primary-element: var(--primitive-primary-3);
--color-primary-hover: var(--primitive-primary-4); --color-primary-hover: var(--primitive-primary-4);
--color-primary-active: var(--primitive-primary-5); --color-primary-active: var(--primitive-primary-5);
--color-primary-border: var(--primitive-primary-6); --color-primary-border: var(--primitive-primary-7);
--color-primary-solid: var(--primitive-primary-9); --color-primary-solid: var(--primitive-primary-9);
--color-primary-solid-hover: var(--primitive-primary-10); --color-primary-solid-hover: var(--primitive-primary-10);
--color-primary-text: var(--primitive-primary-11); --color-primary-text: var(--primitive-primary-11);
@ -7562,7 +7569,7 @@
--color-secondary-element: var(--primitive-secondary-3); --color-secondary-element: var(--primitive-secondary-3);
--color-secondary-hover: var(--primitive-secondary-4); --color-secondary-hover: var(--primitive-secondary-4);
--color-secondary-active: var(--primitive-secondary-5); --color-secondary-active: var(--primitive-secondary-5);
--color-secondary-border: var(--primitive-secondary-6); --color-secondary-border: var(--primitive-secondary-7);
--color-secondary-solid: var(--primitive-secondary-9); --color-secondary-solid: var(--primitive-secondary-9);
--color-secondary-solid-hover: var(--primitive-secondary-10); --color-secondary-solid-hover: var(--primitive-secondary-10);
--color-secondary-text: var(--primitive-secondary-11); --color-secondary-text: var(--primitive-secondary-11);
@ -7597,7 +7604,7 @@
--color-tertiary-element: var(--primitive-tertiary-3); --color-tertiary-element: var(--primitive-tertiary-3);
--color-tertiary-hover: var(--primitive-tertiary-4); --color-tertiary-hover: var(--primitive-tertiary-4);
--color-tertiary-active: var(--primitive-tertiary-5); --color-tertiary-active: var(--primitive-tertiary-5);
--color-tertiary-border: var(--primitive-tertiary-6); --color-tertiary-border: var(--primitive-tertiary-7);
--color-tertiary-solid: var(--primitive-tertiary-9); --color-tertiary-solid: var(--primitive-tertiary-9);
--color-tertiary-solid-hover: var(--primitive-tertiary-10); --color-tertiary-solid-hover: var(--primitive-tertiary-10);
--color-tertiary-text: var(--primitive-tertiary-11); --color-tertiary-text: var(--primitive-tertiary-11);
@ -7632,7 +7639,7 @@
--color-neutral-element: var(--primitive-neutral-3); --color-neutral-element: var(--primitive-neutral-3);
--color-neutral-hover: var(--primitive-neutral-4); --color-neutral-hover: var(--primitive-neutral-4);
--color-neutral-active: var(--primitive-neutral-5); --color-neutral-active: var(--primitive-neutral-5);
--color-neutral-border: var(--primitive-neutral-6); --color-neutral-border: var(--primitive-neutral-7);
--color-neutral-solid: var(--primitive-neutral-9); --color-neutral-solid: var(--primitive-neutral-9);
--color-neutral-solid-hover: var(--primitive-neutral-10); --color-neutral-solid-hover: var(--primitive-neutral-10);
--color-neutral-text: var(--primitive-neutral-11); --color-neutral-text: var(--primitive-neutral-11);
@ -7667,7 +7674,7 @@
--color-affirm-element: var(--primitive-affirm-3); --color-affirm-element: var(--primitive-affirm-3);
--color-affirm-hover: var(--primitive-affirm-4); --color-affirm-hover: var(--primitive-affirm-4);
--color-affirm-active: var(--primitive-affirm-5); --color-affirm-active: var(--primitive-affirm-5);
--color-affirm-border: var(--primitive-affirm-6); --color-affirm-border: var(--primitive-affirm-7);
--color-affirm-solid: var(--primitive-affirm-9); --color-affirm-solid: var(--primitive-affirm-9);
--color-affirm-solid-hover: var(--primitive-affirm-10); --color-affirm-solid-hover: var(--primitive-affirm-10);
--color-affirm-text: var(--primitive-affirm-11); --color-affirm-text: var(--primitive-affirm-11);
@ -7702,7 +7709,7 @@
--color-fulfill-element: var(--primitive-fulfill-3); --color-fulfill-element: var(--primitive-fulfill-3);
--color-fulfill-hover: var(--primitive-fulfill-4); --color-fulfill-hover: var(--primitive-fulfill-4);
--color-fulfill-active: var(--primitive-fulfill-5); --color-fulfill-active: var(--primitive-fulfill-5);
--color-fulfill-border: var(--primitive-fulfill-6); --color-fulfill-border: var(--primitive-fulfill-7);
--color-fulfill-solid: var(--primitive-fulfill-9); --color-fulfill-solid: var(--primitive-fulfill-9);
--color-fulfill-solid-hover: var(--primitive-fulfill-10); --color-fulfill-solid-hover: var(--primitive-fulfill-10);
--color-fulfill-text: var(--primitive-fulfill-11); --color-fulfill-text: var(--primitive-fulfill-11);
@ -7737,7 +7744,7 @@
--color-risk-element: var(--primitive-risk-3); --color-risk-element: var(--primitive-risk-3);
--color-risk-hover: var(--primitive-risk-4); --color-risk-hover: var(--primitive-risk-4);
--color-risk-active: var(--primitive-risk-5); --color-risk-active: var(--primitive-risk-5);
--color-risk-border: var(--primitive-risk-6); --color-risk-border: var(--primitive-risk-7);
--color-risk-solid: var(--primitive-risk-9); --color-risk-solid: var(--primitive-risk-9);
--color-risk-solid-hover: var(--primitive-risk-10); --color-risk-solid-hover: var(--primitive-risk-10);
--color-risk-text: var(--primitive-risk-11); --color-risk-text: var(--primitive-risk-11);
@ -7772,7 +7779,7 @@
--color-threat-element: var(--primitive-threat-3); --color-threat-element: var(--primitive-threat-3);
--color-threat-hover: var(--primitive-threat-4); --color-threat-hover: var(--primitive-threat-4);
--color-threat-active: var(--primitive-threat-5); --color-threat-active: var(--primitive-threat-5);
--color-threat-border: var(--primitive-threat-6); --color-threat-border: var(--primitive-threat-7);
--color-threat-solid: var(--primitive-threat-9); --color-threat-solid: var(--primitive-threat-9);
--color-threat-solid-hover: var(--primitive-threat-10); --color-threat-solid-hover: var(--primitive-threat-10);
--color-threat-text: var(--primitive-threat-11); --color-threat-text: var(--primitive-threat-11);
@ -7807,7 +7814,7 @@
--color-loss-element: var(--primitive-loss-3); --color-loss-element: var(--primitive-loss-3);
--color-loss-hover: var(--primitive-loss-4); --color-loss-hover: var(--primitive-loss-4);
--color-loss-active: var(--primitive-loss-5); --color-loss-active: var(--primitive-loss-5);
--color-loss-border: var(--primitive-loss-6); --color-loss-border: var(--primitive-loss-7);
--color-loss-solid: var(--primitive-loss-9); --color-loss-solid: var(--primitive-loss-9);
--color-loss-solid-hover: var(--primitive-loss-10); --color-loss-solid-hover: var(--primitive-loss-10);
--color-loss-text: var(--primitive-loss-11); --color-loss-text: var(--primitive-loss-11);

@ -69,12 +69,16 @@ export interface RenderCssVariablesOptions {
readonly strict?: boolean readonly strict?: boolean
} }
// Radix functional steps: 1-2 backgrounds · 3-5 component bg (rest/hover/active) ·
// 6 subtle separator · 7 UI element border · 8 hovered element border · 9 solid ·
// 10 solid hover · 11-12 text. The role `border` slot maps to step 7 (Radix's "UI
// element border") — step 6 reads as a washed-out separator on real element borders.
const DEFAULT_COLOR_ROLE_SLOT_STEPS: Record<ColorRoleSlot, string> = { const DEFAULT_COLOR_ROLE_SLOT_STEPS: Record<ColorRoleSlot, string> = {
track: '1', track: '1',
element: '3', element: '3',
hover: '4', hover: '4',
active: '5', active: '5',
border: '6', border: '7',
solid: '9', solid: '9',
solidHover: '10', solidHover: '10',
text: '11', text: '11',
@ -236,9 +240,31 @@ export function renderStaticCss(options: EidosConfig): string {
if (options.motion) blocks.push(renderMotionBlocks(options.motion)) if (options.motion) blocks.push(renderMotionBlocks(options.motion))
blocks.push(renderForcedColorsBlock())
return blocks.join('\n\n') return blocks.join('\n\n')
} }
/**
* Accessibility — Windows High Contrast / `forced-colors`. The OS forces colors via
* `forced-color-adjust: auto`, which auto-maps borders / text / backgrounds to system
* colors BUT drops `box-shadow` entirely — so the box-shadow focus ring (`--focus-ring`)
* vanishes. Guarantee a visible keyboard-focus indicator with a system-colored
* `outline` (which forced-colors preserve). Components that already focus via `outline`
* (e.g. Button) keep theirs by higher specificity; this is the fallback for the
* box-shadow ones. Emitted always — the `@media` gate makes it inert otherwise.
*/
function renderForcedColorsBlock(): string {
return [
'@media (forced-colors: active) {',
'\t:focus-visible {',
'\t\toutline: 2px solid Highlight;',
'\t\toutline-offset: 2px;',
'\t}',
'}'
].join('\n')
}
function indentBlock(block: string): string { function indentBlock(block: string): string {
return block return block
.split('\n') .split('\n')

Loading…
Cancel
Save

Powered by TurnKey Linux.