import { useEffect, useState, useRef } from "react" import { Toolbar } from "./components/Toolbar" import { TitleBar } from "./components/TitleBar" import { LeftSidebar } from "./components/LeftSidebar" import { CanvasView } from "./components/CanvasView" import { PropertiesPanel } from "./components/PropertiesPanel" import { StatusBar } from "./components/StatusBar" import { KeyboardCheatSheet } from "./components/KeyboardCheatSheet" import { ResizeHandle } from "./components/ResizeHandle" import { useStore } from "./store/useStore" import { loadTemplateDialog } from "./utils/loadFile" const MIN_SIDEBAR = 160 const MAX_SIDEBAR = 500 export default function App() { const containerRef = useRef(null) const [showCheatSheet, setShowCheatSheet] = useState(false) const [leftWidth, setLeftWidth] = useState(280) const [rightWidth, setRightWidth] = useState(320) const [leftCollapsed, setLeftCollapsed] = useState(false) const [rightCollapsed, setRightCollapsed] = useState(false) const setToolMode = useStore((s) => s.setToolMode) const undo = useStore((s) => s.undo) const deleteSection = useStore((s) => s.deleteSection) const selectedSectionIdx = useStore((s) => s.selectedSectionIdx) const toolMode = useStore((s) => s.toolMode) const currentBlockName = useStore((s) => s.currentBlockName) const darkMode = useStore((s) => s.darkMode) useEffect(() => { document.documentElement.classList.toggle("dark", darkMode) window.electronAPI?.setTheme(darkMode) }, [darkMode]) useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { const target = e.target as HTMLElement if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") return const ctrl = e.ctrlKey || e.metaKey if (e.key === "Escape") { setShowCheatSheet(false); return } if (e.key === "?") { e.preventDefault(); setShowCheatSheet((v) => !v); return } switch (e.key.toLowerCase()) { case "s": { e.preventDefault() if (ctrl) { const state = useStore.getState() const errors = state.validateExport() if (errors.length > 0) { alert("Validation errors:\n" + errors.map((e) => ` - ${e}`).join("\n")) return } const data = state.buildExportData() const json = JSON.stringify(data, null, 2) if (window.electronAPI) { window.electronAPI.saveFileDialog("metadata.json", [{ name: "JSON", extensions: ["json"] }]) .then((path: string) => { if (!path) { state.setStatusMessage("Export cancelled"); return } return window.electronAPI!.writeFile(path, json) }) .then(() => state.setStatusMessage("Exported successfully")) .catch((err: Error) => { state.setStatusMessage("Export failed"); console.error(err) }) } else { const blob = new Blob([json], { type: "application/json" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = "metadata.json" a.click() URL.revokeObjectURL(url) state.setStatusMessage("Exported metadata.json") } } else { setToolMode("select") } break } case "o": if (ctrl) { e.preventDefault() loadTemplateDialog().then((result) => { if (!result) return useStore.getState().setTemplate(result.dataUrl, result.filePath, result.width, result.height) useStore.getState().setStatusMessage(`Loaded: ${result.name}`) }) } break case "z": if (ctrl) { e.preventDefault() undo() } break case "delete": case "backspace": if (selectedSectionIdx !== null) { e.preventDefault() deleteSection(selectedSectionIdx) } break case "d": if (!ctrl) { e.preventDefault(); setToolMode("draw") } break case "p": if (!ctrl) { e.preventDefault(); setToolMode("pan") } break case "arrowup": case "arrowdown": case "arrowleft": case "arrowright": if (toolMode === "select" && selectedSectionIdx !== null && currentBlockName) { e.preventDefault() const step = e.shiftKey ? 10 : 1 const state = useStore.getState() const block = state.blocks[currentBlockName] if (!block) break const roi = block.sections[selectedSectionIdx] if (!roi) break const [x1, x2, y1, y2] = roi.cords let nx1 = x1, nx2 = x2, ny1 = y1, ny2 = y2 switch (e.key.toLowerCase()) { case "arrowup": ny1 -= step; ny2 -= step; break case "arrowdown": ny1 += step; ny2 += step; break case "arrowleft": nx1 -= step; nx2 -= step; break case "arrowright": nx1 += step; nx2 += step; break } state.pushUndo() state.updateSectionCords(selectedSectionIdx, [nx1, nx2, ny1, ny2]) state.setStatusMessage(`Nudged to (${nx1}, ${ny1})`) } break } } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) }, [setToolMode, undo, deleteSection, selectedSectionIdx, toolMode, currentBlockName]) return (
setLeftCollapsed((v) => !v)} /> {!leftCollapsed && setLeftWidth(Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, clientX)))} />} {!rightCollapsed && { if (!containerRef.current) return const cr = containerRef.current.getBoundingClientRect() setRightWidth(Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, cr.right - clientX))) }} />} setRightCollapsed((v) => !v)} />
{showCheatSheet && setShowCheatSheet(false)} />}
) }