|
|
# 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 `<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 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 `<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 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): `<Surface on="dark">` 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).
|