diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx
index 1fcc988..6ae8668 100644
--- a/packages/frontend/src/App.tsx
+++ b/packages/frontend/src/App.tsx
@@ -6,6 +6,7 @@ import { Build } from "./pages/Build";
import { Server } from "./pages/Server";
import { Toolset } from "./pages/Toolset";
import { Repos } from "./pages/Repos";
+import { Settings } from "./pages/Settings";
import { Setup } from "./pages/Setup";
import { IDELayout } from "./layouts/IDELayout";
import { SetupLayout } from "./layouts/SetupLayout";
@@ -88,6 +89,7 @@ export function App() {
} />
} />
} />
+ } />
diff --git a/packages/frontend/src/pages/Settings.tsx b/packages/frontend/src/pages/Settings.tsx
new file mode 100644
index 0000000..55e7d92
--- /dev/null
+++ b/packages/frontend/src/pages/Settings.tsx
@@ -0,0 +1,323 @@
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import { api } from "../services/api";
+import { useTheme } from "../hooks/useTheme";
+
+function Section({
+ title,
+ children,
+}: {
+ title: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+}
+
+function GitHubSection({ config }: { config: Record }) {
+ const [editing, setEditing] = useState(false);
+ const [pat, setPat] = useState("");
+ const [saving, setSaving] = useState(false);
+ const [msg, setMsg] = useState("");
+
+ const currentPat = (config.githubPat as string) || "";
+ const masked = currentPat ? currentPat.slice(0, 8) + "\u2022".repeat(20) : "Not set";
+
+ const save = async () => {
+ setSaving(true);
+ setMsg("");
+ try {
+ const { login } = await api.github.validatePat(pat);
+ await api.workspace.updateConfig({ githubPat: pat });
+ setMsg(`Updated \u2014 authenticated as ${login}`);
+ setEditing(false);
+ setPat("");
+ } catch (err) {
+ setMsg(err instanceof Error ? err.message : "Validation failed");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Personal Access Token
+
+
+ {masked}
+
+
+ {!editing ? (
+
+ ) : (
+
+ setPat(e.target.value)}
+ placeholder="ghp_..."
+ className="flex-1 rounded px-3 py-1.5 text-sm"
+ style={{
+ backgroundColor: "var(--forge-bg)",
+ border: "1px solid var(--forge-border)",
+ color: "var(--forge-text)",
+ }}
+ />
+
+
+
+ )}
+ {msg && (
+
+ {msg}
+
+ )}
+
+
+ );
+}
+
+function ThemeSection() {
+ const { theme, toggleTheme } = useTheme();
+
+ return (
+
+
+
+ {theme === "dark" ? "Dark" : "Light"} Mode
+
+
+
+
+ );
+}
+
+function PathsSection({ config }: { config: Record }) {
+ return (
+
+
+
+
+ Workspace Path
+
+
+ {(config.WORKSPACE_PATH as string) || "Not set"}
+
+
+
+
+ NWN Home Path
+
+
+ {(config.NWN_HOME_PATH as string) || "Not set"}
+
+
+
+
+ );
+}
+
+function DockerSection() {
+ const images = ["layonara/unified", "mariadb", "layonara/builder"];
+ const [pulling, setPulling] = useState>({});
+ const [status, setStatus] = useState>({});
+
+ const pull = async (image: string) => {
+ setPulling((s) => ({ ...s, [image]: true }));
+ setStatus((s) => ({ ...s, [image]: "" }));
+ try {
+ await api.docker.pull(image);
+ setStatus((s) => ({ ...s, [image]: "Pulled" }));
+ } catch (err) {
+ setStatus((s) => ({
+ ...s,
+ [image]: err instanceof Error ? err.message : "Failed",
+ }));
+ } finally {
+ setPulling((s) => ({ ...s, [image]: false }));
+ }
+ };
+
+ return (
+
+
+ {images.map((image) => (
+
+
+ {image}
+
+
+ {status[image] && (
+
+ {status[image]}
+
+ )}
+
+
+
+ ))}
+
+
+ );
+}
+
+const SHORTCUTS = [
+ { keys: "Ctrl+Shift+B", action: "Go to Build" },
+ { keys: "Ctrl+`", action: "Toggle Terminal" },
+ { keys: "Ctrl+Shift+F", action: "Go to Editor / Search" },
+ { keys: "Ctrl+Shift+G", action: "Go to Repos" },
+ { keys: "Ctrl+,", action: "Go to Settings" },
+];
+
+function ShortcutsSection() {
+ return (
+
+
+ {SHORTCUTS.map((s) => (
+
+
+ {s.action}
+
+
+ {s.keys}
+
+
+ ))}
+
+
+ );
+}
+
+function AboutSection() {
+ return (
+
+
+ Layonara Forge v0.0.1
+
+
+ github.com/Layonara/layonara-forge
+
+
+ );
+}
+
+function ResetSection() {
+ const navigate = useNavigate();
+
+ const reset = async () => {
+ try {
+ await api.workspace.updateConfig({ setupComplete: false });
+ } catch {
+ // continue anyway
+ }
+ navigate("/setup");
+ };
+
+ return (
+
+
+
+ );
+}
+
+export function Settings() {
+ const [config, setConfig] = useState>({});
+
+ useEffect(() => {
+ api.workspace.getConfig().then(setConfig).catch(() => {});
+ }, []);
+
+ return (
+
+ );
+}