You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/gradient-finish-plan-2026-0...

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.

  • 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.

/* 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).

Powered by TurnKey Linux.