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/uix/soma/components/gradient-builder/CONTINUE.md

22 KiB

GradientBuilder — continuation handoff (Phase 2)

Read this first, then docs/README.md for the framework. This component is the interactive gradient editor over the canonical Gradient model.

Polished the GradientBuilder toward "done". All type-clean (npm run check = 59, baseline — nothing new). Still on ACTIVE_DEV_TRACK. Every item below verified in-browser.

Done:

  • Stop color editor (gradient-builder-stop-color.svelte, the per-stop Popover ColorPicker):
    • Value row = the canonical <ColorField><ColorField.Input swatch>…segments…</> (swatch + the framework's styled Select), NOT the old ChannelInput snippet that rendered a native <select>.
    • Rails (hue/alpha) at 90% width + taller track (--slider-track-size-xl).
    • Composable footer: <ColorPicker.Footer> = <ColorPicker.Cancel> (reverts the stop to its open-edge colour via a stopColorOnOpen snapshot) + <ColorPicker.Close> ("Aceptar") + the gradient-specific trash (remove-stop). The Footer is a SIBLING of PickerShell.Body, not inside it.
  • Surface-agnostic: default variant is now ghost (transparent, no card) so the builder adapts to its container; surface/outline opt into a card. The DEMO must NOT wrap it in a card (user was explicit).
  • + add-stop on the same row as the kind switch (Lineal/Radial/Cónico).
  • Add-stop placement: provider.betweenSelectedAndNext() (between the selected stop and the next; if last, penultimate↔last). Rail click-to-add re-enabled (inserts at the cursor) as the primary path; + = coordinate-less fallback.
  • StopList (gradient-builder-stop-list.svelte + -row) — morfo parts stop-list/stop-list-row. Row = a ColorField-value-looking swatch (click → the same color editor) + position % + delete (except the two endpoint stops). Rendered in the DEFAULT editor (always visible).
  • Presets (gradient-builder-presets.svelte) — presets prop (Gradient[]; defined even [] shows the panel). Panel BELOW the editor (flex, NOT tabs — tabs are for the GradientPicker). A preset is a full Gradient, so selecting it (provider.reset) sets the kind too. Save mechanic (ColorPicker saved-swatch style): one action button — "Guardar degradado" (saves current; disabled if already present or at maxPresets, default 10), flips to "Borrar degradado" (threat) when a SAVED preset is selected (data-selected). savedPresets = ephemeral $state in the root. Morfo parts presets/preset.

Platform reference scan (background workflow, 2026-07-04): linear/radial/conic IS the standard complete set (don't add "angular" = conic; diamond = optional radial sub-mode; mesh = separate-tool, correctly deferred). Add-stop: every tool inserts at the clicked rail position. Stops-list: legit opt-in (cssgradient.io / Untitled UI). Our keyboard-accessible role=slider stops + shared OKLCH token model exceed every incumbent.

PENDING — start here next session:

  1. GradientPicker (the popover wrapper — NOT built this session). Design (user's words): a Popover + PickerShell with two tabs [Builder | Presets] + a footer (Clear/Cancel/Close, already in gradient-picker-content.svelte). The Builder tab uses <GradientBuilder presets={…} /> whole (black box); the Presets tab is the picker's. The tabs live in the PICKER, not the builder. NOTE: the current gradient-picker-content.svelte re-composes the builder's parts (Preview/Track/…) — change it to render <GradientBuilder> whole.
  2. Numeric editable position field on the stop rows.
  3. Reverse/flip control (trivial on the provider).
  4. READMEs (eidos builder README stale) → then remove gradient-builder / gradient-picker from ACTIVE_DEV_TRACK and make the catalogue guards green.

Gotchas: ColorPicker.ChannelInput is a render-prop (empty without children). The stop-color popover inherits the shared spin-field.css box — strip it with compound [data-…][data-spin-field] selectors. A {#snippet} as a DIRECT child of a component becomes a snippet PROP — define it at markup top level, render inside. Callback props must be camelCase (onApply, not onapply). variant='ghost' = surface-agnostic; the demo must NOT add its own card.

Redesign pass #3 (2026-06-27 — interaction + scaling feedback)

User feedback (all addressed, verified by EYE in the user's Chrome):

  • Color editing is now ON-CLICK, not always visible: clicking a stop opens a color editor panel (Area + hue + alpha + hex, with a header "Parada de color N"
    • × close) for that stop. Removed the always-visible area/sliders. provider.colorEditOpen + openColorEdit/closeColorEdit; startStopDrag opens it. The StopColor component self-hides via {#if provider.colorEditOpen}.
  • Add-stop lands in the WIDEST GAP (largestGapPosition()), never stacking on 0.5 over existing stops. addStop() (no arg) = widest gap; addStop(pos) = explicit (track click). Verified: 3 stops [0,.5,1] → new at .75.
  • Delete affordance: a RemoveStop trash Button in the stop-actions row (removes the selected stop; disabled at 2 stops). Keyboard Delete still works.
  • Kind switch is always-selected + full width: deselectable={false} + block on the ToggleGroup. The current kind is always highlighted.
  • Preview bar taller (72px md, was 44).
  • Scaling: dimensions now calc(px * var(--scaling, 1)) so the editor zooms with GLOBAL --scaling. Verified: global :root --scaling 1.4 → button 36→50, preview 72→101, gap 16→22.4 (all scale). CAVEAT — the demo's System density/scaling axes are applied LOCALLY on the stage (style="--scaling" / data-density), which does NOT recompute the root-baked --space-* / --control-height-* tokens — so the local sliders don't resize ANY component (universal harness limitation, not gradient-builder). Confirmed: control-height reads calc(36px * 1 * 1) regardless of local --scaling. Fix would be a shared harness change (e.g. zoom on the stage) — deferred, not unilateral.
  • Dark mode: card is now --color-surface-raised + --color-border-default so it reads as a distinct panel (was blending into the page surface).
  • Commit-set ring fix (the "border on slider move"): the commit firma (commit-settle keyframe) pulses a primary box-shadow ring on the [data-event-family='commit'][data-event-phase='active'] target. The builder's commit-set targets the provider CARD and was firing on EVERY angle/color tick (setAngle/setStopColor called commit()). Fix: those setters are now live-update-only; the angle dial commits on onValueCommit (release), the color editor on onValueChangeEnd — one ring pulse on release, not per pixel.

Redesign pass #2 (2026-06-27 — after "no funciona / mal diseñado / no sigue la guía")

User feedback was correct: the first eidos pass shipped an ungrouped pile of controls with a broken color editor and a demo that ignored the v2 demo guide. Fixed:

  • StopColor is now INLINE (gradient-builder-stop-color.svelte): the ColorPicker Area + hue/alpha sliders + hex render directly (no nested popover). The seed effect initialises colorValue from the selected stop UP FRONT (was showing the black default), tracks only selectedIndex. NOTE: the segmented ChannelInput recurses in SSR outside a Popover Content — use ValueText (read-only hex) inline; the segmented editor only works inside the popover.
  • Default full-editor layout: <GradientBuilder bind:value /> with no children renders Preview → Track → (KindSwitch + AddStop) → AngleDial → StopColor. The Provider is now a framed card (bg + border + padding, size-aware); variant (surface/outline/ghost) changes the framing (was inert). AngleDial gained a label + degree readout. Verified by EYE in light + dark.
  • Both demos rewritten to the v2 guide (web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md, canary = Button): data-uix-canvas-inner + header/meta-pills + always-on stage with System axes + the 9 tabs (Live · System · Motion · Sema · Services · API · Morfo · Recipe · A11y) + the shared harness (SystemAxes / MotionPanel / SemaPanel / DemoTrace). No PalettePicker — neither component has a color prop (they paint the user's gradient), justified in the demo + README. Now at gradient-builder/+page.svelte and gradient-picker/+page.svelte (split).
  • 4 READMEs written (soma + eidos for both). svelte-check clean, contracts test unchanged (12 pre-existing, none gradient-*).
  • Verified in the user's real browser (Chrome MCP — preview_screenshot hangs): builder editor coherent in light+dark; Sema tab renders all 4 firmas; picker popover opens showing the builder + Borrar/Cancelar footer; selected-stop color correct (#7c3aed / #ec489a, not black).

Status (updated 2026-06-27 — GradientBuilder + GradientPicker DONE)

Layer GradientBuilder GradientPicker
morfo committed (c474fc6b) DONE (uncommitted) morfo/components/gradient-picker.ts (expression:'delegated', identity wrapper over Popover)
soma committed (8ddd5530) + value getter DONE (uncommitted) soma/components/gradient-picker/ (Provider/Trigger/ValueSwatch + composed PopoverProvider + pickerShellContext commit/cancel/clear)
eidos DONE (uncommitted) 12 files DONE (uncommitted) eidos/components/gradient-picker/ (Trigger+chip, Content = Popover+PickerShell+GradientBuilder body)
sema DONE (uncommitted) + wired in /uix layout n/a (only commit-reset; editing sounds come from the embedded builder)
translations DONE langs/components/gradient-builder.ts DONE langs/components/gradient-picker.ts
demo (testbed) DONE web/routes/uix/components/gradient-builder/+page.svelte (both shown there) included in the same testbed

Both are in ACTIVE_DEV_TRACK in src/uix/contracts.test.ts (catalogue guards skip them while WIP).

What remains:

  1. READMEs — soma + eidos component READMEs for both (needed before the catalogue guards go green).
  2. Canonical-depth demos — split into gradient-builder/ + gradient-picker/ routes at full DEMO_AUTHORING_GUIDE 6-tab depth (the current single +page.svelte is a working testbed).
  3. Remove both from ACTIVE_DEV_TRACK once 1 + 2 land and the soma/eidos guards pass.

GradientPicker — how it works (the value bridge is the one subtle bit)

  • Identity wrapper over a composed Popover (PopoverProvider shares the open writable). GradientPickerProvider exposes commit()/cancel()/clear() via pickerShellContext so <PickerShell.Clear/Cancel/Close> drive it. valueOnOpen snapshot (captured on the open edge via watch) powers cancel()'s revert.
  • The eidos Content composes PopoverContent → PickerShell → GradientBuilder body + Footer(Clear/Cancel/Close). The GradientBuilder edits a local draft ($state); its onValueCommit pushes settled values to provider.setValue; an $effect (if (v !== draft) draft = v) flows external resets (cancel/clear) back. Commit-only push + the ref guard = no loop.
  • Trigger + ValueSwatch paint --gp-current-gradient (stamped inline by soma) — the chip shows the committed gradient.
  • Verified in-browser: one click opens → builder renders inside (2 stops) → footer Borrar/Cancelar/Listo → AddStop commits live (chip updates) → Cancel reverts chip to the open-edge value + closes. aria-expanded/data-state correct, no console errors. svelte-check clean, contracts.test unchanged (12 pre-existing fails, none gradient-*).

What the eidos layer ships (the build composed, never re-implemented)

  • gradient-builder.svelte (Provider, sets a size/variant visual context + self-imports the child recipes)
  • gradient-builder-preview.svelte (decorative live bar; checker behind for alpha)
  • gradient-builder-track.svelte (paints a left→right ramp of the live stops; default-renders one Stop per stop)
  • gradient-builder-stop.svelte (positions the soma slider-thumb at left:%, fills via --gb-stop-fill, centers with the translate property so a press-squeeze transform doesn't fight it)
  • gradient-builder-stop-color.svelte (embeds the full eidos ColorPicker bound to the selected stop; bridge in stop-color.ts)
  • gradient-builder-angle-dial.svelte (eidos Slider 0–360 → setAngle; shown for linear/conic only)
  • gradient-builder-kind-switch.svelte (eidos ToggleGroup linear/radial/conic → setKind)
  • gradient-builder-add-stop.svelte (eidos Button iconOnly + Plus → addStop(0.5))
  • stop-color.ts (the StopColor ↔ ColorValue bridge — css/oklch read, writes back a css hex stop)
  • context.svelte.ts, types.ts, index.ts, gradient-builder.css (self-contained --_gradient-builder-* recipe — gradient-builder is on the active dev track, so it deliberately does NOT register tokens in recipes/base.ts yet; migrate when the foreign base.ts palette WIP is committed)

Verification done (2026-06-27)

  • svelte-check: gradient-builder eidos/soma/sema/langs files all clean (0 errors).
  • SSR (curl against vite): HTTP 200, every part present, stops positioned + colored, linear-gradient(90deg in oklch, …) serialized, aria-valuetext="Stop n of N, p%", translated kind labels (Lineal/Radial/Cónico), no no_context / SSR errors.
  • Computed styles (preview eval): track position:relative + live gradient bg; stop position:absolute left:0 width:16px round + #7c3aed fill + translate:-50% -50%; preview h48px.
  • Interaction (preview eval): AddStop adds a stop; KindSwitch flips data-kind and hides the AngleDial for radial / shows it for linear+conic. No console errors.
  • preview_screenshot hangs in this environment (known headless-browser flakiness, see Gotchas) — verified via SSR DOM + computed styles + eval interaction instead of a literal screenshot.
  • Tests: contracts.test.ts (12 pre-existing fails: chronos/metrics/menu-dial/float-panel) + compile.test.ts hoists role into staticAttrs (pre-existing, from the committed picker/dialog reconciliation cae3d2e0) — none gradient-builder; zero regressions added.

Where it sits — the gradient initiative

  • Axis (Phase 1) — DONE + committed (c474fc6b): $libs/gradient (model + gradientToCss) + eidos/lib/build-gradient.ts (role factories + applyGradients, the 6th builder) + docs. The axis DERIVES gradients from color roles.
  • Component (Phase 2) — morfo + soma DONE + committed (c474fc6b morfo, 8ddd5530 soma): the editor over the SAME Gradient model, so a built gradient is also a themeable token. ← you are here; build the eidos.

The shared model — $libs/gradient (already built)

type Gradient = LinearGradient | RadialGradient | ConicGradient | MeshGradient
interface GradientStop { color: StopColor; position?: number /*0..1*/; alpha?: number }
type StopColor =
  | { kind: 'role'; role: string; slot?: string; step?: number }  // → var(--color-…) — RE-TINTS
  | { kind: 'oklch'; value: Oklch }
  | { kind: 'css'; value: string }                                 // transparent / currentColor
// gradientToCss(g) → a CSS <gradient> string. See $libs/gradient/README.md.

v1 edits linear/radial/conic (.stops). Mesh editing is the fast-follow.

The soma API you compose (already built) — $soma/components/gradient-builder

import * as GradientBuilder from '$soma/components/gradient-builder'
// <GradientBuilder.Provider bind:value> · .Track · .Stop  (wrappers)
import { GradientBuilderProvider } from '$soma/components/gradient-builder'  // for controls

GradientBuilderProvider (get via .require() inside the subtree) exposes the state machine — call these from the eidos controls:

  • stops (derived), kind, angle, selectedIndex, activeDragIndex
  • stopPosition(i), selectStop(i), moveStop(i, 0..1), nudgeStop(i, Δ)
  • addStop(0..1), removeStop(i), setStopColor(i, StopColor), setStopAlpha(i, 0..1)
  • setKind('linear'|'radial'|'conic'), setAngle(deg), commit(), reset(g)
  • positionFromPointer(clientX) (against the registered track)

Each Stop is already a keyboard-accessible role=slider (arrows ±1% / Shift ±10%, Home/End, Delete removes, Enter selects) + pointer drag — the a11y differentiator. The eidos just RENDERS them positioned + colored; the soma owns behaviour.

Build the EIDOS — src/uix/eidos/components/gradient-builder/

Mirror the color-picker eidos (src/uix/eidos/components/color-picker/ — the closest template: how it composes the soma, reuses ColorPicker.Area/ChannelSlider, sets a visual context, and writes the recipe). Eidos rule: compose existing components (Slider, ColorPicker, ToggleGroup, Button), never re-implement.

Files to create:

File Composes / does
gradient-builder.svelte (Provider) <GradientBuilder.Provider> + sets a visual context (size/variant); the root
gradient-builder-preview.svelte the gradient bar — background: {gradientToCss(value)} (or a --gb-preview var); decorative
gradient-builder-track.svelte <GradientBuilder.Track>; background = the live gradient; hosts the stops
gradient-builder-stop.svelte <GradientBuilder.Stop {index}> positioned left: {position}%, filled with the stop colour; the draggable handle
gradient-builder-stop-color.svelte embed ColorPicker bound to the SELECTED stop's colour (read provider.stops[selectedIndex].color, write provider.setStopColor). Reuse the whole ColorPicker.
gradient-builder-angle-dial.svelte <Slider> 0–360 → provider.setAngle; show for linear/conic only
gradient-builder-kind-switch.svelte <ToggleGroup> linear/radial/conic → provider.setKind
gradient-builder-add-stop.svelte <Button> → provider.addStop(0.5)
gradient-builder.css the recipe (track height, stop handle, preview, focus ring). Bare --gradient-builder-* / --_gradient-builder-* tokens.
types.ts + index.ts props + the compound namespace (Provider/Preview/Track/Stop/StopColor/AngleDial/KindSwitch/AddStop)

Stop ↔ ColorPicker bridge is the one non-obvious bit: the selected stop's colour opens a ColorPicker; its onValueChange calls provider.setStopColor(selectedIndex, …). Map StopColor ↔ the ColorPicker value (a {kind:'oklch'} or {kind:'css'} stop ↔ a color string). Role-stops ({kind:'role'}) can show a swatch row of theme roles (reuse ColorPicker.SwatchGroup) instead of/alongside the area.

Then — GradientPicker (mirror ColorPicker exactly)

A SEPARATE component (gradient-picker.ts morfo + soma + eidos) = trigger + Popover wrapping the GradientBuilder, identical structure to color-picker (Trigger / ValueSwatch / Content via PopoverContent + PickerShellRoot / the shell Footer/Clear/Cancel/Close). The ValueSwatch shows gradientToCss(value).

Sema pack — src/uix/sema/components/gradient-builder.ts

Cascade rules via semaSelector(gradientBuilderMorfo, part, matchers) (NEVER hand-written selector strings). The morfo events are handle-pick/handle-drag (family handle → haptic on drag) + commit-set/commit-reset. Mirror slider.ts / color-picker.ts sema packs. Wire it into the demo layout's events.components.

Translations + finish

  • Add the #?components.gradient-builder.* keys (track / add-stop / angle-dial / kind-switch) to the langs catalog (find where color-picker's keys live).
  • Demo at web/routes/uix/components/gradient-builder/+page.svelte (read DEMO_AUTHORING_GUIDE + a reference demo first — canonical depth, every prop a live control). Or fold a showcase into /demos/cristal.
  • When the eidos + sema + translations exist, remove 'gradient-builder' from ACTIVE_DEV_TRACK in src/uix/contracts.test.ts and make the catalogue guards green for it (soma module ↔ morfo scope, eidos recipe ↔ scope, README, etc.).

Design notes from the editor research (don't re-derive)

The competitive finding: every incumbent (Figma/Photoshop/web pickers/gradient.style) stores RGBA + geometry and exports a dead string; nobody stores an OKLCH model shared between a token engine and an editor, nor edits gradients from the keyboard. Eidos owns both. Decisions already taken:

  • Geometry = explicit per-kind fields (angle / shape+size+at / from+at), not an affine matrix — round-trips losslessly to CSS, trivial keyboard mapping.
  • Alpha = per-stop in v1 (a separate opacity rail is reserved, not shipped).
  • Mesh editor = fast-follow (the axis already ships aurora()); diamond dropped.
  • Dual surfaces (a precise stop bar + an on-canvas gizmo) is the target; v1 = the bar.
  • Midpoints (color hints) + parseGradient (paste-to-edit) = Phase-2.5.

Gotchas (cost real cycles this session)

  • JS spring / rAF freezes in the automated browser tab (throttled to 0fps when unfocused) — you CANNOT see a uix.motion spring complete via the preview/Chrome MCP unless the tab is focused. CSS animations (compositor) DO render. Verify motion in the user's FOCUSED tab. (The cristal scroll-reveal hit this.)
  • Dev server is flaky this branch: ports 5173/5180 come and go; preview_start may conflict with another chat's server. preview_screenshot hangs on heavy backdrop-filter — use the Claude-in-Chrome MCP against the user's real tab.
  • An eidos-component demo under web/routes needs a +layout@.svelte that boots createActiveUix + Soma.create() + ActiveEidos.create() (mirror web/routes/temas/grafito/+layout@.svelte), else SSR 500 no_context.
  • contracts.test.ts has ~12 PRE-EXISTING failures from other sessions' WIP (metrics, chronos, menu-dial, float-panel) — NOT yours. gradient-builder is excluded via ACTIVE_DEV_TRACK (both soma collectors now filter it uniformly).
  • Don't touch words/, palabras/, chronos/, or the other session's date/time-picker + picker-shell WIP. Stage explicit paths after git reset -q.

Powered by TurnKey Linux.