// StylePresetPicker — preset choice + per-preset parameter controls for the // selected text element. Only the knobs the preset declares (params) are // shown, matching the backend whitelist so saving can't fail on a stray key. import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import type { QSLPresetInfo, StyleParams, FxParams } from './qslTypes'; interface Props { presets: QSLPresetInfo[]; preset: string; params: StyleParams; onChange: (preset: string, params: StyleParams) => void; } // Quick colour palettes per FX family (mirrors the reference generators): // each sets the 3-stop gradient plus the dark edge and (glossy) silver rim. type Palette = { name: string; top: string; mid: string; bot: string; dark: string; outer?: string }; const GLOSSY_PALETTES: Palette[] = [ { name: 'Gold', top: '#ffe22d', mid: '#ffd600', bot: '#ffcc00', dark: '#262630', outer: '#ced3db' }, { name: 'Silver', top: '#fbfdff', mid: '#c9d4de', bot: '#8496a8', dark: '#262630', outer: '#e8edf2' }, { name: 'Red', top: '#ff7a66', mid: '#ee3322', bot: '#d42410', dark: '#260b08', outer: '#ead9d6' }, { name: 'Blue', top: '#5fb8ff', mid: '#1f8fe8', bot: '#107ad0', dark: '#0d1726', outer: '#d6e2ee' }, { name: 'Green', top: '#a5e84f', mid: '#6ec424', bot: '#5ab012', dark: '#122108', outer: '#dbe8d2' }, { name: 'Pink', top: '#ff9ed0', mid: '#f5559f', bot: '#e83b8c', dark: '#260818', outer: '#ecd8e3' }, ]; const WESTERN_PALETTES: Palette[] = [ { name: 'Gold', top: '#f7c036', mid: '#f39612', bot: '#d06200', dark: '#20140a' }, { name: 'Red', top: '#f0856e', mid: '#d8402a', bot: '#8c1606', dark: '#1f0805' }, { name: 'Blue', top: '#7fc0ee', mid: '#2a7fc0', bot: '#0c4378', dark: '#081320' }, { name: 'Green', top: '#bfd96a', mid: '#7fa82e', bot: '#3f6210', dark: '#101a06' }, { name: 'Cream', top: '#f7ecd0', mid: '#e8d3a0', bot: '#bf9b58', dark: '#2a1f10' }, ]; function ColorRow({ label, value, onChange }: { label: string; value: string; onChange: (v: string) => void }) { return (
onChange(e.target.value)} />
); } function SliderRow({ label, value, min, max, step, onChange }: { label: string; value: number; min: number; max: number; step: number; onChange: (v: number) => void; }) { return (
onChange(parseFloat(e.target.value))} /> {value}
); } export function StylePresetPicker({ presets, preset, params, onChange }: Props) { const info = presets.find((p) => p.name === preset); const has = (k: string) => info?.params.includes(k) ?? false; const set = (patch: Partial) => onChange(preset, { ...params, ...patch }); // gel_* presets are rendered by the canvas FX, not the SVG stack: show the FX // knobs instead of the old shine/halo/shadow/outline rows. const isFx = preset.startsWith('gel_'); const fxWestern = preset.includes('grunge'); const fx: FxParams = params.fx ?? {}; const setFx = (patch: Partial) => set({ fx: { ...fx, ...patch } }); return (
{isFx && (
{(fxWestern ? WESTERN_PALETTES : GLOSSY_PALETTES).map((p) => (
)} {has('color') && ( set({ color: v })} /> )} {has('gradient') && (
{(params.gradient ?? ['#FFD83A', '#FFC312', '#EE9400']).map((c, i) => ( { const g = [...(params.gradient ?? ['#FFD83A', '#FFC312', '#EE9400'])]; g[i] = e.target.value; set({ gradient: g }); }} /> ))}
)} {!isFx && has('outline_color') && ( set({ outline_color: v })} /> )} {!isFx && has('outline_width') && ( set({ outline_width: v })} /> )} {!isFx && has('shine') && ( set({ shine: { coverage: v, opacity: params.shine?.opacity ?? 0.95 } })} /> )} {!isFx && has('halo') && ( set({ halo: { color: params.halo?.color ?? '#cdd9e4', blur: params.halo?.blur ?? 6, opacity: v }, })} /> )} {!isFx && has('shadow') && ( set({ shadow: { dx: params.shadow?.dx ?? 5, dy: params.shadow?.dy ?? 8, blur: params.shadow?.blur ?? 5, color: params.shadow?.color ?? '#14243a', opacity: v, }, })} /> )} {isFx && !fxWestern && ( <> setFx({ plump: v })} /> setFx({ gloss: v })} /> setFx({ gloss_h: v })} /> setFx({ inner_b: v })} /> setFx({ edge: v })} /> setFx({ outerw: v })} /> )} {isFx && fxWestern && ( <> setFx({ depth: v })} /> setFx({ slant: v })} /> setFx({ grunge: v })} /> setFx({ bevel: v })} /> )}
); } // Tiny labelled numeric input shared by the designer's right panel. export function NumberField({ label, value, onChange, min, max }: { label: string; value: number; onChange: (v: number) => void; min?: number; max?: number; }) { return (
onChange(parseFloat(e.target.value) || 0)} />
); }