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:
+59
-29
@@ -1,9 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { Plus, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { COLORS, FONT, STATE_COLOR } from "./theme";
|
||||
import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes";
|
||||
|
||||
const RING: Record<SurfaceState | "stopped", string> = {
|
||||
error: "#F4544E", wait: "#F2B84B", work: "#4C8DFF", done: "#3FB950", idle: "#5A6573", stopped: "#5A6573",
|
||||
};
|
||||
|
||||
function aggregate(w: WorkspaceView): SurfaceState | "stopped" {
|
||||
const order: SurfaceState[] = ["error", "wait", "work", "done", "idle"];
|
||||
const running = Object.values(w.surfaces).filter((s) => s.running);
|
||||
@@ -23,36 +22,67 @@ export function Sidebar({
|
||||
onSelect: (id: string) => void;
|
||||
onNew: () => void;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
||||
const byGroup = (gid: string | null) => workspaces.filter((w) => (w.group_id ?? null) === gid).sort((a, b) => a.order - b.order);
|
||||
const ungrouped = byGroup(null);
|
||||
|
||||
const row = (w: WorkspaceView) => (
|
||||
<div key={w.id} onClick={() => onSelect(w.id)}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: 9, padding: "6px 8px", borderRadius: 6, cursor: "pointer",
|
||||
background: w.id === activeId ? "#1A2029" : "transparent", fontFamily: "Inter", fontSize: 13,
|
||||
color: w.id === activeId ? "#E6EDF3" : "#8B97A6",
|
||||
}}>
|
||||
<span style={{ width: 10, height: 10, borderRadius: "50%", border: `2px solid ${RING[aggregate(w)]}`, boxSizing: "border-box" }} />
|
||||
<span style={{ flex: 1 }}>{w.name}</span>
|
||||
{w.unread && <span style={{ width: 7, height: 7, borderRadius: "50%", background: "#4C8DFF" }} />}
|
||||
<span style={{ fontFamily: "monospace", fontSize: 11, color: "#5A6573" }}>{Object.keys(w.surfaces).length}</span>
|
||||
</div>
|
||||
);
|
||||
const row = (w: WorkspaceView) => {
|
||||
const isActive = w.id === activeId;
|
||||
return (
|
||||
<div key={w.id} onClick={() => onSelect(w.id)}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: 10, height: 34, padding: "0 8px", borderRadius: 6, cursor: "pointer",
|
||||
background: isActive ? COLORS.bgElevated : "transparent", fontFamily: FONT.ui, fontSize: 13,
|
||||
color: isActive ? COLORS.textPrimary : COLORS.textSecondary,
|
||||
}}>
|
||||
<span style={{ width: 10, height: 10, borderRadius: "50%", border: `2px solid ${STATE_COLOR[aggregate(w)]}`, boxSizing: "border-box", flex: "0 0 10px" }} />
|
||||
<span style={{ flex: 1, fontWeight: isActive ? 600 : 400, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{w.name}</span>
|
||||
{w.unread && <span style={{ width: 7, height: 7, borderRadius: "50%", background: COLORS.accent, flex: "0 0 7px" }} />}
|
||||
<span style={{ display: "flex", alignItems: "center", justifyContent: "center", height: 18, minWidth: 18, padding: "0 6px", borderRadius: 9, background: COLORS.bgApp, fontFamily: FONT.mono, fontSize: 11, color: COLORS.textSecondary }}>
|
||||
{Object.keys(w.surfaces).length}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ width: 248, background: "#13171F", height: "100%", padding: 14, boxSizing: "border-box", overflowY: "auto" }}>
|
||||
<button onClick={onNew} style={{ width: "100%", padding: 8, marginBottom: 16, background: "#1A2029", color: "#E6EDF3", border: "1px solid #323C49", borderRadius: 7 }}>+ New workspace</button>
|
||||
{groups.sort((a, b) => a.order - b.order).map((g) => (
|
||||
<div key={g.id} style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 7, padding: "0 4px", marginBottom: 4 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: 2, background: g.color }} />
|
||||
<span style={{ fontFamily: "Inter", fontSize: 11, fontWeight: 700, letterSpacing: 0.5, color: "#8B97A6" }}>{g.name.toUpperCase()}</span>
|
||||
</div>
|
||||
{byGroup(g.id).map(row)}
|
||||
</div>
|
||||
))}
|
||||
{ungrouped.length > 0 && <div style={{ marginTop: 8 }}>{ungrouped.map(row)}</div>}
|
||||
<div style={{ display: "flex", flexDirection: "column", width: 248, flex: "0 0 248px", background: COLORS.bgSidebar, height: "100%", padding: 14, boxSizing: "border-box" }}>
|
||||
<button onClick={onNew}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", justifyContent: "center", gap: 8, width: "100%", height: 34, marginBottom: 16,
|
||||
background: COLORS.bgElevated, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7,
|
||||
fontFamily: FONT.ui, fontSize: 13, fontWeight: 600,
|
||||
}}>
|
||||
<Plus size={15} />
|
||||
New workspace
|
||||
<span style={{ fontFamily: FONT.mono, fontSize: 11, color: COLORS.textMuted }}>⌘N</span>
|
||||
</button>
|
||||
|
||||
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: 2, minHeight: 0 }}>
|
||||
{groups.sort((a, b) => a.order - b.order).map((g) => {
|
||||
const open = !collapsed[g.id];
|
||||
return (
|
||||
<div key={g.id} style={{ marginBottom: 8 }}>
|
||||
<div onClick={() => setCollapsed((c) => ({ ...c, [g.id]: open }))}
|
||||
style={{ display: "flex", alignItems: "center", gap: 7, height: 24, padding: "0 4px", cursor: "pointer" }}>
|
||||
{open ? <ChevronDown size={13} color={COLORS.textMuted} /> : <ChevronRight size={13} color={COLORS.textMuted} />}
|
||||
<span style={{ width: 8, height: 8, borderRadius: 2, background: g.color }} />
|
||||
<span style={{ fontFamily: FONT.ui, fontSize: 11, fontWeight: 700, letterSpacing: 0.5, color: COLORS.textSecondary }}>{g.name.toUpperCase()}</span>
|
||||
</div>
|
||||
{open && byGroup(g.id).map(row)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{ungrouped.length > 0 && <div style={{ marginTop: 4, display: "flex", flexDirection: "column", gap: 2 }}>{ungrouped.map(row)}</div>}
|
||||
</div>
|
||||
|
||||
{/* Daemon status footer — uptime is mocked until the daemon reports it. */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, height: 30, marginTop: 10, padding: "0 6px", borderRadius: 6, background: COLORS.bgPanel }}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: "50%", background: COLORS.stDone, flex: "0 0 7px" }} />
|
||||
<span style={{ fontFamily: FONT.mono, fontSize: 11, color: COLORS.textSecondary }}>spaceshd · live</span>
|
||||
<span style={{ flex: 1 }} />
|
||||
<span style={{ fontFamily: FONT.mono, fontSize: 11, color: COLORS.textMuted }}>3d 4h</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user