LIGHT drops its PROFILE panel, TONE CURVE folds into TONE, and IMPORT .RECIPE moves under the RECIPES chip

Three small moves in the column, plus the rule they needed.

LIGHT: the PROFILE panel is gone — PHOTO STYLE under PRESETS already covers
what it did — and the tone curve chip leaves the column's chip row. It becomes
the last line of the TONE panel's body, at the foot TONE always had room for.

PRESETS: IMPORT .RECIPE leaves the recipes strip and sits in the main column,
directly under the RECIPES chip, which is the set it imports into. The strip now
carries the recipes and nothing else, so its empty hint fires at length zero.

HSL: the mixer's PICK and the IMAGE label each get a rule above them. The rule
is a chip — divider: true in ChipDef — that renders a separator instead of a
button, so a row can say that what follows is a different kind of thing.

Verified by probe against the built app (exit 0, no console errors):
- LIGHT panel heads read WB, TONE, PRESENCE, DETAIL & EFFECTS, with no PROFILE;
  the TONE body's last row is the "∿ TONE CURVE" chip, box [94,647,300,27].
- PRESETS main chips read PHOTO STYLE, RECIPES, IMPORT .RECIPE (y 159, under
  RECIPES at y 124), RESET; the strip no longer carries the import chip.
- HSL order is PICK [94,91,148,27], rule [94,127,148,1], the eight bands, rule
  [94,404,148,1], IMAGE, then HUE/SAT/LUM. The same order holds at 393x852,
  where the rules measure [8,560,377,1] and [8,639,377,1].
- The rule is a 1px line: width 100% with height 1px, which keeps it a line in
  both the desktop column and the phone's wrapped strip.

Co-authored-by: PenguinHarness <noreply@penguin.local>
This commit is contained in:
2026-09-30 06:55:53 +07:00
parent 4476372b9e
commit f7031c1809
4 changed files with 124 additions and 83 deletions
+44 -35
View File
@@ -23,6 +23,11 @@ export interface ChipDef {
// 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;
@@ -61,41 +66,45 @@ export interface SliderDef {
export function ChipRow({ chips, grid }: { chips: ChipDef[]; grid?: boolean }) {
return (
<div className={`chip-row${grid ? ' grid' : ''}`}>
{chips.map((chip) => (
<button
key={chip.key}
type="button"
data-key={chip.key}
className={`chip${chip.active ? ' on' : ''}${chip.amberValue ? ' amber' : ''}${chip.bg ? ' tinted' : ''}`}
style={chip.bg ? { background: chip.bg, color: chip.fg } : undefined}
aria-pressed={!!chip.active}
disabled={chip.disabled}
draggable={!!chip.drag}
onDragStart={
chip.drag
? (e) => {
e.dataTransfer.setData('text/plain', chip.drag!);
e.dataTransfer.effectAllowed = 'copy';
}
: undefined
}
onMouseEnter={chip.onHover}
onMouseLeave={chip.onHoverEnd}
// The click takes the preview off first: a click that applies the look
// (and closes the column the chip was in) would otherwise leave a
// preview nothing is left to take off — and every later knob would
// move the recipe under a photo still painted from the preview.
onClick={() => {
chip.onHoverEnd?.();
chip.onClick();
}}
>
{chip.color ? <span className="chip-dot" data-color={chip.color} style={{ background: chip.color }} /> : null}
{chip.label}
{chip.value ? <span className="val">{chip.value}</span> : null}
{chip.pro ? <span className="pro">PRO</span> : null}
</button>
))}
{chips.map((chip) =>
chip.divider ? (
<span key={chip.key} className="chip-rule" role="separator" aria-orientation="horizontal" />
) : (
<button
key={chip.key}
type="button"
data-key={chip.key}
className={`chip${chip.active ? ' on' : ''}${chip.amberValue ? ' amber' : ''}${chip.bg ? ' tinted' : ''}`}
style={chip.bg ? { background: chip.bg, color: chip.fg } : undefined}
aria-pressed={!!chip.active}
disabled={chip.disabled}
draggable={!!chip.drag}
onDragStart={
chip.drag
? (e) => {
e.dataTransfer.setData('text/plain', chip.drag!);
e.dataTransfer.effectAllowed = 'copy';
}
: undefined
}
onMouseEnter={chip.onHover}
onMouseLeave={chip.onHoverEnd}
// The click takes the preview off first: a click that applies the look
// (and closes the column the chip was in) would otherwise leave a
// preview nothing is left to take off — and every later knob would
// move the recipe under a photo still painted from the preview.
onClick={() => {
chip.onHoverEnd?.();
chip.onClick();
}}
>
{chip.color ? <span className="chip-dot" data-color={chip.color} style={{ background: chip.color }} /> : null}
{chip.label}
{chip.value ? <span className="val">{chip.value}</span> : null}
{chip.pro ? <span className="pro">PRO</span> : null}
</button>
)
)}
</div>
);
}
+11 -9
View File
@@ -24,21 +24,23 @@ const PANELS: {
// A slot rides inside its panel, where the parent's own select belongs in the
// reading order — before the panel's knobs except in DETAIL & EFFECTS, whose
// D.RANGE selects close the column.
slot?: 'profile' | 'wb' | 'effects';
slot?: 'wb' | 'effects';
// A pick that closes its panel rather than opening it: drawn under the rows,
// where WB's two Color Chromes belong (they are read after the pair of tracks
// they are the other axis of).
foot?: 'wbFoot';
// they are the other axis of) and where TONE's curve belongs, standing under
// the range it is the shape of.
foot?: 'wbFoot' | 'toneFoot';
slotFirst?: boolean;
}[] = [
// PROFILE is the film simulation and nothing else: EV's ruler is a knob of the
// TONE panel's, the range the picture is on, so it sits with the rest of them
// and not with the stock the frame is shot on.
{ slug: 'profile', title: 'PROFILE', rows: [], slot: 'profile', slotFirst: true },
{ slug: 'wb', title: 'WB', rows: ['temperature', 'tint'], slot: 'wb', slotFirst: true, foot: 'wbFoot' },
// EV leads TONE: it is the one number the whole range hangs off, and the
// exposure below it is the finer move on the same axis.
{ slug: 'tone', title: 'TONE', rows: ['ev', 'exposure', 'contrast', 'highlight', 'shadow', 'whites', 'blacks'] },
{
slug: 'tone',
title: 'TONE',
rows: ['ev', 'exposure', 'contrast', 'highlight', 'shadow', 'whites', 'blacks'],
foot: 'toneFoot',
},
{ slug: 'presence', title: 'PRESENCE', rows: ['clarity', 'dehaze', 'vibrance', 'color'] },
{
slug: 'effects',
@@ -113,7 +115,7 @@ export function DevelopPanels({
set: (patch: Partial<ColorAdjustments>) => void;
pro?: boolean;
onPro?: () => void;
slots?: { profile?: ReactNode; wb?: ReactNode; wbFoot?: ReactNode; effects?: ReactNode };
slots?: { wb?: ReactNode; wbFoot?: ReactNode; toneFoot?: ReactNode; effects?: ReactNode };
}) {
const [open, setOpen] = useState<Record<string, boolean>>(() => {
const saved = new Set((recall(OPEN_KEY) ?? '').split(','));