# Gradient finish — plan de implementación (2026-07) > Estado: **v1 en ejecución** (2026-07-15). Diseño cerrado tras tres análisis + revisión > de coherencia + investigación de frameworks de referencia + mockup interactivo > verificado en Chrome real. Este documento es el plan operativo; la doctrina queda > en `docs/theming/reference.md` (sección «Gradient finish») cuando aterrice. ## La decisión (resumen ejecutivo) **El gradiente es un ACABADO (finish) del fill, no una identidad de color.** - `color` sigue diciendo *quién* es el componente (rol / escala / custom) — intacto. - El prop nuevo `gradient` decide *cómo se pinta su fill saturado*: el wrapper eidos estampa el attr `data-gradient` (eidos-only, familia `data-variant`/`data-size`; NO se declara en morfo — ver la fila del checklist: el runtime lo resolvería desde el espacio de props de soma y clobbearía el stamp). - Taxonomía: color = identidad · variant = porte · depth/frost = material · state layer = feedback → el gradiente entra como **material/acabado**, pariente de frost, jamás como valor de `data-color`. (El diseño «gradiente como valor del eje color» fue analizado y descartado: un `` no puede cumplir el contrato del eje — 10 slots derivados mecánicamente + toda variante lo expresa. La historia completa vive en la memoria del proyecto y en el mockup.) ## La receta v1 (rampa derivada, un dial) Fill por capas: ``` background-color = la base sólida (--_{c}-bg — sin cambio; degrada en forced-colors) background-image = acabado (--_{c}-fill-finish) [+ veil del state layer, ver «Diferido»] ``` El acabado se compone EN CSS desde los slots que la identidad ya provee, escalado por **un solo dial del tema**. > **Desviación declarada al implementar (2026-07-15):** el hover del recipe es > `[data-button]:hover:not(...):not(...)` = (0,4,0) con shorthand `background:` — > un re-assert genérico del generador a (0,3,0) PERDERÍA. Reparto final: > **el generador emite solo la VAR** (`--_{c}-fill-finish`) y **el recipe pinta** > (base + su propio hover guard) — coherente con «el generador fabrica, el > recipe decide dónde pinta», como las variantes. > **Rectificación medida (2026-07-15, antes de commitear):** el lift global > hacia blanco rompía la tinta heredada en **52/84** combos a 26% (sonda con > floors APCA≥60∧WCAG≥3; techo global seguro = 0% — teal 1%, azules/verdes de > modo oscuro 0%: el paso 9 no tiene margen hacia el blanco en media paleta). > Forma FINAL: **rampa ANCLADA al lado de sombra de la tinta** («la rampa huye > de la tinta») — tinta blanca → ancla `#000`, extremo fuerte abajo (CTA > sombreado); tinta oscura → ancla `#fff`, extremo fuerte arriba (glossy). > Ancla + ángulo se resuelven por color×modo con el MISMO flip del slot > `contrast`. El contraste solo puede mejorar → seguridad constructiva, dial > sin topes (0 regresiones hasta 40%). Guard: `gradient-finish-guard.test.ts`. ```css /* generated/base.css — renderRecipeGradientFinish (gate v1: button, badge) */ [data-button][data-gradient] { --_button-finish-anchor: var(--color-primary-finish-anchor); --_button-finish-angle: var(--color-primary-finish-angle); --_button-fill-finish: linear-gradient( var(--_button-finish-angle), color-mix(in oklch, var(--button-palette-solid), var(--_button-finish-anchor) calc(var(--gradient-finish-lift) / 3)), color-mix(in oklch, var(--button-palette-solid-hover), var(--_button-finish-anchor) var(--gradient-finish-lift)) ); } [data-button][data-gradient][data-color] { --_button-finish-anchor: var(--palette-finish-anchor, var(--color-primary-finish-anchor)); --_button-finish-angle: var(--palette-finish-angle, var(--color-primary-finish-angle)); } /* button.css — el recipe pinta el fill saturado + re-assert en hover */ [data-button][data-gradient][data-variant='solid'] { background-image: var(--_button-fill-finish); } [data-button][data-gradient][data-variant='solid']:hover:not([data-disabled]):not([data-loading]) { background-image: var(--_button-fill-finish); } ``` - **Dial**: `--gradient-finish-lift` (default `26%`), emitido por el generador desde `primitives.gradientFinish.lift` — mismo patrón que las magnitudes del state layer (config data, §38/§40 del theming reference). `lift: 0%` ≈ acabado apagado. - **Derivación**: claro = `solid + lift` · profundo = `solid-hover − lift/3`. Un dial escala toda la rampa. Override por instancia gratis (cascada). - **Tinta**: HEREDADA (`palette-contrast`) — el extremo profundo (paso 10) ya está validado por práctica shipped; el extremo claro (lift) lo acota el guard de build (pendiente, ver Cola). - **Por qué no saltar pasos (9→7 / 9→11)**: la escala de 12 pasos es funcional y relativa al modo (7 = borde, 11 = texto; en oscuro el 11 es claro `#d19dff` → la rampa se invertiría y mataría la tinta; en claro 9/10/11 están apiñados, ΔL ≈ 0.04). El único ancla estable entre modos es el solid — por eso el lift se define relativo al solid. Valores verificados en `generated/base.css`. ## Fases ### v1 — rampa derivada en Button (EN EJECUCIÓN) | Pieza | Fichero | Estado | | --- | --- | --- | | Config `gradientFinish.lift` | `src/uix/eidos/lib/config-types.ts` | ✅ 2026-07-15 | | Default `26%` | `src/uix/eidos/lib/primitives/static.ts` | ✅ | | Emisión `--gradient-finish-lift` | `src/uix/eidos/lib/render-css.ts` | ✅ | | Var finish por recipe (gate v1: button) + pintura en recipe | `render-css.ts` + `button.css` | ✅ (ver desviación) | | Attr `data-gradient` | eidos-only de wrapper (familia `data-variant`) — **NO morfo**: declararlo hizo que el runtime lo resolviera desde props de soma → `undefined` → `mergeProps` clobbereó el stamp (bug cazado en vivo 2026-07-15 y revertido) | ✅ | | Prop `gradient?: boolean` + estampado | `src/uix/eidos/components/button/{button.svelte,types.ts}` | ✅ | | Regenerar `generated/base.css` | `npm run generate:eidos-css` | ✅ (dial en `:root:224`, var en `:4123`) | | README de Button (prop nueva) | `src/uix/eidos/components/button/README.md` | ✅ | | Lab del eje | `web/routes/temas/gradientes/+page.svelte` | ✅ | | Doctrina + changelog | `reference.md §39` + `changelog.md §42` | ✅ | | Checks | contrato 26/28 (2 fallos preexistentes de `proof-of-human`, ajenos) · audit 144/144 · eidos-lint button `invalid: 0` · svelte-check sin errores nuevos | ✅ | | Navegador real (lab + dial + hover + claro/oscuro) | — | ☐ | ### v1.x — cola inmediata (no bloquea v1) - ✅ **Guard del dial** (2026-07-15): `src/uix/eidos/gradient-finish-guard.test.ts` — no-regresión ≤40% sobre 84 combos + set flat-fail clavado (cyan/orange, deuda preexistente del on-solid). Con la rampa anclada el guard pasa 3/3. - ✅ **Badge** (2026-07-15): gate ampliado (paleta privada; sin `solid-hover` → extremo profundo cae a `solid`); attr eidos-only de wrapper (NO morfo). - ✅ **Override por tema/modo del lift** (2026-07-15): `ThemeDefinition.gradientFinish.lift` → re-emisión en el bloque de tema (misma especificidad, después en cascada → el tema gana; `0%` apaga por tema). Test: `active-eidos-config.test.ts` («per-theme gradient-finish dial override»). **v1.x COMPLETA.** - **Composición del veil del state layer**: `background-image: veil, var(--_{c}-fill-finish, none)` a nivel archetype — solo necesaria cuando un componente que DEPENDE del veil (item/option) gane gradiente; Button/Badge no lo necesitan (su hover re-asserta). ### v1.5 — kind `spread` ✅ (2026-07-15) MEDIDO ANTES DE IMPLEMENTAR (doctrina D6b, esta vez en orden): (1) sonda de 84 combos — la rotación pura a L constante rompía grass (±4°) y gold (±27°) porque L de OKLCH ≠ luminancia; con la mezcla débil del ancla (lift/3) en ambos stops: **0 fallos hasta ±45°**; (2) RCS en Chromium por valores computados — el canal `h` es ``: `calc(h ± 30deg)` computa `none` → el token `--gradient-finish-spread` es SIN unidad (`'30'`). Shipped: `gradient="spread"` en Button+Badge (`data-gradient='spread'` overridea la var del acabado; cero cambios de recipe CSS), guard 4/4, lab con especímenes reales (incl. gris ≈plano, C≈0). Registro: D10 en `docs/theming/gradient-finish.md`. ### v2 — named finishes + Surface - ✅ **Primitiva `Surface`** (v2.1, 2026-07-15): Box + tratamiento — compone `` (patrón Section) y estampa `data-surface` + `color`/`variant` (soft/solid)/`gradient` (ambos kinds)/`rounded`; recipe palette-tint espejo de Card sin chrome (`_palette-*` 5×8 → THM-2: roles + 33 escalas gratis); morfo declarativo patrón Box (0 eventos justificados); audit **145/145**; verificada por valores en navegador (rampa/spread/soft/tinta contrast). Ficha con Decisiones/Gaps propios: `components/surface/README.md`. - ✅ **Named finishes** (v2.2, 2026-07-15 — D11): `gradientFinish.named` opta gradientes del open cage como acabado CON tinta autorada obligatoria (`--gradient-{name}-ink`, override de `--_{c}-fg` a (0,3,0)); la base sigue siendo el solid de la identidad (fill por capas) → `aurora` shipped como blobs de rol con alfa SIN color base final. Honestidad: la validación numérica al peor stop solo es posible para gradientes de modelo (`buildGradient`) — diferida y documentada; el guard clava la sanidad del config (named ⊆ gradients + ink presente). - ✅ **`on`/`data-on` mínimo** (v2.2 — D12): `` re-vincula `--color-content-*`/`--color-border-default` para el subárbol (verificado en vivo: muted = tinta del contexto al 64%); forced-colors → `CanvasText`. Límites POR CONSTRUCCIÓN: anidados con tokens propios y portales excluidos — la inversión COMPLETA de subárbol sigue siendo iniciativa independiente (Card solid la necesita hoy sin gradientes). **EL PLAN ESTÁ COMPLETO (2026-07-15).** Fuera del plan quedan, declarados: la iniciativa de inversión completa de subárbol, la demo propia `web/routes/uix/components/surface` (gap en la ficha de Surface) y la validación de modelo para tintas de named finishes. ## Referencias - Mockup interactivo de los 9 casos (verificado en Chrome real, dial en vivo): artifact `38814ae1-7922-46b2-b344-12dc9528316d` (claude.ai). - Memoria del proyecto: `project_gradient_as_color_2026-07-14` (historia completa: diseño A descartado, mapa de 3 mecanismos de fondo, research de referencia, revisión de coherencia).