014b3e5a4b
Cursor over the image: the wheel zooms about the pointer, and a drag pans, both clamped to the contain-fit of the loaded bitmap so the photo can never be lost off-screen. Cursor over an open parameter ruler: the wheel moves that parameter by one step, clamped to its min/max. While the crop frame is being dragged the wheel keeps resizing the crop instead, and APPLY puts the view back on the fit.
97 lines
3.0 KiB
TypeScript
97 lines
3.0 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;
|
|
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))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|