diff --git a/src/uix/eidos/DEPTH_ENGINE_RFC.md b/src/uix/eidos/DEPTH_ENGINE_RFC.md index 3628d873d..47a51c190 100644 --- a/src/uix/eidos/DEPTH_ENGINE_RFC.md +++ b/src/uix/eidos/DEPTH_ENGINE_RFC.md @@ -81,7 +81,8 @@ primitivos existentes** — no los reemplaza: ``` --depth-{plane}-surface → var(--color-surface-{…}) (tinte tonal, mode-aware) ---depth-{plane}-shadow → var(--shadow-{…}) (computado en Fase 2) +--depth-{plane}-shadow → var(--shadow-{…}) (gota, mode-aware vía tema) +--depth-{plane}-halo → oklab rim-light (lift en dark · Fase 2) --depth-{plane}-z → var(--z-index-{…}) (banda) --depth-{plane}-blur → atmósfera (Fase 4) --depth-{plane}-scrim → atmósfera (Fase 4) @@ -96,9 +97,15 @@ fondos de componente. Sin renombrar nada existente → **cero rotura de componen 1. ✅ **Plano unificado** — `EidosConfig.depth` + planos canónicos + emisión `--depth-{plane}-*` (componen surface/shadow/z) + regla `[data-depth]` (sombra+z) + validación + test + regen. Puertas: set config-driven, mezcla por plano, primitivos intactos, escape a raw. -2. ⏸️ **Mezcla computada mode-adaptive** — diferida: las sombras YA son mode-aware (light slate - sutil / dark negro más opaco), así que es **refinamiento** (sombra OKLCH desde la superficie + - halo en dark), no un hueco. Se retoma como pulido o se funde en el showcase. +2. ✅ **Mezcla computada mode-adaptive** — la sombra de gota mantiene la escala themed (slate + sutil en light / negro más opaco en dark — ya un tinte, no negro plano); Fase 2 añade el cue + **`halo`**: un rim-light de borde superior **computado en oklab** + (`color-mix(in oklab, white N%, transparent)`, escalado por plano: 5/7/8% en raised/overlay/ + modal). Es invisible sobre superficies claras (manda la gota) y se vuelve la señal de + elevación sobre superficies oscuras (donde la gota apenas se ve) — resuelve "la sombra miente + en dark" **sin tocar las sombras globales**: vive solo en el canal depth, compuesto en el + `box-shadow` de `[data-depth]` (`shadow, halo`). El halo derivado de la superficie + el frost + atmosférico quedan para Fase 4. 3. ✅ **Canal `depth` eventful (momento-evento)** — la dimensión de ELEVACIÓN vive en la *firma*: `present-rise` (emerge) hace crecer la sombra desde plano → la de reposo del elemento (sube); `press-squeeze` (contact) la aplana a la superficie (recede). Generic (flush = no-op), @@ -106,7 +113,10 @@ fondos de componente. Sin renombrar nada existente → **cero rotura de componen reduced-motion (cap global). Anulable: un tema sobreescribe los keyframes/signatures. Monta sobre el sistema de `signatures` existente, **no** un sistema paralelo. 4. **Atmósfera** — blur/scrim en config; opt-out por overlay. + builder `applyDepth(seed)`. -5. **Showcase + docs** — `/temas/profundidad` (planos/mezcla/eventful en vivo) + THEMING §depth. +5. ✅ **Showcase + docs** — `/temas/profundidad` a profundidad de referencia: reacciona · + estados (dynamic elevation) · asciende (firma) · escalera de planos · catálogo en reposo · + luz vs sombra (el halo) · jaula abierta · a11y. Supera la amplitud de la referencia de + *elevation* de Material añadiendo los dos ejes que le faltan (eventful + jaula abierta). + THEMING §29. ## 7. Composición con lo existente diff --git a/src/uix/eidos/THEMING.md b/src/uix/eidos/THEMING.md index 9bfc0bad4..f51feb591 100644 --- a/src/uix/eidos/THEMING.md +++ b/src/uix/eidos/THEMING.md @@ -2273,8 +2273,11 @@ superficie + z). Guía canónica: `DEPTH_ENGINE_RFC.md`. **Dos momentos**: overlay · modal · recessed`) que cohere superficie + sombra + z. Los tokens `--depth-{plane}-{cue}` **componen los primitivos existentes** (`--color-surface-*`, `--shadow-*`, `--z-index-*`), así que la mezcla es mode-aware gratis. La regla - `[data-depth]` aplica las señales aditivas seguras (sombra + z); `surface` queda opt-in - (no pisa fondos de componente). + `[data-depth]` aplica las señales aditivas seguras (`box-shadow` = gota `shadow` + rim-light + `halo`, más `z-index`); `surface` queda opt-in (no pisa fondos de componente). El **`halo`** + es un rim de borde superior computado en oklab (`color-mix(in oklab, white N%, transparent)`): + invisible sobre superficies claras (manda la gota), señal de elevación sobre oscuras — la + respuesta mode-adaptive a "la sombra miente en dark". - **Evento** — al **emerger** la sombra crece desde plano → la de reposo (sube); al **presionar** se aplana (recede). Vive en la *firma* (`present-rise` / `press-squeeze` sobre `data-event-*`), coordinado con motion + sound + haptic desde **un solo evento**. @@ -2285,10 +2288,9 @@ añade/renombra/retunea); los primitivos siguen accesibles (`box-shadow`/`z-inde paso); la capa eventful es aditiva y anulable (sobreescribe keyframes/signatures). Demo en vivo: `/temas/profundidad`. -**Pendiente** (no del núcleo): mezcla computada OKLCH + halo dark (refinamiento; las sombras -ya son mode-aware), atmósfera frost/scrim (requiere superficies translúcidas), y **adopción -por componentes** (hoy card/dialog/popover cablean sombra/z aparte; migrarlos a `data-depth` -es una pasada separada). +**Pendiente** (no del núcleo): atmósfera frost/scrim (Fase 4 — requiere superficies +translúcidas) + builder `applyDepth(seed)`, y **adopción por componentes** (hoy +card/dialog/popover cablean sombra/z aparte; migrarlos a `data-depth` es una pasada separada). --- diff --git a/src/uix/eidos/active-eidos-config.test.ts b/src/uix/eidos/active-eidos-config.test.ts index 50c3775a2..711d6f695 100644 --- a/src/uix/eidos/active-eidos-config.test.ts +++ b/src/uix/eidos/active-eidos-config.test.ts @@ -469,16 +469,22 @@ describe('ActiveEidos config', () => { eidos.clearTypeScale(); // reverts cleanly, no throw }); - it('emits depth plane tokens + [data-depth] rules (composing surface/shadow/z)', () => { + it('emits depth plane tokens + [data-depth] rules (composing surface/shadow/halo/z)', () => { const css = createThemeBaseEidos().renderStaticCss(); - // tokens compose the existing primitives — no new shadow/surface/z math + // surface/shadow/z compose the existing primitives — no new math there expect(css).toContain('--depth-overlay-shadow: var(--shadow-overlay);'); expect(css).toContain('--depth-overlay-z: var(--z-index-popover);'); expect(css).toContain('--depth-raised-surface: var(--color-surface-raised);'); - // the rule applies the safe additive cues (shadow + z); surface stays an opt-in token + // Fase 2 — the rim-light halo is the one computed cue (oklab), the dark-mode lift signal + expect(css).toContain( + '--depth-overlay-halo: inset 0 1px 0 0 color-mix(in oklab, white 7%, transparent);' + ); + // the rule applies the safe additive cues; box-shadow composes the drop shadow + the rim expect(css).toContain("[data-depth='overlay'] {"); - expect(css).toContain('box-shadow: var(--depth-overlay-shadow);'); + expect(css).toContain('box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo);'); expect(css).toContain('z-index: var(--depth-overlay-z);'); + // a plane without a halo (flush) keeps a single-value box-shadow — no dangling comma + expect(css).toContain('box-shadow: var(--depth-flush-shadow);'); }); it('depth planes are config-driven — a theme can add a plane (jaula abierta)', () => { diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 23b70b7d3..8898f59ac 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -442,12 +442,15 @@ --depth-flush-z: var(--z-index-base); --depth-raised-surface: var(--color-surface-raised); --depth-raised-shadow: var(--shadow-raised); + --depth-raised-halo: inset 0 1px 0 0 color-mix(in oklab, white 5%, transparent); --depth-raised-z: var(--z-index-raised); --depth-overlay-surface: var(--color-surface-overlay); --depth-overlay-shadow: var(--shadow-overlay); + --depth-overlay-halo: inset 0 1px 0 0 color-mix(in oklab, white 7%, transparent); --depth-overlay-z: var(--z-index-popover); --depth-modal-surface: var(--color-surface-overlay); --depth-modal-shadow: var(--shadow-overlay); + --depth-modal-halo: inset 0 1px 0 0 color-mix(in oklab, white 8%, transparent); --depth-modal-z: var(--z-index-modal); --depth-recessed-surface: var(--color-surface-muted); --depth-recessed-shadow: inset 0 1px 2px color-mix(in srgb, var(--color-neutral-contrast) 12%, transparent); @@ -4325,17 +4328,17 @@ } [data-depth='raised'] { - box-shadow: var(--depth-raised-shadow); + box-shadow: var(--depth-raised-shadow), var(--depth-raised-halo); z-index: var(--depth-raised-z); } [data-depth='overlay'] { - box-shadow: var(--depth-overlay-shadow); + box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo); z-index: var(--depth-overlay-z); } [data-depth='modal'] { - box-shadow: var(--depth-modal-shadow); + box-shadow: var(--depth-modal-shadow), var(--depth-modal-halo); z-index: var(--depth-modal-z); } diff --git a/src/uix/eidos/lib/config-types.ts b/src/uix/eidos/lib/config-types.ts index 555c5874c..95baab769 100644 --- a/src/uix/eidos/lib/config-types.ts +++ b/src/uix/eidos/lib/config-types.ts @@ -673,6 +673,14 @@ export interface DepthPlane { readonly surface?: string; /** Box-shadow (`var(--shadow-{…})`, a raw shadow, or `none`). */ readonly shadow?: string; + /** + * Rim-light halo — a top-edge inset highlight composed alongside `shadow` in the + * `[data-depth]` box-shadow. Computed in oklab (`color-mix(in oklab, white N%, transparent)`) + * so it is invisible against light surfaces (where the drop shadow leads) but reads as the + * lift cue against dark surfaces (where a downward shadow barely shows). The mode-adaptive + * answer to "shadow lies in dark". (DEPTH_ENGINE_RFC §3 piece 2 / Fase 2) + */ + readonly halo?: string; /** z-index (`var(--z-index-{…})` or a raw value). */ readonly z?: string; /** Backdrop blur (atmosphere — DEPTH_ENGINE_RFC Fase 4). */ diff --git a/src/uix/eidos/lib/config.ts b/src/uix/eidos/lib/config.ts index 92fe4e4d9..a5256f7a1 100644 --- a/src/uix/eidos/lib/config.ts +++ b/src/uix/eidos/lib/config.ts @@ -920,7 +920,7 @@ function validateDepthPrimitives(options: EidosConfig, issues: EidosValidationIs issues.push({ path, message: 'depth plane must be a plain object' }) continue } - for (const cue of ['surface', 'shadow', 'z', 'blur', 'scrim'] as const) { + for (const cue of ['surface', 'shadow', 'halo', 'z', 'blur', 'scrim'] as const) { const value = (cues as Record)[cue] if (value !== undefined) validateNonEmptyCssValue(`${path}.${cue}`, value, issues) } diff --git a/src/uix/eidos/lib/primitives/static.ts b/src/uix/eidos/lib/primitives/static.ts index 0ec2df5e2..e96f8577a 100644 --- a/src/uix/eidos/lib/primitives/static.ts +++ b/src/uix/eidos/lib/primitives/static.ts @@ -214,16 +214,19 @@ export const STATIC_DEPTH: DepthPrimitiveSet = { raised: { surface: 'var(--color-surface-raised)', shadow: 'var(--shadow-raised)', + halo: 'inset 0 1px 0 0 color-mix(in oklab, white 5%, transparent)', z: 'var(--z-index-raised)' }, overlay: { surface: 'var(--color-surface-overlay)', shadow: 'var(--shadow-overlay)', + halo: 'inset 0 1px 0 0 color-mix(in oklab, white 7%, transparent)', z: 'var(--z-index-popover)' }, modal: { surface: 'var(--color-surface-overlay)', shadow: 'var(--shadow-overlay)', + halo: 'inset 0 1px 0 0 color-mix(in oklab, white 8%, transparent)', z: 'var(--z-index-modal)' }, recessed: { diff --git a/src/uix/eidos/lib/render-css.ts b/src/uix/eidos/lib/render-css.ts index 56525102b..11f169d57 100644 --- a/src/uix/eidos/lib/render-css.ts +++ b/src/uix/eidos/lib/render-css.ts @@ -768,7 +768,7 @@ function appendTypographyDeclarations( /** Emit `--depth-{plane}-{cue}` tokens — each plane's cue values composed into `:root`. */ function appendDepthDeclarations(declarations: string[], depth: DepthPrimitiveSet): void { for (const [plane, cues] of Object.entries(depth.planes)) { - for (const cue of ['surface', 'shadow', 'z', 'blur', 'scrim'] as const) { + for (const cue of ['surface', 'shadow', 'halo', 'z', 'blur', 'scrim'] as const) { const value = cues[cue] if (value !== undefined) declarations.push(cssVar(`depth-${plane}-${cue}`, value)) } @@ -777,15 +777,20 @@ function appendDepthDeclarations(declarations: string[], depth: DepthPrimitiveSe /** * `[data-depth='{plane}']` applies the *safe additive* depth cues — box-shadow + z-index — - * referencing the plane tokens (so `setCssVariables` / a theme can retune them). `surface` - * (background) + `blur`/`scrim` (atmosphere) stay opt-in tokens, never forced here, so the - * rule never fights a component's own background. (DEPTH_ENGINE_RFC §5) + * referencing the plane tokens (so `setCssVariables` / a theme can retune them). The + * box-shadow composes the drop `shadow` with the rim-light `halo` (comma-joined) when both + * are declared, so the lift cue survives in dark mode. `surface` (background) + `blur`/`scrim` + * (atmosphere) stay opt-in tokens, never forced here, so the rule never fights a component's + * own background. (DEPTH_ENGINE_RFC §5) */ function renderDepthBlocks(depth: DepthPrimitiveSet): string[] { const blocks: string[] = [] for (const [plane, cues] of Object.entries(depth.planes)) { const lines: string[] = [] - if (cues.shadow !== undefined) lines.push(`box-shadow: var(--depth-${plane}-shadow);`) + const shadowParts: string[] = [] + if (cues.shadow !== undefined) shadowParts.push(`var(--depth-${plane}-shadow)`) + if (cues.halo !== undefined) shadowParts.push(`var(--depth-${plane}-halo)`) + if (shadowParts.length) lines.push(`box-shadow: ${shadowParts.join(', ')};`) if (cues.z !== undefined) lines.push(`z-index: var(--depth-${plane}-z);`) if (lines.length) blocks.push(renderBlock(`[data-depth='${plane}']`, lines)) } diff --git a/web/routes/temas/profundidad/+page.svelte b/web/routes/temas/profundidad/+page.svelte index bf3587757..4a02819f3 100644 --- a/web/routes/temas/profundidad/+page.svelte +++ b/web/routes/temas/profundidad/+page.svelte @@ -1,12 +1,17 @@ Profundidad · Eidos @@ -65,7 +126,39 @@ - + +
+
+

Responde a cada estado

+

+ No solo al hover. Reposo, foco, pulsado, deshabilitado — la profundidad cambia con cada + estado. Donde Material publica una tabla de niveles por estado, aquí es un solo canal que + reacciona. El último control es real: pásale el cursor, enfócalo con Tab, púlsalo. +

+
+
+ {#each STATE_LADDER as s} +
+
+ Guardar +
+
{s.state} · {s.moves}
+
+ {/each} +
+
+ +
+
en vivo · hover · foco · pulsa
+
+
+
+ +

Asciende

@@ -112,7 +205,27 @@
- + +
+
+

La escalera de planos

+

+ Cinco posiciones en la jerarquía de atención, de hundido a cima: + recessed → flush → raised → overlay → modal. Cada peldaño compone superficie + + sombra + z — míralos trepar hacia ti. +

+
+
+ {#each PLANE_LADDER as plane, i} +
+ {plane} + z · {PLANE_CATALOG.find((p) => p.plane === plane)?.z} +
+ {/each} +
+
+ +

Cada plano, un rol

@@ -183,6 +296,119 @@
+ +
+
+

Catálogo de planos en reposo

+

+ La elevación en reposo de cada familia de componente — el equivalente a la tabla de + especificación de Material, pero en planos nombrados por su rol, no en milímetros. +

+
+
+
+ Plano + Banda z + Componentes + Muestra +
+ {#each PLANE_CATALOG as p} +
+ {p.plane} + {p.z} + {p.use} + +
+ {/each} +
+
+ + +
+
+

Luz vs sombra

+

+ La misma tarjeta overlay, los dos modos a la vez. En claro manda la + sombra proyectada y el halo es invisible. En oscuro la sombra apenas se ve — + toma el relevo el halo: un rim de luz computado en oklab. La señal se + adapta al modo en vez de mentir. +

+
+
+
+
+ claro + la sombra define el relieve +
+
+
+
+ oscuro + el halo toma el relevo +
+
+
+
+ + +
+
+

La jaula abierta

+

+ Opinión fuerte que nunca atrapa. El plano overlay trae una sombra + canónica — pero la mezcla es tuya: retúnea por config, anula por componente, o baja al + primitivo crudo. La misma tarjeta, cuatro mezclas — cámbiala y míralo en vivo. +

+
+ +
+
+
+ data-depth="overlay" + Sombra {SHADOW_VARIANTS.find((v) => v.key === shadowKey)?.label} + el plano no cambia — su mezcla, sí +
+
+ +
+
+ {#each SHADOW_VARIANTS as v} + + {/each} +
+ box-shadow: {activeShadow}; +

+ canónica = el plano sin tocar · de marca = mezcla OKLCH con el color + primario · primitivo crudo = var(--shadow-3), siempre a un paso. En + una app real esto vive en EidosConfig.depth o en un override de recipe — el + inline aquí es solo el atajo más corto a la misma puerta. +

+
+
+
+ + +
+
+

Accesibilidad

+

La profundidad acompaña — nunca carga sola con el significado.

+
+
+ {#each [{ t: 'Nunca el único canal', d: 'Cada plano va con borde, superficie y posición — no solo sombra. Quítale la sombra y la jerarquía sigue legible.' }, { t: 'Respeta reduced-motion', d: 'El ascenso eventful (la firma) degrada a un cambio de estado instantáneo cuando se prefiere menos movimiento.' }, { t: 'Sobrevive a forced-colors', d: 'En alto contraste el box-shadow desaparece; el plano se apoya en borde y superficie, que el modo sí respeta.' }, { t: 'Contraste, no relieve', d: 'La profundidad no sustituye al contraste de texto: el contenido cumple APCA en cualquier plano.' }] as item} +
+ {item.t} + {item.d} +
+ {/each} +
+
+