import { useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'; import type { SheetWidget, SystemDefinition, WidgetType } from '../shared/types.ts'; import { WidgetPreview } from './WidgetPreview.tsx'; const PALETTE: Array<{ type: WidgetType; label: string; defaultBind?: string; w: number; h: number }> = [ { type: 'section', label: 'Section', w: 220, h: 40 }, { type: 'number', label: 'Number', defaultBind: 'attr.str', w: 110, h: 72 }, { type: 'modBadge', label: 'Mod badge', defaultBind: 'derived.strMod', w: 110, h: 72 }, { type: 'dots', label: 'Dots', defaultBind: 'attr.str', w: 160, h: 72 }, { type: 'track', label: 'Track / HP', defaultBind: 'resource.hp', w: 220, h: 80 }, { type: 'skillList', label: 'Skill list', w: 280, h: 240 }, { type: 'notes', label: 'Notes', defaultBind: 'meta.notes', w: 280, h: 140 }, { type: 'locationBar', label: 'Armor zones', w: 360, h: 90 }, { type: 'text', label: 'Text', defaultBind: 'meta.title', w: 200, h: 60 }, ]; type Props = { system: SystemDefinition; onChange: (next: SystemDefinition) => void; locale: 'ru' | 'en'; }; function snap(n: number, grid = 8): number { return Math.round(n / grid) * grid; } export function DesignerWorkspace({ system, onChange, locale }: Props) { const [selectedId, setSelectedId] = useState(null); const [pageIndex, setPageIndex] = useState(0); const dragRef = useRef<{ id: string; mode: 'move' | 'resize'; startX: number; startY: number; orig: { x: number; y: number; w: number; h: number }; } | null>(null); const page = system.sheetLayout.pages[pageIndex] ?? system.sheetLayout.pages[0]; const selected = page?.widgets.find((w) => w.id === selectedId) ?? null; function updateWidgets(mutator: (widgets: SheetWidget[]) => SheetWidget[]) { if (!page) return; const pages = system.sheetLayout.pages.map((p, i) => i === pageIndex ? { ...p, widgets: mutator(p.widgets) } : p, ); onChange({ ...system, sheetLayout: { ...system.sheetLayout, pages } }); } function addWidget(item: (typeof PALETTE)[number]) { if (!page) return; const id = `w_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; const widget: SheetWidget = { id, type: item.type, bind: item.defaultBind, frame: { x: snap(24 + (page.widgets.length % 5) * 16), y: snap(24 + (page.widgets.length % 7) * 16), w: item.w, h: item.h, }, props: { label: item.label, title: item.label, max: item.type === 'dots' ? 5 : 20, }, }; updateWidgets((ws) => [...ws, widget]); setSelectedId(id); } function onPointerDownWidget(e: ReactPointerEvent, id: string, mode: 'move' | 'resize') { e.stopPropagation(); e.preventDefault(); const w = page?.widgets.find((x) => x.id === id); if (!w) return; setSelectedId(id); dragRef.current = { id, mode, startX: e.clientX, startY: e.clientY, orig: { ...w.frame }, }; (e.target as HTMLElement).setPointerCapture?.(e.pointerId); } function onPointerMove(e: ReactPointerEvent) { const d = dragRef.current; if (!d) return; const dx = e.clientX - d.startX; const dy = e.clientY - d.startY; updateWidgets((ws) => ws.map((w) => { if (w.id !== d.id) return w; if (d.mode === 'move') { return { ...w, frame: { ...w.frame, x: snap(Math.max(0, d.orig.x + dx)), y: snap(Math.max(0, d.orig.y + dy)), }, }; } return { ...w, frame: { ...w.frame, w: snap(Math.max(48, d.orig.w + dx)), h: snap(Math.max(36, d.orig.h + dy)), }, }; }), ); } function onPointerUp() { dragRef.current = null; } function patchSelected(patch: Partial & { props?: Record }) { if (!selected) return; updateWidgets((ws) => ws.map((w) => w.id === selected.id ? { ...w, ...patch, props: { ...w.props, ...patch.props }, } : w, ), ); } function removeSelected() { if (!selected) return; updateWidgets((ws) => ws.filter((w) => w.id !== selected.id)); setSelectedId(null); } function addBrick(kind: 'attributes' | 'skills' | 'resources' | 'derived') { const key = `new_${Date.now().toString(36).slice(-4)}`; const bricks = { ...system.bricks }; if (kind === 'attributes') { bricks.attributes = [ ...(bricks.attributes ?? []), { key, label: key.toUpperCase(), scale: 'numeric', min: 1, max: 20, default: 10 }, ]; } else if (kind === 'skills') { bricks.skills = [ ...(bricks.skills ?? []), { key, label: key, scale: 'numeric', default: 0 }, ]; } else if (kind === 'resources') { bricks.resources = [ ...(bricks.resources ?? []), { key, label: key, min: 0, default: 0, track: true }, ]; } else { bricks.derived = [ ...(bricks.derived ?? []), { key, label: key, formula: 'floor((attr.str - 10) / 2)', dependsOn: ['attr.str'], }, ]; } onChange({ ...system, bricks }); } function removeBrick(kind: 'attributes' | 'skills' | 'resources' | 'derived', key: string) { const bricks = { ...system.bricks }; if (kind === 'attributes') bricks.attributes = (bricks.attributes ?? []).filter((a) => a.key !== key); if (kind === 'skills') bricks.skills = (bricks.skills ?? []).filter((a) => a.key !== key); if (kind === 'resources') bricks.resources = (bricks.resources ?? []).filter((a) => a.key !== key); if (kind === 'derived') bricks.derived = (bricks.derived ?? []).filter((a) => a.key !== key); onChange({ ...system, bricks }); } const bindOptions: string[] = []; for (const a of system.bricks.attributes ?? []) bindOptions.push(`attr.${a.key}`); for (const s of system.bricks.skills ?? []) bindOptions.push(`skill.${s.key}`); for (const r of system.bricks.resources ?? []) bindOptions.push(`resource.${r.key}`); for (const d of system.bricks.derived ?? []) bindOptions.push(`derived.${d.key}`); bindOptions.push('meta.notes', 'meta.title'); return (
{locale === 'ru' ? 'Палитра' : 'Palette'}
{PALETTE.map((item) => ( ))}
{locale === 'ru' ? 'Кирпичики' : 'Bricks'}
{(system.bricks.attributes ?? []).map((a) => (
attr.{a.key}
))} {(system.bricks.skills ?? []).map((a) => (
skill.{a.key}
))} {(system.bricks.resources ?? []).map((a) => (
resource.{a.key}
))} {(system.bricks.derived ?? []).map((a) => (
derived.{a.key}
))}
setSelectedId(null)} >
{system.sheetLayout.pages.map((p, i) => ( ))}
{(page?.widgets ?? []).map((w) => (
{ e.stopPropagation(); setSelectedId(w.id); }} onPointerDown={(e) => onPointerDownWidget(e, w.id, 'move')} >
{w.type}{w.bind ? ` · ${w.bind}` : ''}
onPointerDownWidget(e, w.id, 'resize')} />
))}
{locale === 'ru' ? 'Свойства' : 'Properties'}
{!selected ? (
{locale === 'ru' ? 'Выберите виджет' : 'Select a widget'}
) : (
)}
System