import { useState } from 'react'; import type { ReactNode } from 'react'; import type { ColorAdjustments } from '../../shared/types'; import { PARAM_DEFS } from '../../shared/utils/paramDefs'; import { keep, recall } from '../remember'; import { MiniSlider } from './ChipColumn'; // One lookup for every row. The defs are split over the three tabs' tables and // the sidebar reads them all at once, so the map is built once here; a key that // is not in it draws no row rather than a broken one. const DEF_BY_KEY = new Map( [...PARAM_DEFS.iq, ...PARAM_DEFS.wb, ...PARAM_DEFS.filters].map((def) => [def.key, def]) ); // The stack, top to bottom, and the rows each panel owns. The two WB tracks are // the only rows whose travel is a colour, so the class each input wears is named // here with them; anything else is the bare slider. const TRACK: Record = { temperature: 'temp', tint: 'tint' }; const PANELS: { slug: string; title: string; rows: string[]; // 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 // COLOR CHROME selects close the column. slot?: 'profile' | 'wb' | 'effects'; slotFirst?: boolean; }[] = [ { slug: 'profile', title: 'PROFILE', rows: ['ev'], slot: 'profile', slotFirst: true }, { slug: 'wb', title: 'WB', rows: ['temperature', 'tint'], slot: 'wb', slotFirst: true }, { slug: 'tone', title: 'TONE', rows: ['exposure', 'contrast', 'highlight', 'shadow', 'whites', 'blacks'] }, { slug: 'presence', title: 'PRESENCE', rows: ['clarity', 'dehaze', 'vibrance', 'color'] }, { slug: 'effects', title: 'DETAIL & EFFECTS', rows: ['denoise', 'sharpening', 'vignette', 'hdf', 'grain', 'grainSize'], slot: 'effects', }, ]; function DevRow({ paramKey, adj, set, pro, onPro, }: { paramKey: string; adj: ColorAdjustments; set: (patch: Partial) => void; pro: boolean; onPro?: () => void; }) { const def = DEF_BY_KEY.get(paramKey); if (!def) return null; // A PRO knob on the LITE build is a label with nothing under it: the value // stays at its default, and the row's own click is handed to the parent's // upgrade path instead of a slider that would move nothing on the photo. if (def.pro && !pro) { return ( ); } return ( set(def.set(v))} onReset={() => set(def.set(def.defaultValue))} /> ); } // Which panels the studio draws open, under this key. The list is the panels' // slugs, comma-joined; a slug no panel answers to is dropped on the way in, so a // panel that is renamed or taken out does not leave a hole in the column. const OPEN_KEY = 'recipescam.develop.open'; // The studio's right column: every panel's knobs behind a disclosure that folds // its own rows away, and the column opens with all of them folded — a dozen rows // of sliders is a wall to scroll past, and the knob being looked for is named by // its panel's head. What the reader opens is remembered, so the second visit // opens the way the first was left. One undo step is the parent's `set`. export function DevelopPanels({ adj, set, pro = false, onPro, slots, }: { adj: ColorAdjustments; set: (patch: Partial) => void; pro?: boolean; onPro?: () => void; slots?: { profile?: ReactNode; wb?: ReactNode; effects?: ReactNode }; }) { const [open, setOpen] = useState>(() => { const saved = new Set((recall(OPEN_KEY) ?? '').split(',')); return Object.fromEntries(PANELS.filter((panel) => saved.has(panel.slug)).map((panel) => [panel.slug, true])); }); const fold = (slug: string, isOpen: boolean) => { const next = { ...open, [slug]: !isOpen }; setOpen(next); keep(OPEN_KEY, PANELS.filter((panel) => next[panel.slug]).map((panel) => panel.slug).join(',')); }; return (
{PANELS.map((panel) => { const isOpen = open[panel.slug]; const slot = panel.slot ? slots?.[panel.slot] : null; return (
{isOpen ? (
{panel.slotFirst ? slot : null} {panel.rows.map((key) => ( ))} {panel.slotFirst ? null : slot}
) : null}
); })}
); }