f13fc37b5a
Layout - the panel is a cascade of columns: the rail's tabs, the tab's chips, the open chip's sub-chips, then the ruler. A child column no longer hides the column it came from (TEMP -> COLOR TEMP keeps TEMP visible); chips stack one per row instead of wrapping - FRAME's WATERMARK opens its own column, so the frame chips stay put - CREATE RECIPES gets the wide column its two-up form needs WB colour swatches - the ruler draws a colour box under the slider that follows the value: COLOR TEMP is the Kelvin colour (Tanner Helland), TINT runs green -10 -> neutral 0 -> magenta +10 HDF EFFECT - knee 0.55..0.85 -> 0.45..0.75, blur 0.004+0.015n -> 0.006+0.024n of the width, screen alpha 0.15+0.35n -> 0.28+0.52n: a wide halo on the highlights instead of a hairline glow. Web copy of toneShader only — the phone keeps its own tuning. Tabs - rail order is PRESETS, FAVORITED, WB, LIGHT, FX, FRAME, CREATE RECIPES
111 lines
3.5 KiB
TypeScript
111 lines
3.5 KiB
TypeScript
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;
|
|
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;
|
|
// "<" — closes the ruler and reopens the strip it was launched from.
|
|
onBack: () => void;
|
|
}
|
|
|
|
export function ChipRow({ chips }: { chips: ChipDef[] }) {
|
|
return (
|
|
<div className="chip-row">
|
|
{chips.map((chip) => (
|
|
<button
|
|
key={chip.key}
|
|
type="button"
|
|
data-key={chip.key}
|
|
className={`chip${chip.active ? ' on' : ''}${chip.amberValue ? ' amber' : ''}`}
|
|
aria-pressed={!!chip.active}
|
|
disabled={chip.disabled}
|
|
onClick={chip.onClick}
|
|
>
|
|
{chip.label}
|
|
{chip.value ? <span className="val">{chip.value}</span> : null}
|
|
{chip.pro ? <span className="pro">PRO</span> : null}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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<HTMLDivElement>(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 (
|
|
<div className="slider-row" ref={rowRef}>
|
|
<button type="button" className="back" onClick={slider.onBack} aria-label="back"><</button>
|
|
<div className="ruler">
|
|
<div className="head">
|
|
<span>{slider.label}</span>
|
|
<b className={slider.edited ? 'amber' : ''}>{slider.display}</b>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min={slider.min}
|
|
max={slider.max}
|
|
step={slider.step}
|
|
value={slider.value}
|
|
onChange={(e) => slider.onChange(Number(e.target.value))}
|
|
/>
|
|
{slider.swatch ? (
|
|
<div
|
|
className="swatch"
|
|
data-key={`swatch-${slider.key}`}
|
|
data-color={slider.swatch}
|
|
style={{ background: slider.swatch }}
|
|
title={slider.swatch}
|
|
>
|
|
<span>{slider.swatch}</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|