feat: complete UI/UX overhaul with Impeccable design system
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 <button> elements with aria-labels. All 8 pages (Dashboard, Editor, Build, Server, Toolset, Repos, Settings, Setup) rewritten with consistent card/section/button patterns.
This commit is contained in:
@@ -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<BuildStatus, string> = {
|
||||
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<BuildStatus, React.CSSProperties> = {
|
||||
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 (
|
||||
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${colors[status]}`}>
|
||||
<span
|
||||
style={{
|
||||
...styles[status],
|
||||
borderRadius: "9999px",
|
||||
padding: "0.125rem 0.625rem",
|
||||
fontSize: "var(--text-xs)",
|
||||
fontWeight: 600,
|
||||
fontFamily: "var(--font-mono)",
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: "0.03em",
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
@@ -43,31 +77,47 @@ function BuildOutput({
|
||||
}, [lines, collapsed]);
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<div style={{ marginTop: "0.75rem" }}>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="flex items-center gap-1 text-xs transition-colors hover:opacity-80"
|
||||
style={{ color: "var(--forge-text-secondary)" }}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.375rem",
|
||||
fontSize: "var(--text-xs)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: "0.25rem 0",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
<span>{collapsed ? "\u25B6" : "\u25BC"}</span>
|
||||
{collapsed ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
|
||||
<span>Output ({lines.length} lines)</span>
|
||||
</button>
|
||||
{!collapsed && (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="mt-1 max-h-64 overflow-auto rounded p-3"
|
||||
style={{
|
||||
backgroundColor: "#0d1117",
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
||||
fontSize: "12px",
|
||||
lineHeight: "1.5",
|
||||
marginTop: "0.5rem",
|
||||
maxHeight: "16rem",
|
||||
overflowY: "auto",
|
||||
borderRadius: "0.5rem",
|
||||
padding: "0.875rem 1rem",
|
||||
backgroundColor: "var(--forge-log-bg)",
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: "var(--text-xs)",
|
||||
lineHeight: "1.6",
|
||||
}}
|
||||
>
|
||||
{lines.length === 0 ? (
|
||||
<span style={{ color: "var(--forge-text-secondary)" }}>No output yet</span>
|
||||
<span style={{ color: "var(--forge-text-secondary)", fontStyle: "italic" }}>
|
||||
No output yet
|
||||
</span>
|
||||
) : (
|
||||
lines.map((line, i) => (
|
||||
<div key={i} style={{ color: "#c9d1d9" }}>
|
||||
<div key={i} style={{ color: "var(--forge-log-text)" }}>
|
||||
{line}
|
||||
</div>
|
||||
))
|
||||
@@ -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<string, React.CSSProperties> = {
|
||||
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({
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className="rounded border px-3 py-1.5 text-sm font-medium transition-opacity disabled:cursor-not-allowed disabled:opacity-50"
|
||||
style={styles[variant]}
|
||||
style={{
|
||||
...variantStyles[variant],
|
||||
borderRadius: "0.375rem",
|
||||
padding: "0.5rem 1rem",
|
||||
fontSize: "var(--text-sm)",
|
||||
fontWeight: 600,
|
||||
fontFamily: "var(--font-sans)",
|
||||
cursor: disabled ? "not-allowed" : "pointer",
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.375rem",
|
||||
transition: "opacity 0.15s ease",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
@@ -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 (
|
||||
<div className="h-full overflow-auto p-6" style={{ color: "var(--forge-text)" }}>
|
||||
<h2 className="mb-6 text-2xl font-bold" style={{ color: "var(--forge-accent)" }}>
|
||||
Build Pipeline
|
||||
</h2>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
overflowY: "auto",
|
||||
padding: "1.5rem",
|
||||
color: "var(--forge-text)",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: "var(--font-heading)",
|
||||
fontSize: "var(--text-xl)",
|
||||
fontWeight: 700,
|
||||
color: "var(--forge-text)",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Build Pipeline
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: "var(--text-sm)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
margin: "0.375rem 0 0 0",
|
||||
}}
|
||||
>
|
||||
Compile, pack, and deploy module resources
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Module Section */}
|
||||
<section
|
||||
className="mb-6 rounded-lg border p-4"
|
||||
style={{
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
borderColor: "var(--forge-border)",
|
||||
}}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">Module</h3>
|
||||
<section style={cardStyle}>
|
||||
<div style={sectionHeaderStyle}>
|
||||
<div style={sectionTitleStyle}>
|
||||
<Hammer size={14} />
|
||||
<span>Module</span>
|
||||
</div>
|
||||
<StatusBadge status={module.status} />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div style={buttonRowStyle}>
|
||||
<ActionButton
|
||||
label="Compile"
|
||||
variant="primary"
|
||||
icon={<Play size={14} />}
|
||||
disabled={isBuilding}
|
||||
onClick={() => handleAction(() => api.build.compileModule(), "module")}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Pack Module"
|
||||
icon={<Archive size={14} />}
|
||||
disabled={isBuilding}
|
||||
onClick={() => handleAction(() => api.build.packModule(), "module")}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Deploy to Server"
|
||||
variant="warning"
|
||||
icon={<Upload size={14} />}
|
||||
disabled={isBuilding}
|
||||
onClick={() => handleAction(() => api.build.deploy(), "module")}
|
||||
/>
|
||||
@@ -238,21 +365,19 @@ export function Build() {
|
||||
</section>
|
||||
|
||||
{/* Haks Section */}
|
||||
<section
|
||||
className="mb-6 rounded-lg border p-4"
|
||||
style={{
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
borderColor: "var(--forge-border)",
|
||||
}}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">Haks</h3>
|
||||
<section style={cardStyle}>
|
||||
<div style={sectionHeaderStyle}>
|
||||
<div style={sectionTitleStyle}>
|
||||
<Package size={14} />
|
||||
<span>Haks</span>
|
||||
</div>
|
||||
<StatusBadge status={haks.status} />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div style={buttonRowStyle}>
|
||||
<ActionButton
|
||||
label="Build Haks"
|
||||
variant="primary"
|
||||
icon={<Play size={14} />}
|
||||
disabled={isBuilding}
|
||||
onClick={() => handleAction(() => api.build.buildHaks(), "haks")}
|
||||
/>
|
||||
@@ -265,38 +390,49 @@ export function Build() {
|
||||
</section>
|
||||
|
||||
{/* NWNX Section */}
|
||||
<section
|
||||
className="rounded-lg border p-4"
|
||||
style={{
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
borderColor: "var(--forge-border)",
|
||||
}}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">
|
||||
NWNX <span className="text-xs font-normal opacity-60">(Advanced)</span>
|
||||
</h3>
|
||||
<section style={cardStyle}>
|
||||
<div style={sectionHeaderStyle}>
|
||||
<div style={sectionTitleStyle}>
|
||||
<Cpu size={14} />
|
||||
<span>NWNX</span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
textTransform: "none",
|
||||
opacity: 0.6,
|
||||
letterSpacing: "normal",
|
||||
}}
|
||||
>
|
||||
(Advanced)
|
||||
</span>
|
||||
</div>
|
||||
<StatusBadge status={nwnx.status} />
|
||||
</div>
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
<div style={{ ...buttonRowStyle, marginBottom: "0.75rem" }}>
|
||||
<ActionButton
|
||||
label="Build All"
|
||||
variant="primary"
|
||||
icon={<Play size={14} />}
|
||||
disabled={isBuilding}
|
||||
onClick={() => handleAction(() => api.build.buildNwnx(), "nwnx")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||
<input
|
||||
type="text"
|
||||
value={nwnxTarget}
|
||||
onChange={(e) => 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",
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
@@ -308,10 +444,18 @@ export function Build() {
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
className="mt-2 text-xs"
|
||||
style={{ color: "#f59e0b" }}
|
||||
style={{
|
||||
marginTop: "0.75rem",
|
||||
marginBottom: 0,
|
||||
fontSize: "var(--text-xs)",
|
||||
color: "var(--forge-warning)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.375rem",
|
||||
}}
|
||||
>
|
||||
⚠ Requires server restart to pick up changes
|
||||
<AlertTriangle size={12} />
|
||||
Requires server restart to pick up changes
|
||||
</p>
|
||||
<BuildOutput
|
||||
lines={nwnx.output}
|
||||
|
||||
Reference in New Issue
Block a user