feat(app): UI parity with Pencil mockup — top bar, panel cards, sidebar/event-center polish
Top bar (breadcrumb + actions + account), rounded panel cards with active accent + rich headers, sidebar count pills/collapsible groups/daemon footer, preset chips + scrollback pill, Event Center tabs + external-notify footer, JetBrains Mono + Inter via @fontsource, shared theme tokens. Backend-absent pieces are mocked (search, zoom, uptime, channels) pending SP1–SP5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+66
-20
@@ -1,7 +1,9 @@
|
||||
import { useRef } from "react";
|
||||
import { Maximize2, RotateCw } from "lucide-react";
|
||||
import { TerminalView } from "./TerminalView";
|
||||
import { StatusRing } from "./StatusRing";
|
||||
import type { LayoutNode, SurfaceState } from "./layoutTypes";
|
||||
import { COLORS, FONT, STATE_COLOR } from "./theme";
|
||||
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
|
||||
import { setRatios, restartSurface } from "./socketBridge";
|
||||
|
||||
interface Props {
|
||||
@@ -10,36 +12,81 @@ interface Props {
|
||||
/** surface_id -> running flag, from the latest status/events. */
|
||||
running: Record<string, boolean>;
|
||||
states: Record<string, SurfaceState>;
|
||||
surfaces: Record<string, SurfaceView>;
|
||||
focusedId: string | null;
|
||||
onFocus: (id: string) => void;
|
||||
}
|
||||
|
||||
export function LayoutEngine({ workspaceId, layout, running, states }: Props) {
|
||||
/** Collapse an absolute cwd into a ~/<leaf> style label for the panel header. */
|
||||
function shortPath(cwd: string): string {
|
||||
const leaf = cwd.split("/").filter(Boolean).pop();
|
||||
return leaf ? `~/${leaf}` : cwd;
|
||||
}
|
||||
|
||||
export function LayoutEngine({ workspaceId, layout, running, states, surfaces, focusedId, onFocus }: Props) {
|
||||
if (!layout) {
|
||||
return <div style={{ color: "#666", padding: 24 }}>Empty workspace — apply a preset to add panels.</div>;
|
||||
return <div style={{ color: COLORS.textMuted, padding: 24 }}>Empty workspace — apply a preset to add panels.</div>;
|
||||
}
|
||||
return <Node workspaceId={workspaceId} node={layout} path={[]} running={running} states={states} />;
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%", padding: 12, boxSizing: "border-box" }}>
|
||||
<Node workspaceId={workspaceId} node={layout} path={[]} running={running} states={states} surfaces={surfaces} focusedId={focusedId} onFocus={onFocus} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Node({ workspaceId, node, path, running, states }: { workspaceId: string; node: LayoutNode; path: number[]; running: Record<string, boolean>; states: Record<string, SurfaceState> }) {
|
||||
function Node({ workspaceId, node, path, running, states, surfaces, focusedId, onFocus }: {
|
||||
workspaceId: string; node: LayoutNode; path: number[];
|
||||
running: Record<string, boolean>; states: Record<string, SurfaceState>;
|
||||
surfaces: Record<string, SurfaceView>; focusedId: string | null; onFocus: (id: string) => void;
|
||||
}) {
|
||||
if ("leaf" in node) {
|
||||
const id = node.leaf.surface_id;
|
||||
const focused = focusedId === id;
|
||||
const card = (inner: React.ReactNode) => (
|
||||
<div
|
||||
onMouseDown={() => onFocus(id)}
|
||||
style={{
|
||||
display: "flex", flexDirection: "column", width: "100%", height: "100%",
|
||||
background: COLORS.bgPanel, borderRadius: 8, overflow: "hidden",
|
||||
border: focused ? `2px solid ${COLORS.accent}` : `1px solid ${COLORS.borderSubtle}`,
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (running[id] === false) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", width: "100%", background: "#0A0D12", color: "#8B97A6", flexDirection: "column", gap: 10 }}>
|
||||
<div style={{ fontFamily: "monospace", fontSize: 13 }}>Process exited</div>
|
||||
<button onClick={() => void restartSurface(id)} style={{ padding: "6px 14px" }}>⏎ Restart</button>
|
||||
return card(
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", width: "100%", color: COLORS.textSecondary, flexDirection: "column", gap: 10 }}>
|
||||
<div style={{ fontFamily: FONT.mono, fontSize: 13 }}>Process exited</div>
|
||||
<button onClick={() => void restartSurface(id)}
|
||||
style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: COLORS.bgElevated, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
|
||||
<RotateCw size={13} /> Restart
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", width: "100%", height: "100%" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 7, padding: "3px 8px", background: "#0A0D12", borderBottom: "1px solid #232A33" }}>
|
||||
<StatusRing state={states[id] ?? "idle"} running={true} />
|
||||
<span style={{ fontFamily: "monospace", fontSize: 11, color: "#8B97A6" }}>{id}</span>
|
||||
|
||||
const spec = surfaces[id]?.spec;
|
||||
const agent = spec?.agent_label ?? "shell";
|
||||
const state = states[id] ?? "idle";
|
||||
return card(
|
||||
<>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, height: 30, flex: "0 0 30px", padding: "0 10px", background: COLORS.bgElevated, borderBottom: `1px solid ${COLORS.borderSubtle}` }}>
|
||||
<StatusRing state={state} running={true} />
|
||||
<span style={{ fontFamily: FONT.mono, fontSize: 12, fontWeight: 600, color: COLORS.textPrimary }}>{agent}</span>
|
||||
{spec?.cwd && <span style={{ fontFamily: FONT.mono, fontSize: 11, color: COLORS.textMuted, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{shortPath(spec.cwd)}</span>}
|
||||
<span style={{ flex: 1 }} />
|
||||
<span style={{ display: "flex", alignItems: "center", height: 16, padding: "0 7px", borderRadius: 8, background: "#000", fontFamily: FONT.mono, fontSize: 10, fontWeight: 600, color: STATE_COLOR[state] }}>
|
||||
{state}
|
||||
</span>
|
||||
<Maximize2 size={13} color={COLORS.textMuted} style={{ cursor: "pointer" }} aria-label="Zoom (mock)" />
|
||||
</div>
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
<TerminalView key={id} surfaceId={id} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,7 +102,7 @@ function Node({ workspaceId, node, path, running, states }: { workspaceId: strin
|
||||
next[i + 1] = Math.max(0.05, (next[i + 1] ?? 1) - deltaFrac);
|
||||
void setRatios(workspaceId, path, next);
|
||||
}}>
|
||||
<Node workspaceId={workspaceId} node={child} path={[...path, i]} running={running} states={states} />
|
||||
<Node workspaceId={workspaceId} node={child} path={[...path, i]} running={running} states={states} surfaces={surfaces} focusedId={focusedId} onFocus={onFocus} />
|
||||
</Pane>
|
||||
))}
|
||||
</div>
|
||||
@@ -69,8 +116,7 @@ function Pane({ grow, isLast, orient, onResize, children }: { grow: number; isLa
|
||||
const parent = ref.current?.parentElement;
|
||||
if (!parent) return;
|
||||
const total = orient === "h" ? parent.clientWidth : parent.clientHeight;
|
||||
const start = orient === "h" ? e.clientX : e.clientY;
|
||||
let last = start;
|
||||
let last = orient === "h" ? e.clientX : e.clientY;
|
||||
const move = (ev: MouseEvent) => {
|
||||
const cur = orient === "h" ? ev.clientX : ev.clientY;
|
||||
const delta = (cur - last) / total;
|
||||
@@ -92,9 +138,9 @@ function Pane({ grow, isLast, orient, onResize, children }: { grow: number; isLa
|
||||
{!isLast && (
|
||||
<div onMouseDown={startDrag}
|
||||
style={{
|
||||
flex: "0 0 4px",
|
||||
flex: "0 0 10px",
|
||||
cursor: orient === "h" ? "col-resize" : "row-resize",
|
||||
background: "#232A33",
|
||||
background: "transparent",
|
||||
}} />
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user