import { useEffect, useRef } from 'react';
// One strip at a time — the same shape as the phone's AdjustmentPanel: a ruler
// row for the open slider, an options strip for the open group, then the tab's
// chip row. Chip labels are the phone's own strings (they are not translated
// there either).
export interface ChipDef {
key: string;
label: string;
value?: string;
// amber border while this chip's row/strip is open, amber text when the value
// it carries is not its default.
active?: boolean;
amberValue?: boolean;
pro?: boolean;
disabled?: boolean;
// CSS colour drawn as a dot on the chip's leading edge — the HSL mixer's band
// chips name a colour, so they show it too.
color?: string;
// The chip's own FILL and the text colour that fill can carry (WB's preset
// table: the swatch the pick paints, printed on the button itself). `bg` wins
// over the class's own background, so an active button keeps its swatch and
// shows its state through the border alone.
bg?: string;
fg?: string;
// A rule instead of a button: the line the row is read across, between two
// sets that are not the same kind of thing (the mixer's PICK, the bands, the
// three image-wide knobs). It takes a line of its own and nothing else — the
// label it separates is the chip below it.
divider?: boolean;
// Present when the chip itself can be dragged somewhere: the payload the drop
// target reads back (the recipe's id, for the FAVORITED rail button).
drag?: string;
// HOVER PREVIEW: a look the pointer lays on the photo before any click. Only
// the chips that carry a whole look (a sim, a recipe) have one; the pair is
// always both or neither, so leaving the chip always takes it back off.
onHover?: () => void;
onHoverEnd?: () => void;
onClick: () => void;
}
export interface SliderDef {
key: string;
label: string;
value: number;
min: number;
max: number;
step: number;
display: string;
// amber value readout once the knob leaves its default (phone's ruler row)
edited: boolean;
// CSS colour the knob is currently driving (WB's temperature/tint), drawn as a
// swatch under the ruler so the hue moves with the number. Absent = no swatch.
swatch?: string;
onChange: (value: number) => void;
// Double-clicking the track puts the parameter back to its default.
onReset: () => void;
// "<" — closes the ruler and reopens the strip it was launched from. Left out
// by a panel that IS the strip: a mask's own column has nowhere to go back to.
onBack?: () => void;
}
// `grid` lays the row out as the table a set of same-kind values is read as
// (WB's presets, LIGHT's two Color Chromes) instead of the wrapping strip the
// option rows use.
export function ChipRow({ chips, grid }: { chips: ChipDef[]; grid?: boolean }) {
return (
{chips.map((chip) =>
chip.divider ? (
) : (
)
)}
);
}
export function SliderRow({ slider }: { slider: SliderDef }) {
// The wheel belongs to the open ruler: one notch is one step, held inside the
// parameter's own range. The listener is native because React's is passive —
// the panel must not scroll away under the cursor while the value moves.
const rowRef = useRef(null);
const live = useRef(slider);
live.current = slider;
useEffect(() => {
const el = rowRef.current;
if (!el) return;
const onWheel = (e: WheelEvent) => {
e.preventDefault();
const s = live.current;
const step = s.step || 1;
const next = Math.min(s.max, Math.max(s.min, s.value + (e.deltaY < 0 ? step : -step)));
if (next !== s.value) s.onChange(next);
};
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, []);
return (
// No data-key on the row itself: the parameter's own key is already on the
// chip that opened it, and a second element answering to it would make a
// probe's own selector ambiguous.
{slider.onBack ? (
) : null}
{/* Label, track, readout: one line, the way a ruler is read — the name
of the knob at the head of the row, the number it is on at the foot,
and the travel between them taking the width that is left. */}
);
}
// The same ruler without its own column: one knob of the mixer's inline panel,
// where HUE/SAT/LUM sit side by side under the colour they are moving — and, in
// the same shape, the knobs a gradient mask carries. Wheel steps it, double-click
// puts it back to 0. A mask's knobs are not the mixer's -10..+10 integers (an
// exposure is a stop, a feather is a percentage), so the range and the readout
// come in with them; the defaults are the mixer's own, unchanged.
export function MiniSlider({
label,
value,
min = -10,
max = 10,
step = 1,
prefix = 'hsl-knob',
format,
defaultValue = 0,
track,
dataKey,
onChange,
onReset,
}: {
label: string;
value: number;
min?: number;
max?: number;
step?: number;
prefix?: string;
format?: (value: number) => string;
// The value the readout stays grey at. 0 is the mixer's own default, but the
// studio's WB rows are not the only knobs here: a temperature's default is
// 5500K and a grain size's is 100, so the caller that knows says so.
defaultValue?: number;
// A coloured track for the two WB knobs (CSS track-temp / track-tint), whose
// travel is a hue rather than a number.
track?: 'temp' | 'tint';
// Replaces the `-