From cbe51a6e67ce75ea9ec21d93484c6a23f885438f Mon Sep 17 00:00:00 2001 From: plenarius Date: Tue, 21 Apr 2026 03:06:29 -0400 Subject: [PATCH] feat: complete UI/UX overhaul with Impeccable design system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Inter/Baskerville with self-hosted Manrope/Alegreya/JetBrains Mono variable fonts. Migrate all colors from hex to OKLCH tokens (30+ CSS custom properties) with full dark/light mode support. Replace Unicode emoji with lucide-react SVG icons throughout. Convert all page layouts to inline styles (Tailwind CSS 4 flex/grid classes unreliable in this project). Code-split routes via React.lazy (760KB → 15KB initial shell + 10 lazy chunks). Add global styles: scrollbar theming, selection color, input/button bases, :focus-visible ring, prefers-reduced-motion. Setup wizard gets 4-phase indicator with numbered circles, PathInput and StatusDot components. Toast container gets aria-live="polite". Tab close buttons changed to proper @@ -133,7 +133,7 @@ export function CommitDialog({ repo, onClose, onCommitted }: CommitDialogProps) onClick={() => handleSubmit(true)} disabled={!isValid || loading} className="rounded border px-3 py-1.5 text-sm font-medium transition-opacity disabled:opacity-50" - style={{ backgroundColor: "#854d0e", borderColor: "#a16207", color: "#fef08a" }} + style={{ backgroundColor: "var(--forge-warning-bg)", borderColor: "var(--forge-warning-border)", color: "var(--forge-warning)" }} > Commit & Push diff --git a/packages/frontend/src/components/ErrorDisplay.tsx b/packages/frontend/src/components/ErrorDisplay.tsx index 1b3ac2b..4a866df 100644 --- a/packages/frontend/src/components/ErrorDisplay.tsx +++ b/packages/frontend/src/components/ErrorDisplay.tsx @@ -25,10 +25,10 @@ export function ErrorDisplay({ className="rounded-lg p-6" style={{ backgroundColor: "var(--forge-surface)", - border: "1px solid #7f1d1d", + border: "1px solid var(--forge-danger-border)", }} > -

+

{title}

@@ -50,7 +50,7 @@ export function ErrorDisplay({ style={{ backgroundColor: "var(--forge-bg)", color: "var(--forge-text-secondary)", - fontFamily: "'JetBrains Mono', 'Fira Code', monospace", + fontFamily: "var(--font-mono)", }} > {fullLog} @@ -64,7 +64,7 @@ export function ErrorDisplay({ diff --git a/packages/frontend/src/components/Toast.tsx b/packages/frontend/src/components/Toast.tsx index 68cf7d4..90cb43c 100644 --- a/packages/frontend/src/components/Toast.tsx +++ b/packages/frontend/src/components/Toast.tsx @@ -29,9 +29,9 @@ export function useToast() { } const COLORS: Record = { - success: { bg: "#052e16", border: "#166534", text: "#4ade80" }, - error: { bg: "#3b1111", border: "#7f1d1d", text: "#fca5a5" }, - info: { bg: "#1c1403", border: "#946200", text: "#fbbf24" }, + success: { bg: "var(--forge-success-bg)", border: "var(--forge-success-border)", text: "var(--forge-success)" }, + error: { bg: "var(--forge-danger-bg)", border: "var(--forge-danger-border)", text: "var(--forge-danger)" }, + info: { bg: "var(--forge-warning-bg)", border: "var(--forge-warning-border)", text: "var(--forge-warning)" }, }; const AUTO_DISMISS: Record = { @@ -49,7 +49,7 @@ function ToastItem({ }) { const { bg, border, text } = COLORS[toast.type]; const timeout = AUTO_DISMISS[toast.type]; - const timerRef = useRef>(); + const timerRef = useRef>(undefined); useEffect(() => { if (timeout) { @@ -92,7 +92,7 @@ export function ToastProvider({ children }: { children: ReactNode }) { return ( {children} -

+
{toasts.map((t) => ( ))} diff --git a/packages/frontend/src/components/editor/EditorTabs.tsx b/packages/frontend/src/components/editor/EditorTabs.tsx index 437ba4b..91f76aa 100644 --- a/packages/frontend/src/components/editor/EditorTabs.tsx +++ b/packages/frontend/src/components/editor/EditorTabs.tsx @@ -56,16 +56,18 @@ export function EditorTabs({ /> )} - { e.stopPropagation(); onClose(tab.path); }} className="ml-1 rounded p-0.5 opacity-0 transition-opacity hover:bg-white/10 group-hover:opacity-100" - style={{ color: "var(--forge-text-secondary)" }} + style={{ appearance: "none", border: "none", background: "transparent", cursor: "pointer", color: "var(--forge-text-secondary)" }} > × - + ); })} diff --git a/packages/frontend/src/components/editor/FileExplorer.tsx b/packages/frontend/src/components/editor/FileExplorer.tsx index 5c372e5..f78ea07 100644 --- a/packages/frontend/src/components/editor/FileExplorer.tsx +++ b/packages/frontend/src/components/editor/FileExplorer.tsx @@ -87,7 +87,7 @@ function FileTreeNode({ paddingLeft: `${depth * 16 + 8}px`, backgroundColor: isSelected ? "var(--forge-surface)" : undefined, color: isSelected ? "var(--forge-text)" : "var(--forge-text-secondary)", - fontFamily: "'JetBrains Mono', 'Fira Code', monospace", + fontFamily: "var(--font-mono)", fontSize: "13px", }} > @@ -186,8 +186,17 @@ export function FileExplorer({ )} {error && ( -
- {error} +
+ {error.includes("ENOENT") ? ( +
+

Repository not cloned

+

+ Clone repositories from the Repos page or run the setup wizard. +

+
+ ) : ( +

{error}

+ )}
)} diff --git a/packages/frontend/src/components/editor/MonacoEditor.tsx b/packages/frontend/src/components/editor/MonacoEditor.tsx index 9e03914..ac424b8 100644 --- a/packages/frontend/src/components/editor/MonacoEditor.tsx +++ b/packages/frontend/src/components/editor/MonacoEditor.tsx @@ -167,13 +167,14 @@ function registerNWScript(monaco: Parameters[1]) { function defineForgeTheme(monaco: Parameters[1]) { const style = getComputedStyle(document.documentElement); - const bg = style.getPropertyValue("--forge-bg").trim() || "#121212"; - const surface = style.getPropertyValue("--forge-surface").trim() || "#1e1e2e"; - const accent = style.getPropertyValue("--forge-accent").trim() || "#946200"; - const text = style.getPropertyValue("--forge-text").trim() || "#f2f2f2"; + const bg = style.getPropertyValue("--forge-bg").trim() || "#1f1a14"; + const surface = style.getPropertyValue("--forge-surface").trim() || "#2a2419"; + const accent = style.getPropertyValue("--forge-accent").trim() || "#b07a1a"; + const text = style.getPropertyValue("--forge-text").trim() || "#ede8e0"; const textSecondary = - style.getPropertyValue("--forge-text-secondary").trim() || "#888888"; - const border = style.getPropertyValue("--forge-border").trim() || "#2e2e3e"; + style.getPropertyValue("--forge-text-secondary").trim() || "#9a9080"; + const border = style.getPropertyValue("--forge-border").trim() || "#3d3528"; + const accentSubtle = style.getPropertyValue("--forge-accent-subtle").trim() || "#2e2818"; monaco.editor.defineTheme("forge-dark", { base: "vs-dark", @@ -196,7 +197,7 @@ function defineForgeTheme(monaco: Parameters[1]) { "editor.lineHighlightBackground": surface, "editorLineNumber.foreground": textSecondary, "editorLineNumber.activeForeground": text, - "editor.selectionBackground": "#264f7840", + "editor.selectionBackground": accentSubtle, "editorWidget.background": surface, "editorWidget.border": border, "editorSuggestWidget.background": surface, diff --git a/packages/frontend/src/components/editor/SearchPanel.tsx b/packages/frontend/src/components/editor/SearchPanel.tsx index 481bad7..bc4589c 100644 --- a/packages/frontend/src/components/editor/SearchPanel.tsx +++ b/packages/frontend/src/components/editor/SearchPanel.tsx @@ -82,9 +82,9 @@ export function SearchPanel({ repo, onResultClick }: SearchPanelProps) { const toggleBtnStyle = (active: boolean): React.CSSProperties => ({ backgroundColor: active ? "var(--forge-accent)" : "transparent", - color: active ? "#121212" : "var(--forge-text-secondary)", + color: active ? "var(--forge-accent-text)" : "var(--forge-text-secondary)", border: `1px solid ${active ? "var(--forge-accent)" : "var(--forge-border)"}`, - fontFamily: "'JetBrains Mono', 'Fira Code', monospace", + fontFamily: "var(--font-mono)", fontSize: "12px", lineHeight: "1", }); @@ -119,7 +119,7 @@ export function SearchPanel({ repo, onResultClick }: SearchPanelProps) { backgroundColor: "var(--forge-surface)", color: "var(--forge-text)", border: "1px solid var(--forge-border)", - fontFamily: "'JetBrains Mono', 'Fira Code', monospace", + fontFamily: "var(--font-mono)", fontSize: "13px", }} /> @@ -172,7 +172,7 @@ export function SearchPanel({ repo, onResultClick }: SearchPanelProps) { className="w-full rounded px-3 py-1 text-sm font-medium transition-colors disabled:opacity-50" style={{ backgroundColor: "var(--forge-accent)", - color: "#121212", + color: "var(--forge-accent-text)", }} > {loading ? "Searching..." : "Search"} @@ -181,7 +181,7 @@ export function SearchPanel({ repo, onResultClick }: SearchPanelProps) {
{error && ( -
{error}
+
{error}
)} {searched && !loading && !error && ( @@ -213,7 +213,7 @@ export function SearchPanel({ repo, onResultClick }: SearchPanelProps) { {group.file} @@ -240,7 +240,7 @@ export function SearchPanel({ repo, onResultClick }: SearchPanelProps) { className="shrink-0 text-xs" style={{ color: "var(--forge-text-secondary)", - fontFamily: "'JetBrains Mono', 'Fira Code', monospace", + fontFamily: "var(--font-mono)", fontSize: "11px", minWidth: "32px", textAlign: "right", @@ -251,7 +251,7 @@ export function SearchPanel({ repo, onResultClick }: SearchPanelProps) { {type === "entry" ? "E" : "R"}{index} @@ -287,7 +287,7 @@ export function DialogEditor({ repo, filePath, content, onSave, onSwitchToRaw }: onClick={handleSave} disabled={!dirty || saving} className="rounded px-3 py-1 text-sm font-medium transition-colors disabled:opacity-40" - style={{ backgroundColor: "var(--forge-accent)", color: "#fff" }} + style={{ backgroundColor: "var(--forge-accent)", color: "var(--forge-accent-text)" }} > {saving ? "Saving..." : "Save"} @@ -317,7 +317,7 @@ export function DialogEditor({ repo, filePath, content, onSave, onSwitchToRaw }: {/* Content */}
{error && ( -

{error}

+

{error}

)} {activeTab === "tree" && ( diff --git a/packages/frontend/src/components/gff/GffEditor.tsx b/packages/frontend/src/components/gff/GffEditor.tsx index b00cbff..1cad62d 100644 --- a/packages/frontend/src/components/gff/GffEditor.tsx +++ b/packages/frontend/src/components/gff/GffEditor.tsx @@ -215,13 +215,13 @@ function FieldRenderer({ field, value, onChange, onLocStringChange }: FieldRende className="flex-1 rounded border px-2 py-1.5 font-mono text-sm" style={{ backgroundColor: "var(--forge-bg)", - borderColor: valid ? "var(--forge-border)" : "#ef4444", + borderColor: valid ? "var(--forge-border)" : "var(--forge-danger)", color: "var(--forge-text)", }} /> {str.length}/16 @@ -421,7 +421,7 @@ export function GffEditor({ return (
-

{error}

+

{error}

{onSwitchToRaw && (
@@ -480,7 +480,7 @@ export function GffEditor({ className="rounded px-3 py-1 text-sm font-medium transition-colors disabled:opacity-40" style={{ backgroundColor: "var(--forge-accent)", - color: "#fff", + color: "var(--forge-accent-text)", }} > {saving ? "Saving..." : "Save"} diff --git a/packages/frontend/src/components/gff/ItemEditor.tsx b/packages/frontend/src/components/gff/ItemEditor.tsx index 665da74..9ed1c81 100644 --- a/packages/frontend/src/components/gff/ItemEditor.tsx +++ b/packages/frontend/src/components/gff/ItemEditor.tsx @@ -141,7 +141,7 @@ function PropertiesListOverride({ value }: FieldOverrideProps) { diff --git a/packages/frontend/src/components/terminal/Terminal.tsx b/packages/frontend/src/components/terminal/Terminal.tsx index 740046b..fa03dc0 100644 --- a/packages/frontend/src/components/terminal/Terminal.tsx +++ b/packages/frontend/src/components/terminal/Terminal.tsx @@ -15,21 +15,28 @@ export function Terminal({ sessionId }: TerminalProps) { useEffect(() => { if (!containerRef.current) return; + const style = getComputedStyle(document.documentElement); + const bg = style.getPropertyValue("--forge-bg").trim(); + const fg = style.getPropertyValue("--forge-text").trim(); + const accent = style.getPropertyValue("--forge-accent").trim(); + const secondary = style.getPropertyValue("--forge-text-secondary").trim(); + const accentHover = style.getPropertyValue("--forge-accent-hover").trim(); + const term = new XTerm({ theme: { - background: "#121212", - foreground: "#f2f2f2", - cursor: "#946200", - selectionBackground: "#946200", - selectionForeground: "#f2f2f2", - black: "#121212", - brightBlack: "#666666", - white: "#f2f2f2", - brightWhite: "#ffffff", - yellow: "#946200", - brightYellow: "#c48800", + background: bg, + foreground: fg, + cursor: accent, + selectionBackground: accent, + selectionForeground: fg, + black: bg, + brightBlack: secondary, + white: fg, + brightWhite: fg, + yellow: accent, + brightYellow: accentHover, }, - fontFamily: "'JetBrains Mono', 'Fira Code', monospace", + fontFamily: "var(--font-mono)", fontSize: 13, cursorBlink: true, }); @@ -80,7 +87,7 @@ export function Terminal({ sessionId }: TerminalProps) {
); } diff --git a/packages/frontend/src/layouts/IDELayout.tsx b/packages/frontend/src/layouts/IDELayout.tsx index 536639d..5bf7799 100644 --- a/packages/frontend/src/layouts/IDELayout.tsx +++ b/packages/frontend/src/layouts/IDELayout.tsx @@ -3,14 +3,28 @@ import { Outlet, Link, useLocation, useNavigate } from "react-router-dom"; import { Terminal } from "../components/terminal/Terminal"; import { useWebSocket } from "../hooks/useWebSocket"; import { useTheme } from "../hooks/useTheme"; +import { + Code2, + Wrench, + Hammer, + Play, + GitBranch, + Settings, + Sun, + Moon, + Terminal as TerminalIcon, + ChevronDown, + ChevronUp, +} from "lucide-react"; +import type { LucideIcon } from "lucide-react"; -const NAV_ITEMS = [ - { path: "/editor", label: "Editor", icon: "\u270E" }, - { path: "/toolset", label: "Toolset", icon: "\u2699" }, - { path: "/build", label: "Build", icon: "\u2692" }, - { path: "/server", label: "Server", icon: "\u25B6" }, - { path: "/repos", label: "Repos", icon: "\u2387" }, - { path: "/settings", label: "Settings", icon: "\u2318" }, +const NAV_ITEMS: { path: string; label: string; Icon: LucideIcon }[] = [ + { path: "/editor", label: "Editor", Icon: Code2 }, + { path: "/toolset", label: "Toolset", Icon: Wrench }, + { path: "/build", label: "Build", Icon: Hammer }, + { path: "/server", label: "Server", Icon: Play }, + { path: "/repos", label: "Repos", Icon: GitBranch }, + { path: "/settings", label: "Settings", Icon: Settings }, ]; export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) { @@ -21,6 +35,7 @@ export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) { const navigate = useNavigate(); const { subscribe } = useWebSocket(); const { theme, toggleTheme } = useTheme(); + const showSidebar = location.pathname === "/editor" || location.pathname.startsWith("/editor/"); useEffect(() => { return subscribe("git:upstream-update", (event) => { @@ -85,25 +100,28 @@ export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) { }; return ( -
+
{/* Left sidebar nav */} {/* Main content area */} -
+
-
- - Layonara Forge - -
-
+ + Layonara Forge +
-
- {sidebar && ( +
+ {sidebar && showSidebar && ( )} -
+
{terminalOpen && (
diff --git a/packages/frontend/src/layouts/SetupLayout.tsx b/packages/frontend/src/layouts/SetupLayout.tsx index 5086ce1..e0d417c 100644 --- a/packages/frontend/src/layouts/SetupLayout.tsx +++ b/packages/frontend/src/layouts/SetupLayout.tsx @@ -3,22 +3,35 @@ import { Outlet } from "react-router-dom"; export function SetupLayout() { return (
-
-

- Layonara Forge -

+
+
+

+ Layonara Forge +

+

+ Development environment setup +

+
diff --git a/packages/frontend/src/pages/Build.tsx b/packages/frontend/src/pages/Build.tsx index 7ef15a5..fa93788 100644 --- a/packages/frontend/src/pages/Build.tsx +++ b/packages/frontend/src/pages/Build.tsx @@ -1,6 +1,17 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { api } from "../services/api"; import { useWebSocket } from "../hooks/useWebSocket"; +import { + Hammer, + Package, + Cpu, + Play, + Archive, + Upload, + ChevronDown, + ChevronUp, + AlertTriangle, +} from "lucide-react"; type BuildStatus = "idle" | "building" | "success" | "failed"; @@ -11,15 +22,38 @@ interface BuildSectionState { } function StatusBadge({ status }: { status: BuildStatus }) { - const colors: Record = { - idle: "bg-gray-500/20 text-gray-400", - building: "bg-yellow-500/20 text-yellow-400", - success: "bg-green-500/20 text-green-400", - failed: "bg-red-500/20 text-red-400", + const styles: Record = { + idle: { + backgroundColor: "var(--forge-surface-raised)", + color: "var(--forge-text-secondary)", + }, + building: { + backgroundColor: "var(--forge-warning-bg)", + color: "var(--forge-warning)", + }, + success: { + backgroundColor: "var(--forge-success-bg)", + color: "var(--forge-success)", + }, + failed: { + backgroundColor: "var(--forge-danger-bg)", + color: "var(--forge-danger)", + }, }; return ( - + {status} ); @@ -43,31 +77,47 @@ function BuildOutput({ }, [lines, collapsed]); return ( -
+
{!collapsed && (
{lines.length === 0 ? ( - No output yet + + No output yet + ) : ( lines.map((line, i) => ( -
+
{line}
)) @@ -83,27 +133,29 @@ function ActionButton({ onClick, disabled, variant = "default", + icon, }: { label: string; onClick: () => void; disabled?: boolean; variant?: "default" | "primary" | "warning"; + icon?: React.ReactNode; }) { - const styles = { + const variantStyles: Record = { default: { - backgroundColor: "var(--forge-surface)", - borderColor: "var(--forge-border)", + backgroundColor: "var(--forge-surface-raised)", + border: "1px solid var(--forge-border)", color: "var(--forge-text)", }, primary: { backgroundColor: "var(--forge-accent)", - borderColor: "var(--forge-accent)", - color: "#fff", + border: "none", + color: "var(--forge-accent-text)", }, warning: { - backgroundColor: "#854d0e", - borderColor: "#a16207", - color: "#fef08a", + backgroundColor: "var(--forge-warning-bg)", + border: "1px solid var(--forge-warning-border)", + color: "var(--forge-warning)", }, }; @@ -111,9 +163,22 @@ function ActionButton({ ); @@ -191,41 +256,103 @@ export function Build() { [], ); - const isBuilding = module.status === "building" || haks.status === "building" || nwnx.status === "building"; + const isBuilding = + module.status === "building" || haks.status === "building" || nwnx.status === "building"; + + const cardStyle: React.CSSProperties = { + backgroundColor: "var(--forge-surface)", + border: "1px solid var(--forge-border)", + borderRadius: "0.75rem", + padding: "1.25rem", + marginBottom: "1rem", + }; + + const sectionHeaderStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + marginBottom: "1rem", + }; + + const sectionTitleStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: "0.5rem", + textTransform: "uppercase", + fontSize: "var(--text-xs)", + fontWeight: 600, + letterSpacing: "0.05em", + color: "var(--forge-text-secondary)", + fontFamily: "var(--font-heading)", + }; + + const buttonRowStyle: React.CSSProperties = { + display: "flex", + flexWrap: "wrap", + gap: "0.5rem", + alignItems: "center", + }; return ( -
-

- Build Pipeline -

+
+
+

+ Build Pipeline +

+

+ Compile, pack, and deploy module resources +

+
{/* Module Section */} -
-
-

Module

+
+
+
+ + Module +
-
+
} disabled={isBuilding} onClick={() => handleAction(() => api.build.compileModule(), "module")} /> } disabled={isBuilding} onClick={() => handleAction(() => api.build.packModule(), "module")} /> } disabled={isBuilding} onClick={() => handleAction(() => api.build.deploy(), "module")} /> @@ -238,21 +365,19 @@ export function Build() {
{/* Haks Section */} -
-
-

Haks

+
+
+
+ + Haks +
-
+
} disabled={isBuilding} onClick={() => handleAction(() => api.build.buildHaks(), "haks")} /> @@ -265,38 +390,49 @@ export function Build() {
{/* NWNX Section */} -
-
-

- NWNX (Advanced) -

+
+
+
+ + NWNX + + (Advanced) + +
-
+
} disabled={isBuilding} onClick={() => handleAction(() => api.build.buildNwnx(), "nwnx")} />
-
+
setNwnxTarget(e.target.value)} placeholder="Target (e.g. Item, Creature)" - className="rounded border px-3 py-1.5 text-sm" style={{ backgroundColor: "var(--forge-bg)", - borderColor: "var(--forge-border)", + border: "1px solid var(--forge-border)", + borderRadius: "0.375rem", + padding: "0.5rem 0.75rem", + fontSize: "var(--text-sm)", color: "var(--forge-text)", + fontFamily: "var(--font-sans)", + outline: "none", + flex: "0 1 16rem", }} />

- ⚠ Requires server restart to pick up changes + + Requires server restart to pick up changes

({ + width: "0.5rem", + height: "0.5rem", + borderRadius: "50%", + backgroundColor: color, + flexShrink: 0, +}); + +const primaryBtn: React.CSSProperties = { + backgroundColor: "var(--forge-accent)", + color: "var(--forge-accent-text)", + border: "none", + borderRadius: "0.375rem", + padding: "0.5rem 1rem", + fontSize: "var(--text-sm)", + fontWeight: 600, + cursor: "pointer", + width: "100%", + transition: "background-color 150ms", +}; function StatusBadge({ status }: { status: string }) { const color = status === "running" - ? "#4ade80" - : status === "stopped" - ? "#f87171" - : "#fbbf24"; + ? "var(--forge-success)" + : status === "stopped" || status === "exited" || status === "not created" + ? "var(--forge-danger)" + : "var(--forge-warning)"; return ( - + {status} ); @@ -57,49 +104,36 @@ function ServerCard() { } }; + const isRunning = status.nwserver === "running"; + return ( -
-

- Server Status -

-
-
-
- - NWServer - - -
-
- - MariaDB - - -
+
+

Server

+
+
+ NWServer + +
+
+ MariaDB +
-
+
@@ -120,39 +154,32 @@ function ReposSummary() { }, []); return ( -
-

- Repositories -

-
- {repos.map((repo) => { +
+

Repositories

+
+ {repos.map((repo, i) => { const s = repoStatus[repo]; const branch = (s?.branch as string) || "\u2014"; const clean = s?.clean !== false; return (
0 ? "1px solid var(--forge-border)" : undefined, + }} > - + {repo} -
- +
+ {branch} - +
); @@ -166,41 +193,38 @@ function QuickActions() { const navigate = useNavigate(); const actions = [ - { label: "Build Module", onClick: () => navigate("/build") }, - { label: "Build Haks", onClick: () => navigate("/build") }, - { label: "Open Editor", onClick: () => navigate("/editor") }, - { - label: "Open Terminal", - onClick: () => { - /* terminal is toggled from IDELayout via Ctrl+` */ - navigate("/editor"); - }, - }, + { label: "Build Module", Icon: Hammer, onClick: () => navigate("/build") }, + { label: "Open Editor", Icon: Code2, onClick: () => navigate("/editor") }, + { label: "Server Logs", Icon: Database, onClick: () => navigate("/server") }, ]; return ( -
-

- Quick Actions -

-
+
+

Quick Actions

+
{actions.map((a) => ( ))} @@ -211,24 +235,16 @@ function QuickActions() { export function Dashboard() { return ( -
-
-

- Layonara Forge +
+
+

+ Dashboard

-

- NWN Development Environment +

+ Server, repositories, and quick actions

-
-
- -
+
+
diff --git a/packages/frontend/src/pages/Editor.tsx b/packages/frontend/src/pages/Editor.tsx index cfa6565..4a5f940 100644 --- a/packages/frontend/src/pages/Editor.tsx +++ b/packages/frontend/src/pages/Editor.tsx @@ -6,6 +6,7 @@ import { ItemEditor } from "../components/gff/ItemEditor"; import { CreatureEditor } from "../components/gff/CreatureEditor"; import { AreaEditor } from "../components/gff/AreaEditor"; import { DialogEditor } from "../components/gff/DialogEditor"; +import { FileCode, Code2, Eye } from "lucide-react"; const GFF_EXTENSIONS = [".uti.json", ".utc.json", ".are.json", ".dlg.json", ".utp.json", ".utm.json"]; @@ -50,7 +51,6 @@ export function Editor({ editorState }: EditorProps) { markClean, } = editorState; - // Track per-tab editor mode: "visual" or "raw". GFF files default to visual. const [editorModes, setEditorModes] = useState>({}); const tabs = useMemo( @@ -105,8 +105,28 @@ export function Editor({ editorState }: EditorProps) { const renderEditor = () => { if (!activeTab) { return ( -
-

+

+ +

Open a file from the File Explorer to start editing

@@ -139,22 +159,41 @@ export function Editor({ editorState }: EditorProps) { } return ( -
+
{isActiveGff && activeMode === "raw" && (
)} -
+
+
-
+
{renderEditor()}
diff --git a/packages/frontend/src/pages/Repos.tsx b/packages/frontend/src/pages/Repos.tsx index 3f20be1..fec49c6 100644 --- a/packages/frontend/src/pages/Repos.tsx +++ b/packages/frontend/src/pages/Repos.tsx @@ -2,6 +2,18 @@ import { useState, useEffect, useCallback } from "react"; import { api } from "../services/api"; import { CommitDialog } from "../components/CommitDialog"; import { useWebSocket } from "../hooks/useWebSocket"; +import { + GitBranch, + GitCommit, + GitPullRequest, + Download, + Upload, + Copy, + FileCode, + AlertCircle, + CheckCircle, + X, +} from "lucide-react"; interface RepoStatus { modified: string[]; @@ -26,6 +38,54 @@ interface PrForm { body: string; } +const badge = ( + bg: string, + fg: string, + extra?: React.CSSProperties, +): React.CSSProperties => ({ + display: "inline-flex", + alignItems: "center", + gap: "0.3rem", + padding: "0.15rem 0.55rem", + borderRadius: "9999px", + fontSize: "var(--text-xs)", + fontWeight: 600, + lineHeight: 1.4, + backgroundColor: bg, + color: fg, + whiteSpace: "nowrap", + ...extra, +}); + +const btnBase: React.CSSProperties = { + display: "inline-flex", + alignItems: "center", + gap: "0.4rem", + padding: "0.4rem 0.85rem", + borderRadius: "0.5rem", + fontSize: "var(--text-sm)", + fontWeight: 600, + fontFamily: "var(--font-sans)", + border: "1px solid", + cursor: "pointer", + transition: "opacity 0.15s", + lineHeight: 1.4, +}; + +const outlineBtn: React.CSSProperties = { + ...btnBase, + backgroundColor: "var(--forge-surface)", + borderColor: "var(--forge-border)", + color: "var(--forge-text)", +}; + +const accentBtn: React.CSSProperties = { + ...btnBase, + backgroundColor: "var(--forge-accent)", + borderColor: "var(--forge-accent)", + color: "var(--forge-accent-text)", +}; + export function Repos() { const [repos, setRepos] = useState([]); const [loading, setLoading] = useState(true); @@ -133,106 +193,247 @@ export function Repos() { const isDirty = (status?: RepoStatus) => status && (status.modified.length > 0 || status.staged.length > 0 || status.untracked.length > 0); + const disabledStyle = (disabled: boolean | undefined): React.CSSProperties => + disabled ? { opacity: 0.45, pointerEvents: "none" } : {}; + if (loading) { return ( -
- Loading repositories... +
+ Loading repositories…
); } return ( -
-

- Repositories -

+
+ {/* Page heading */} +
+

+ Repositories +

+

+ Clone, sync, and manage your Layonara repos +

+
+ {/* Error banner */} {error && ( -
- {error} - +
+ + {error} +
)} + {/* PR success banner */} {prResult && ( -
- PR created: {prResult.url} - +
+ + + PR created:{" "} + + {prResult.url} + + +
)} -
+ {/* Repo cards */} +
{repos.map((repo) => (
-
-
-

{repo.name}

- + {/* Card header */} +
+
+

+ {repo.name} +

+ + + {repo.branch} + {repo.cloned && repo.status && ( <> - - {isDirty(repo.status) ? "dirty" : "clean"} - - {repo.status.behind > 0 && ( - - {repo.status.behind} commit{repo.status.behind !== 1 ? "s" : ""} behind upstream + {isDirty(repo.status) ? ( + + dirty + + ) : ( + + + clean )} + + {repo.status.behind > 0 && ( + + + {repo.status.behind} behind + + )} + {repo.status.ahead > 0 && ( - + + {repo.status.ahead} ahead )} )}
- + + {repo.upstream}
+ {/* Actions */} {!repo.cloned ? ( ) : ( <> -
+
- + + + +
+ {/* Changed files list */} {repo.status && isDirty(repo.status) && ( -
-
- Changes +
+
+ + + Changes +
-
+ +
{repo.status.modified.map((f) => (
handleShowDiff(repo.name, f)} + style={{ + display: "flex", + alignItems: "center", + gap: "0.5rem", + padding: "0.35rem 0.75rem", + fontSize: "var(--text-xs)", + cursor: "pointer", + borderBottom: "1px solid var(--forge-border)", + }} > - M - {f} + M + {f}
))} {repo.status.staged.map((f) => ( -
- S - {f} +
+ S + {f}
))} {repo.status.untracked.map((f) => ( -
- ? - {f} +
+ ? + {f}
))}
@@ -285,6 +551,7 @@ export function Repos() { ))}
+ {/* Commit dialog */} {commitRepo && ( )} + {/* PR form modal */} {prForm && ( -
setPrForm(null)}> +
setPrForm(null)} + style={{ + position: "fixed", + inset: 0, + zIndex: 50, + display: "flex", + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(0,0,0,0.6)", + }} + >
e.stopPropagation()} + style={{ + width: "100%", + maxWidth: "32rem", + backgroundColor: "var(--forge-surface)", + border: "1px solid var(--forge-border)", + borderRadius: "0.75rem", + padding: "1.5rem", + }} > -

- Create Pull Request — {prForm.repo} -

+
+ +

+ Create Pull Request +

+ + — {prForm.repo} + +
+ setPrForm({ ...prForm, title: e.target.value })} placeholder="PR Title" - className="mb-3 w-full rounded border px-3 py-1.5 text-sm" - style={{ backgroundColor: "var(--forge-bg)", borderColor: "var(--forge-border)", color: "var(--forge-text)" }} + style={{ + width: "100%", + padding: "0.5rem 0.75rem", + marginBottom: "0.75rem", + borderRadius: "0.5rem", + border: "1px solid var(--forge-border)", + backgroundColor: "var(--forge-bg)", + color: "var(--forge-text)", + fontSize: "var(--text-sm)", + fontFamily: "var(--font-sans)", + boxSizing: "border-box", + outline: "none", + }} /> +