Files
RecipesCam/docker/frontend/src/ui/ChipColumn.tsx
T
3dtours 014b3e5a4b Zoom and pan the preview with the wheel, and step a parameter from its ruler
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.
2026-09-17 20:57:00 +07:00

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">&lt;</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>
);
}