feat: add generic GFF visual editor component

This commit is contained in:
plenarius
2026-04-20 20:09:24 -04:00
parent c7f5646e62
commit 5620e38282
3 changed files with 654 additions and 26 deletions
+104 -26
View File
@@ -1,17 +1,28 @@
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import { MonacoEditor } from "../components/editor/MonacoEditor";
import { EditorTabs } from "../components/editor/EditorTabs";
import { GffEditor } from "../components/gff/GffEditor";
const GFF_EXTENSIONS = [".uti.json", ".utc.json", ".are.json", ".dlg.json", ".utp.json", ".utm.json"];
function isGffFile(tabKey: string): boolean {
return GFF_EXTENSIONS.some((ext) => tabKey.endsWith(ext));
}
function repoFromTabKey(tabKey: string): string {
const idx = tabKey.indexOf(":");
return idx > 0 ? tabKey.slice(0, idx) : "";
}
function filePathFromTabKey(tabKey: string): string {
const idx = tabKey.indexOf(":");
return idx > 0 ? tabKey.slice(idx + 1) : tabKey;
}
interface EditorProps {
editorState: ReturnType<typeof import("../hooks/useEditorState").useEditorState>;
}
function displayName(tabKey: string): string {
const parts = tabKey.split(":");
const filePath = parts.length > 1 ? parts.slice(1).join(":") : tabKey;
return filePath.split("/").pop() ?? filePath;
}
export function Editor({ editorState }: EditorProps) {
const {
openTabs,
@@ -21,8 +32,12 @@ export function Editor({ editorState }: EditorProps) {
closeFile,
updateContent,
getContent,
markClean,
} = editorState;
// Track per-tab editor mode: "visual" or "raw". GFF files default to visual.
const [editorModes, setEditorModes] = useState<Record<string, "visual" | "raw">>({});
const tabs = useMemo(
() =>
openTabs.map((path: string) => ({
@@ -33,11 +48,13 @@ export function Editor({ editorState }: EditorProps) {
);
const activeContent = activeTab ? (getContent(activeTab) ?? "") : "";
const activeFilePath = activeTab
? activeTab.includes(":")
? activeTab.split(":").slice(1).join(":")
: activeTab
: "";
const activeFilePath = activeTab ? filePathFromTabKey(activeTab) : "";
const activeRepo = activeTab ? repoFromTabKey(activeTab) : "";
const isActiveGff = activeTab ? isGffFile(activeTab) : false;
const activeMode = activeTab
? editorModes[activeTab] ?? (isActiveGff ? "visual" : "raw")
: "raw";
const handleChange = useCallback(
(value: string) => {
@@ -48,6 +65,80 @@ export function Editor({ editorState }: EditorProps) {
[activeTab, updateContent],
);
const handleSwitchToRaw = useCallback(() => {
if (activeTab) {
setEditorModes((prev) => ({ ...prev, [activeTab]: "raw" }));
}
}, [activeTab]);
const handleSwitchToVisual = useCallback(() => {
if (activeTab) {
setEditorModes((prev) => ({ ...prev, [activeTab]: "visual" }));
}
}, [activeTab]);
const handleGffSave = useCallback(
(newContent: string) => {
if (activeTab) {
updateContent(activeTab, newContent);
markClean(activeTab);
}
},
[activeTab, updateContent, markClean],
);
const renderEditor = () => {
if (!activeTab) {
return (
<div className="flex h-full items-center justify-center">
<p style={{ color: "var(--forge-text-secondary)" }} className="text-lg">
Open a file from the File Explorer to start editing
</p>
</div>
);
}
if (isActiveGff && activeMode === "visual") {
return (
<GffEditor
key={activeTab}
repo={activeRepo}
filePath={activeFilePath}
content={activeContent}
onSave={handleGffSave}
onSwitchToRaw={handleSwitchToRaw}
/>
);
}
return (
<div className="flex h-full flex-col">
{isActiveGff && activeMode === "raw" && (
<div
className="flex shrink-0 items-center justify-end border-b px-4 py-1"
style={{ borderColor: "var(--forge-border)", backgroundColor: "var(--forge-surface)" }}
>
<button
onClick={handleSwitchToVisual}
className="rounded px-3 py-1 text-xs transition-colors hover:opacity-80"
style={{ backgroundColor: "var(--forge-bg)", color: "var(--forge-text-secondary)" }}
>
Switch to Visual Editor
</button>
</div>
)}
<div className="flex-1 overflow-hidden">
<MonacoEditor
key={activeTab}
filePath={activeFilePath}
content={activeContent}
onChange={handleChange}
/>
</div>
</div>
);
};
return (
<div className="flex h-full flex-col" style={{ backgroundColor: "var(--forge-bg)" }}>
<EditorTabs
@@ -57,20 +148,7 @@ export function Editor({ editorState }: EditorProps) {
onClose={closeFile}
/>
<div className="flex-1 overflow-hidden">
{activeTab ? (
<MonacoEditor
key={activeTab}
filePath={activeFilePath}
content={activeContent}
onChange={handleChange}
/>
) : (
<div className="flex h-full items-center justify-center">
<p style={{ color: "var(--forge-text-secondary)" }} className="text-lg">
Open a file from the File Explorer to start editing
</p>
</div>
)}
{renderEditor()}
</div>
</div>
);