import { useEffect, useState, useCallback, useMemo } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { toast } from "sonner"; import * as api from "@/api/client"; import { autocompletion } from "@codemirror/autocomplete"; import { useEditorStore } from "@/stores/editorStore"; import { usePagesStore } from "@/stores/pagesStore"; import { useUnsavedGuard } from "@/hooks/useUnsavedGuard"; import { useCompile } from "@/hooks/useCompile"; import { useKeyboardSave } from "@/hooks/useKeyboardSave"; import { uframeHighlight } from "@/components/editor/uframeHighlight"; import { uframeCommandSource, uframeValueHintSource, loadCommandsFromApi } from "@/components/editor/uframeCommands"; import { keywordHoverTooltip } from "@/components/editor/uframeHover"; import EditorPointer from "@/components/editor/EditorPointer"; import PreviewPane from "@/components/editor/PreviewPane"; import SourcePane from "@/components/editor/SourcePane"; import ToolBar from "@/components/editor/ToolBar"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle, } from "@/components/ui/resizable"; export default function EditorView() { const { name } = useParams<{ name: string }>(); const navigate = useNavigate(); const isNew = !name; const ufSource = useEditorStore((s) => s.ufSource); const isDirty = useEditorStore((s) => s.isDirty); const setSource = useEditorStore((s) => s.setSource); const setDirty = useEditorStore((s) => s.setDirty); const setCurrentPage = useEditorStore((s) => s.setCurrentPage); const reset = useEditorStore((s) => s.reset); const { fetchPages } = usePagesStore(); const [pageName, setPageName] = useState(name ?? ""); const [saving, setSaving] = useState(false); // µFrame extensions: syntax highlighting + slash commands const extensions = useMemo( () => [ ...uframeHighlight(), autocompletion({ override: [uframeCommandSource, uframeValueHintSource], icons: false, activateOnTyping: true, }), keywordHoverTooltip, ], [], ); // Load DSL commands from backend registry on mount useEffect(() => { loadCommandsFromApi(); }, []); // Auto-compile on source changes useCompile(); useUnsavedGuard(); // Fetch pages for backlinks useEffect(() => { fetchPages(); }, []); // Load page on mount / route change useEffect(() => { reset(); if (name) { setPageName(name); api.fetchPage(name).then((data) => { if (data.source != null) { setSource(data.source); setDirty(false); } }); } return () => reset(); }, [name]); const handleSave = useCallback( async (publish: boolean) => { const slug = pageName .trim() .toLowerCase() .replace(/[^a-z0-9_-]/g, "-"); if (!slug) { toast.error("Enter a page name."); return; } setSaving(true); try { const meta = await api.savePage(slug, ufSource, publish); setCurrentPage(meta); setDirty(false); fetchPages(); toast.success(publish ? "Published" : "Draft saved"); if (isNew) navigate(`/editor/${slug}`, { replace: true }); } catch (e) { toast.error(`Save failed: ${e}`); } finally { setSaving(false); } }, [pageName, ufSource, isNew, navigate], ); useKeyboardSave( useCallback(() => handleSave(false), [handleSave]), useCallback(() => handleSave(true), [handleSave]), ); return (
handleSave(false)} onPublish={() => handleSave(true)} saving={saving} isDirty={isDirty} />
); }