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:
@@ -2,6 +2,16 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { DiffEditor } from "@monaco-editor/react";
|
||||
import { api } from "../services/api";
|
||||
import { useWebSocket } from "../hooks/useWebSocket";
|
||||
import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
FileCode,
|
||||
Check,
|
||||
X,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
ArrowUpCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
interface ChangeEntry {
|
||||
filename: string;
|
||||
@@ -16,61 +26,6 @@ interface DiffData {
|
||||
filename: string;
|
||||
}
|
||||
|
||||
function StatusBadge({ active }: { active: boolean }) {
|
||||
return (
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
active
|
||||
? "bg-green-500/20 text-green-400"
|
||||
: "bg-gray-500/20 text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{active ? "Active" : "Inactive"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButton({
|
||||
label,
|
||||
onClick,
|
||||
disabled,
|
||||
variant = "default",
|
||||
}: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
variant?: "default" | "primary" | "danger";
|
||||
}) {
|
||||
const styles = {
|
||||
default: {
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
borderColor: "var(--forge-border)",
|
||||
color: "var(--forge-text)",
|
||||
},
|
||||
primary: {
|
||||
backgroundColor: "var(--forge-accent)",
|
||||
borderColor: "var(--forge-accent)",
|
||||
color: "#fff",
|
||||
},
|
||||
danger: {
|
||||
backgroundColor: "#7f1d1d",
|
||||
borderColor: "#991b1b",
|
||||
color: "#fca5a5",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<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]}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTimestamp(ts: number): string {
|
||||
return new Date(ts).toLocaleTimeString();
|
||||
}
|
||||
@@ -222,217 +177,578 @@ export function Toolset() {
|
||||
};
|
||||
|
||||
const handleDiscardAll = async () => {
|
||||
if (!window.confirm("Discard all changes? This cannot be undone.")) return;
|
||||
await api.toolset.discardAll();
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
style={{ color: "var(--forge-text)" }}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
color: "var(--forge-text)",
|
||||
}}
|
||||
>
|
||||
{/* Status bar */}
|
||||
<div
|
||||
className="flex shrink-0 items-center justify-between px-6 py-3"
|
||||
style={{ borderBottom: "1px solid var(--forge-border)" }}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<h2
|
||||
className="text-xl font-bold"
|
||||
style={{ color: "var(--forge-accent)" }}
|
||||
{/* Page heading */}
|
||||
<div style={{ padding: "24px 28px 0" }}>
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: "var(--font-heading)",
|
||||
fontSize: "var(--text-xl)",
|
||||
fontWeight: 700,
|
||||
color: "var(--forge-text)",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Toolset
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "var(--text-sm)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
margin: "4px 0 0",
|
||||
}}
|
||||
>
|
||||
Watch for NWN Toolset changes and apply them to the repository
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Watcher status card */}
|
||||
<div style={{ padding: "16px 28px" }}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
border: "1px solid var(--forge-border)",
|
||||
borderRadius: 8,
|
||||
padding: "16px 20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 16 }}
|
||||
>
|
||||
Toolset
|
||||
</h2>
|
||||
<StatusBadge active={active} />
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: "var(--forge-text-secondary)" }}
|
||||
>
|
||||
{changes.length} pending
|
||||
</span>
|
||||
{lastChange && (
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: "var(--forge-text-secondary)" }}
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 8 }}
|
||||
>
|
||||
Last: {formatTimestamp(lastChange)}
|
||||
{active ? (
|
||||
<Eye size={16} style={{ color: "var(--forge-success)" }} />
|
||||
) : (
|
||||
<EyeOff
|
||||
size={16}
|
||||
style={{ color: "var(--forge-text-secondary)" }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: "3px 10px",
|
||||
borderRadius: 999,
|
||||
fontSize: "var(--text-xs)",
|
||||
fontWeight: 600,
|
||||
backgroundColor: active
|
||||
? "var(--forge-success-bg)"
|
||||
: "var(--forge-surface-raised)",
|
||||
color: active
|
||||
? "var(--forge-success)"
|
||||
: "var(--forge-text-secondary)",
|
||||
}}
|
||||
>
|
||||
{active ? "Active" : "Inactive"}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "var(--text-xs)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
}}
|
||||
>
|
||||
{changes.length} pending change{changes.length !== 1 && "s"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{active ? (
|
||||
<ActionButton label="Stop Watcher" onClick={handleStop} />
|
||||
) : (
|
||||
<ActionButton
|
||||
label="Start Watcher"
|
||||
onClick={handleStart}
|
||||
variant="primary"
|
||||
/>
|
||||
)}
|
||||
{lastChange && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: "var(--text-xs)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
}}
|
||||
>
|
||||
Last change: {formatTimestamp(lastChange)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={active ? handleStop : handleStart}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: "6px 14px",
|
||||
borderRadius: 6,
|
||||
border: "1px solid",
|
||||
fontSize: "var(--text-sm)",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
backgroundColor: active
|
||||
? "var(--forge-surface)"
|
||||
: "var(--forge-accent)",
|
||||
borderColor: active
|
||||
? "var(--forge-border)"
|
||||
: "var(--forge-accent)",
|
||||
color: active
|
||||
? "var(--forge-text)"
|
||||
: "var(--forge-accent-text)",
|
||||
}}
|
||||
>
|
||||
{active ? (
|
||||
<>
|
||||
<EyeOff size={14} />
|
||||
Stop Watcher
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye size={14} />
|
||||
Start Watcher
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action bar */}
|
||||
{changes.length > 0 && (
|
||||
{/* Main content area */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
padding: "0 28px 20px",
|
||||
}}
|
||||
>
|
||||
{/* Changes card */}
|
||||
<div
|
||||
className="flex shrink-0 items-center gap-2 px-6 py-2"
|
||||
style={{ borderBottom: "1px solid var(--forge-border)" }}
|
||||
style={{
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
border: "1px solid var(--forge-border)",
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: diffData ? "0 0 auto" : 1,
|
||||
maxHeight: diffData ? "40%" : undefined,
|
||||
}}
|
||||
>
|
||||
<ActionButton
|
||||
label="Apply Selected"
|
||||
variant="primary"
|
||||
disabled={selected.size === 0}
|
||||
onClick={handleApplySelected}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Apply All"
|
||||
variant="primary"
|
||||
onClick={handleApplyAll}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Discard Selected"
|
||||
variant="danger"
|
||||
disabled={selected.size === 0}
|
||||
onClick={handleDiscardSelected}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Discard All"
|
||||
variant="danger"
|
||||
onClick={handleDiscardAll}
|
||||
/>
|
||||
<span
|
||||
className="ml-2 text-xs"
|
||||
style={{ color: "var(--forge-text-secondary)" }}
|
||||
{/* Card header with action bar */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "12px 16px",
|
||||
borderBottom: "1px solid var(--forge-border)",
|
||||
backgroundColor: "var(--forge-surface-raised)",
|
||||
}}
|
||||
>
|
||||
{selected.size} selected
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content: table + diff */}
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
{/* Changes table */}
|
||||
<div
|
||||
className="shrink-0 overflow-auto"
|
||||
style={{ maxHeight: diffData ? "40%" : "100%" }}
|
||||
>
|
||||
{changes.length === 0 ? (
|
||||
<div
|
||||
className="flex h-40 items-center justify-center text-sm"
|
||||
style={{ color: "var(--forge-text-secondary)" }}
|
||||
style={{ display: "flex", alignItems: "center", gap: 8 }}
|
||||
>
|
||||
{active
|
||||
? "Watching for changes in temp0/..."
|
||||
: "Start the watcher to detect Toolset changes"}
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr
|
||||
<FileCode
|
||||
size={16}
|
||||
style={{ color: "var(--forge-accent)" }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "var(--text-sm)",
|
||||
fontWeight: 600,
|
||||
color: "var(--forge-text)",
|
||||
}}
|
||||
>
|
||||
Pending Changes
|
||||
</span>
|
||||
{changes.length > 0 && (
|
||||
<span
|
||||
style={{
|
||||
borderBottom: "1px solid var(--forge-border)",
|
||||
fontSize: "var(--text-xs)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
marginLeft: 4,
|
||||
}}
|
||||
>
|
||||
<th className="px-6 py-2 text-left font-medium">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.size === changes.length}
|
||||
onChange={toggleAll}
|
||||
className="cursor-pointer"
|
||||
{selected.size} of {changes.length} selected
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{changes.length > 0 && (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 6 }}
|
||||
>
|
||||
<button
|
||||
onClick={handleApplySelected}
|
||||
disabled={selected.size === 0}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
padding: "5px 12px",
|
||||
borderRadius: 5,
|
||||
border: "none",
|
||||
fontSize: "var(--text-xs)",
|
||||
fontWeight: 500,
|
||||
cursor: selected.size === 0 ? "not-allowed" : "pointer",
|
||||
opacity: selected.size === 0 ? 0.5 : 1,
|
||||
backgroundColor: "var(--forge-accent)",
|
||||
color: "var(--forge-accent-text)",
|
||||
}}
|
||||
>
|
||||
<Check size={12} />
|
||||
Apply Selected
|
||||
</button>
|
||||
<button
|
||||
onClick={handleApplyAll}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
padding: "5px 12px",
|
||||
borderRadius: 5,
|
||||
border: "1px solid var(--forge-accent)",
|
||||
fontSize: "var(--text-xs)",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--forge-accent)",
|
||||
}}
|
||||
>
|
||||
<ArrowUpCircle size={12} />
|
||||
Apply All
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDiscardSelected}
|
||||
disabled={selected.size === 0}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
padding: "5px 12px",
|
||||
borderRadius: 5,
|
||||
border: "none",
|
||||
fontSize: "var(--text-xs)",
|
||||
fontWeight: 500,
|
||||
cursor: selected.size === 0 ? "not-allowed" : "pointer",
|
||||
opacity: selected.size === 0 ? 0.5 : 1,
|
||||
backgroundColor: "var(--forge-danger-bg)",
|
||||
color: "var(--forge-danger)",
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Discard Selected
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDiscardAll}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
padding: "5px 12px",
|
||||
borderRadius: 5,
|
||||
border: "1px solid var(--forge-danger-border)",
|
||||
fontSize: "var(--text-xs)",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--forge-danger)",
|
||||
}}
|
||||
>
|
||||
<X size={12} />
|
||||
Discard All
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Table or empty state */}
|
||||
<div style={{ overflow: "auto", flex: 1 }}>
|
||||
{changes.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "48px 24px",
|
||||
color: "var(--forge-text-secondary)",
|
||||
}}
|
||||
>
|
||||
{active ? (
|
||||
<>
|
||||
<RefreshCw
|
||||
size={28}
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
opacity: 0.4,
|
||||
animation: "spin 3s linear infinite",
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<th className="px-2 py-2 text-left font-medium">Filename</th>
|
||||
<th className="px-2 py-2 text-left font-medium">Type</th>
|
||||
<th className="px-2 py-2 text-left font-medium">
|
||||
Repo Path
|
||||
</th>
|
||||
<th className="px-2 py-2 text-left font-medium">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{changes.map((change) => (
|
||||
<span style={{ fontSize: "var(--text-sm)" }}>
|
||||
Watching for changes in temp0/...
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EyeOff
|
||||
size={28}
|
||||
style={{ marginBottom: 12, opacity: 0.4 }}
|
||||
/>
|
||||
<span style={{ fontSize: "var(--text-sm)" }}>
|
||||
Start the watcher to detect Toolset changes
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: "var(--text-sm)",
|
||||
borderCollapse: "collapse",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
key={change.filename}
|
||||
className="cursor-pointer transition-colors hover:bg-white/5"
|
||||
style={{
|
||||
borderBottom: "1px solid var(--forge-border)",
|
||||
backgroundColor:
|
||||
diffData?.filename === change.filename
|
||||
? "var(--forge-surface)"
|
||||
: undefined,
|
||||
backgroundColor: "var(--forge-surface-raised)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
}}
|
||||
onClick={() => viewDiff(change)}
|
||||
>
|
||||
<td className="px-6 py-2">
|
||||
<th
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
textAlign: "left",
|
||||
fontWeight: 500,
|
||||
width: 40,
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(change.filename)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleSelect(change.filename);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="cursor-pointer"
|
||||
checked={
|
||||
selected.size === changes.length &&
|
||||
changes.length > 0
|
||||
}
|
||||
onChange={toggleAll}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 font-mono">{change.filename}</td>
|
||||
<td className="px-2 py-2">
|
||||
<span className="rounded bg-white/10 px-1.5 py-0.5 text-xs">
|
||||
{change.gffType}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="px-2 py-2 font-mono text-xs"
|
||||
style={{ color: "var(--forge-text-secondary)" }}
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
textAlign: "left",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{change.repoPath ?? "—"}
|
||||
</td>
|
||||
<td
|
||||
className="px-2 py-2 text-xs"
|
||||
style={{ color: "var(--forge-text-secondary)" }}
|
||||
Filename
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
textAlign: "left",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatTimestamp(change.timestamp)}
|
||||
</td>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
textAlign: "left",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Repo Path
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
textAlign: "left",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{changes.map((change) => (
|
||||
<tr
|
||||
key={change.filename}
|
||||
onClick={() => viewDiff(change)}
|
||||
style={{
|
||||
borderBottom: "1px solid var(--forge-border)",
|
||||
cursor: "pointer",
|
||||
backgroundColor:
|
||||
diffData?.filename === change.filename
|
||||
? "var(--forge-accent-subtle)"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: "8px 16px" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(change.filename)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleSelect(change.filename);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
fontFamily: "var(--font-mono)",
|
||||
color: "var(--forge-text)",
|
||||
}}
|
||||
>
|
||||
{change.filename}
|
||||
</td>
|
||||
<td style={{ padding: "8px 10px" }}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "2px 8px",
|
||||
borderRadius: 4,
|
||||
fontSize: "var(--text-xs)",
|
||||
fontWeight: 500,
|
||||
backgroundColor: "var(--forge-accent-subtle)",
|
||||
color: "var(--forge-accent)",
|
||||
}}
|
||||
>
|
||||
{change.gffType}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: "var(--text-xs)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
}}
|
||||
>
|
||||
{change.repoPath ?? "—"}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
fontSize: "var(--text-xs)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
}}
|
||||
>
|
||||
{formatTimestamp(change.timestamp)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diff panel */}
|
||||
{/* Diff viewer panel */}
|
||||
{diffData && (
|
||||
<div
|
||||
ref={diffContainerRef}
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
style={{ borderTop: "1px solid var(--forge-border)" }}
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginTop: 16,
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
border: "1px solid var(--forge-border)",
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Diff header */}
|
||||
<div
|
||||
className="flex shrink-0 items-center justify-between px-4 py-1.5"
|
||||
style={{
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "10px 16px",
|
||||
backgroundColor: "var(--forge-surface-raised)",
|
||||
borderBottom: "1px solid var(--forge-border)",
|
||||
}}
|
||||
>
|
||||
<span className="text-xs font-medium">
|
||||
Diff: {diffData.filename}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<FileCode
|
||||
size={14}
|
||||
style={{ color: "var(--forge-accent)" }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "var(--text-sm)",
|
||||
fontWeight: 600,
|
||||
fontFamily: "var(--font-mono)",
|
||||
color: "var(--forge-text)",
|
||||
}}
|
||||
>
|
||||
{diffData.filename}
|
||||
</span>
|
||||
{loading && (
|
||||
<span style={{ color: "var(--forge-text-secondary)" }}>
|
||||
{" "}
|
||||
(loading...)
|
||||
<span
|
||||
style={{
|
||||
fontSize: "var(--text-xs)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
}}
|
||||
>
|
||||
Loading...
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setDiffData(null)}
|
||||
className="text-xs transition-opacity hover:opacity-80"
|
||||
style={{ color: "var(--forge-text-secondary)" }}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
padding: "4px 10px",
|
||||
borderRadius: 5,
|
||||
border: "1px solid var(--forge-border)",
|
||||
fontSize: "var(--text-xs)",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
color: "var(--forge-text-secondary)",
|
||||
}}
|
||||
>
|
||||
<X size={12} />
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1">
|
||||
|
||||
{/* Diff content */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
backgroundColor: "var(--forge-log-bg)",
|
||||
}}
|
||||
>
|
||||
<DiffEditor
|
||||
original={diffData.original}
|
||||
modified={diffData.modified}
|
||||
|
||||
Reference in New Issue
Block a user