From 8df7a78c085b6a8473ec7a553cae2d7f1ae721eb Mon Sep 17 00:00:00 2001 From: plenarius Date: Mon, 20 Apr 2026 22:11:03 -0400 Subject: [PATCH] feat: add Settings page with PAT management, theme, and shortcuts --- packages/frontend/src/App.tsx | 2 + packages/frontend/src/pages/Settings.tsx | 323 +++++++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 packages/frontend/src/pages/Settings.tsx 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 ( +
+

+ Settings +

+
+ + + + + + + +
+
+ ); +}