// EditorPanel — the designer's right-hand controls: card-level options // (scrim, name, default) and per-selection editing (font, size, rotation, // style preset + params; insert width/border; QSO box geometry). import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; import { cn } from '@/lib/utils'; import type { CardTemplate, CardElement, QSOBox, QSLPresetInfo, StyleParams } from './qslTypes'; import type { CardSelection } from './CardPreview'; import { StylePresetPicker, NumberField } from './StylePresetPicker'; interface Props { template: CardTemplate; sel: CardSelection; presets: QSLPresetInfo[]; fontFamilies: string[]; // embedded + detected system faces onPatchElement: (idx: number, patch: Partial) => void; onPatchBox: (patch: Partial) => void; onScrim: (enabled: boolean) => void; onSelect?: (sel: CardSelection) => void; } const TYPE_LABELS: Record = { callsign: 'Callsign', operator: 'Operator name', info_line: 'Info line', country: 'Country + flag', insert: 'Photo insert', }; // A readable layer name, disambiguating the two info lines and numbering inserts. function layerLabel(el: CardElement, i: number, all: CardElement[]): string { if (el.type === 'info_line') { return /rig|antenna|ant\b/i.test(el.text ?? '') ? 'Station (rig / ant)' : 'Info line (zones)'; } if (el.type === 'insert') { const n = all.slice(0, i + 1).filter((x) => x.type === 'insert').length; return `Photo insert ${n}`; } return TYPE_LABELS[el.type] ?? el.type; } function TextControls({ e, idx, presets, fontFamilies, onPatch }: { e: CardElement; idx: number; presets: QSLPresetInfo[]; fontFamilies: string[]; onPatch: (idx: number, patch: Partial) => void; }) { return (
onPatch(idx, { size })} /> onPatch(idx, { rotate })} />
onPatch(idx, { text: ev.target.value })} />
onPatch(idx, { style_preset, style_params })} />
); } export function EditorPanel({ template, sel, presets, fontFamilies, onPatchElement, onPatchBox, onScrim, onSelect }: Props) { const e = typeof sel === 'number' ? template.elements[sel] : undefined; const box = template.qso_box; return (
Card
{/* Layers: show/hide each piece, click a name to edit it. */}
Show on card
{template.elements.map((el, i) => (
onPatchElement(i, { hidden: v !== true })} />
))} {box && (
onPatchBox({ enabled: v === true })} />
)}
{e ? TYPE_LABELS[e.type] ?? e.type : sel === 'box' ? 'QSO box' : 'Selection'}
{!e && sel !== 'box' && (

Click an element on the card to edit it; drag to move it.

)} {e && (e.type === 'callsign' || e.type === 'operator' || e.type === 'info_line') && ( )} {e && e.type === 'insert' && (
onPatchElement(sel as number, { w })} /> onPatchElement(sel as number, { border_px })} /> onPatchElement(sel as number, { rotate })} />
)} {e && e.type === 'country' && ( onPatchElement(sel as number, { size })} /> )} {sel === 'box' && box && (
onPatchBox({ w })} /> onPatchBox({ h })} />
onPatchBox({ footer: ev.target.value })} />
)}

Text supports {'{profile.*}'} and {'{qso.*}'} placeholders — they fill in automatically on every card.

); }