10 KiB
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.
colorsigue diciendo quién es el componente (rol / escala / custom) — intacto.- El prop nuevo
gradientdecide cómo se pinta su fill saturado: el wrapper eidos estampa el attrdata-gradient(eidos-only, familiadata-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<image>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 shorthandbackground:— 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 slotcontrast. El contraste solo puede mejorar → seguridad constructiva, dial sin topes (0 regresiones hasta 40%). Guard:gradient-finish-guard.test.ts.
/* 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(default26%), emitido por el generador desdeprimitives.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 engenerated/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 asolid); 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 <number>: 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<Box>(patrón Section) y estampadata-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.namedopta gradientes del open cage como acabado CON tinta autorada obligatoria (--gradient-{name}-ink, override de--_{c}-fga (0,3,0)); la base sigue siendo el solid de la identidad (fill por capas) →aurorashipped 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-onmínimo (v2.2 — D12):<Surface on="dark">re-vincula--color-content-*/--color-border-defaultpara 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).