PURPOSE

Controlled React widget that edits a four-vector cosine palette (Palette4) used by the VFX workbench. Renders a live gradient preview strip plus inputs for the three RGB channels (a, b, c) and the three phase axes (d).

OWNS

  • Local color-input handling (setChannel) that converts hex to Vec3 and replaces the indexed channel in the palette tuple.
  • Local phase-input handling (setPhase) that mutates a copy of the d vector by axis and reassembles the Palette4 tuple.
  • A memoized CSS gradient string built from STRIP_STOPS (32) evenly spaced samples of evalPalette(value, t).
  • Inline styles for the section divider, label row, color pickers, phase number inputs, and preview strip.

READS FROM

  • value: Palette4 prop — the current palette tuple [a, b, c, d].
  • @engine/vfx-workbench/palettePalette4, Vec3, vec3ToHex, hexToVec3, evalPalette.
  • reactuseMemo.

PUSHES TO

  • onChange(next: Palette4) prop — emits a new palette tuple whenever a color channel or phase axis changes.

DOES NOT

  • Hold internal state for the palette (fully controlled by the parent via value / onChange).
  • Validate or clamp phase numeric inputs beyond the native min={0}, max={1}, step={0.01} attributes.
  • Surface errors from malformed hex input; the setChannel try block silently ignores invalid hex during typing.
  • Render any label or title other than the static “PALETTE (cosine)” header.
  • Persist the palette anywhere; persistence is the parent’s concern.

Signals

  • data-testid="palette-preview-strip" — the gradient preview div.
  • data-testid="palette-color-a" | "palette-color-b" | "palette-color-c" — the three hex color inputs.
  • data-testid="palette-phase-0" | "palette-phase-1" | "palette-phase-2" — the three phase-axis number inputs.

Entry points

  • Default named export PaletteWidget({ value, onChange }).
  • Mounted by parent VFX-workbench panes that hold a Palette4 and want a compact editor for it.

Pattern notes

  • Fully controlled component — every keystroke / picker change calls onChange with a freshly assembled Palette4 tuple; no internal useState.
  • Channel updates use Array.prototype.map with an index check to build the next tuple immutably, then cast to Palette4.
  • Phase updates spread the existing d vector, mutate one axis, and reassemble the full tuple immutably.
  • Gradient is computed via useMemo keyed on value, sampling evalPalette at 32 stops and emitting a linear-gradient(to right, ...) CSS string.
  • Color inputs use onInput (not onChange) so the parent sees updates while the user drags the native picker.
  • Phase inputs use onChange with parseFloat; non-numeric or empty input produces NaN, which is forwarded to the parent without local guarding.
  • Layout uses a two-column grid (auto 1fr) with a display: contents wrapper to keep each label/input pair on the same row, plus a nested 3-column grid for the phase inputs.
  • Styling is inline only — no class names, no external CSS, no theming hooks; colors and fonts are hard-coded (#222, #888, #aaa, #ddd, #0a0a12, Cal Sans, monospace).