import { useEffect, useRef } from 'react'; import { keep, recall } from '../remember'; // 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. `list` is the other end of it: the rows that are a LIST OF // NAMES — MY PHOTOS' frames, FAVORITED and RECIPES' looks, PHOTO STYLE's sims — // drop the button's chrome and read as plain text, with a lead glyph saying what // kind of thing the name is and only the accent marking the one in force (see // .chip-row.list). export function ChipRow({ chips, grid, grid3, list, glyph, }: { chips: ChipDef[]; grid?: boolean; grid3?: boolean; list?: boolean; // The mark drawn at the head of every row of a `list` (a frame, a look). It // is decoration and nothing else — the name beside it is the row. glyph?: string; }) { return (
{chips.map((chip) => chip.divider ? ( ) : ( ) )}
); } // The edge of a column you can drag: the width is written on the column itself // and remembered (remember.ts), so the next visit opens at the size it was left // at. Drag, not a panel: the grip appears where the column's edge already is. // ponytail: the DOM takes the width and React never learns it — a column width // is presentation, not state, and a re-render carrying it would fight the drag. export function ColGrip({ col, min = 110, max = 560 }: { col: string; min?: number; max?: number }) { const ref = useRef(null); const key = `studio-col-${col}`; useEffect(() => { const box = ref.current?.parentElement; const kept = recall(key); if (box && kept) box.style.width = kept; }, [key]); return (
{ const box = ref.current?.parentElement; if (!box) return; e.preventDefault(); const from = e.clientX; const start = box.getBoundingClientRect().width; const move = (m: PointerEvent) => { box.style.width = `${Math.round(Math.min(max, Math.max(min, start + m.clientX - from)))}px`; }; const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); keep(key, box.style.width); }; window.addEventListener('pointermove', move); window.addEventListener('pointerup', up); }} /> ); } 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. */}
{slider.label} slider.onChange(Number(e.target.value))} onDoubleClick={slider.onReset} /> {slider.display}
{slider.swatch ? (
{slider.swatch}
) : null}
); } // 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 `-