Files
RecipesCam/docker/frontend/src/ui/ChipColumn.tsx
T
3dtours f13fc37b5a feat(panel): cascading columns, WB colour swatches, stronger HDF glow
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
2026-09-18 06:42:38 +07:00

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