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/src/libs/gradient/README.md

3.5 KiB

$libs/gradient — the canonical gradient model

A pure, zero-dependency gradient model + CSS serializer, shared by two consumers so a gradient is one thing in both places:

  • the eidos theming axis (uix/eidos/lib/build-gradient.ts) — derives named --gradient-* tokens from color roles;
  • the GradientBuilder component (soma, Phase 2) — the interactive editor.

A gradient built in the editor is therefore also a themeable token, and a token re-opens in the editor. No other system unifies "gradient derived from a color system" with "hand-edited gradient" — that unification is the whole point of keeping the model in a lib both layers can import (below soma, no layer violation).

The model (model.ts)

type Gradient = LinearGradient | RadialGradient | ConicGradient | MeshGradient

interface GradientStop {
  color: StopColor      // role ref | OKLCH literal | raw css
  position?: number     // 0..1 along the line
  alpha?: number        // 0..1
}

type StopColor =
  | { kind: 'role';  role: string; slot?: string; step?: number }  // → var(--color-…) — RE-TINTS
  | { kind: 'oklch'; value: Oklch }                                 // stored perceptual literal
  | { kind: 'css';   value: string }                                // transparent / currentColor

Geometry is explicit per kind (LinearGradient.angle, RadialGradient.shape/size/at, ConicGradient.from/at, MeshGradient.points) so it round-trips losslessly to CSS and maps trivially to a future on-canvas gizmo + keyboard editing — no opaque affine matrix. Colors are stored as Oklch (not hex): the "store OKLCH, not hex" property that keeps gradients gamut-honest and perceptually interpolated.

Why role stops are the headline

A { kind: 'role', … } stop serializes to var(--color-{role}-{slot}) rather than a frozen color. So a single --gradient-{name} token re-tints with the brand seed and flips light/dark for free — Tailwind / Open Props / Panda blend two literal endpoints; Material 3 has no gradient axis at all. Nobody derives the stops themselves from a color system.

The serializer (to-css.ts)

gradientToCss(g: Gradient): string   // model → a CSS <gradient> (or stacked-radial mesh) string
meshToCss(m: MeshGradient): string
stopColorToCss(c: StopColor, alpha?): string
  • Role stops → var(--color-…); OKLCH → oklch(L% C H / a); css → verbatim.
  • Interpolation defaults to in oklch (perceptual; not the muddy sRGB midpoint dead-zone) — emitted geometry-first then in <space> [<hue>], the ordering Tailwind v4 ships in production. Hue paths (longer/shorter) drive aurora / iridescent sweeps from two role endpoints.
  • Mesh serializes to layered radial-gradients over a base — the only portable CSS mesh today (native CSS mesh is unshipped). Re-tints because each blob is a role/OKLCH color in the model.

The OKLCH→hex fallback sibling (wide-gamut honesty for pinned literals) is a consumer concern (eidos has $color); this lib stays pure and emits the oklch() form.

Status

  • ✅ model + gradientToCss (this lib) — tested in to-css.test.ts.
  • ✅ eidos axis (buildGradient / applyGradients) — see uix/eidos.
  • ⏳ parseGradient (CSS → model, lossless round-trip) — Phase 2, what the GradientBuilder needs (paste-to-edit). The model is shaped for it (hints, explicit geometry) but it is not implemented yet.

Powered by TurnKey Linux.