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
-13
@@ -1,3 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { Check, Hourglass, X, CircleDot, Power, Send, MessageSquare } from "lucide-react";
|
||||
import { COLORS, FONT } from "./theme";
|
||||
import type { SurfaceState } from "./layoutTypes";
|
||||
|
||||
export interface FeedEntry {
|
||||
@@ -9,29 +12,79 @@ export interface FeedEntry {
|
||||
time: string;
|
||||
}
|
||||
|
||||
const ICON: Record<string, string> = { done: "✓", wait: "⌛", error: "✕", work: "●", idle: "·", exit: "⏻" };
|
||||
const COLOR: Record<string, string> = { done: "#3FB950", wait: "#F2B84B", error: "#F4544E", work: "#4C8DFF", idle: "#5A6573", exit: "#5A6573" };
|
||||
const ICON: Record<string, React.ReactNode> = {
|
||||
done: <Check size={13} />, wait: <Hourglass size={13} />, error: <X size={13} />,
|
||||
work: <CircleDot size={13} />, idle: <CircleDot size={13} />, exit: <Power size={13} />,
|
||||
};
|
||||
const COLOR: Record<string, string> = {
|
||||
done: COLORS.stDone, wait: COLORS.stWait, error: COLORS.stError, work: COLORS.stWork, idle: COLORS.stIdle, exit: COLORS.textMuted,
|
||||
};
|
||||
|
||||
type Tab = "all" | "unread" | "errors";
|
||||
const TABS: { id: Tab; label: string }[] = [
|
||||
{ id: "all", label: "All" },
|
||||
{ id: "unread", label: "Unread" },
|
||||
{ id: "errors", label: "Errors" },
|
||||
];
|
||||
|
||||
export function EventCenter({ feed, onMarkRead, onSelect }: { feed: FeedEntry[]; onMarkRead: () => void; onSelect: (surfaceId: string) => void }) {
|
||||
const [tab, setTab] = useState<Tab>("all");
|
||||
const shown = tab === "errors" ? feed.filter((e) => e.kind === "error") : feed;
|
||||
|
||||
return (
|
||||
<div style={{ width: 300, background: "#13171F", height: "100%", padding: 14, boxSizing: "border-box", display: "flex", flexDirection: "column", borderLeft: "1px solid #232A33" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", width: 300, flex: "0 0 300px", background: COLORS.bgSidebar, height: "100%", padding: 14, boxSizing: "border-box", borderLeft: `1px solid ${COLORS.borderSubtle}` }}>
|
||||
<div style={{ display: "flex", alignItems: "center", marginBottom: 12 }}>
|
||||
<span style={{ fontFamily: "Inter", fontSize: 13, fontWeight: 700, color: "#E6EDF3", flex: 1 }}>Event Center</span>
|
||||
<span onClick={onMarkRead} style={{ fontSize: 11, color: "#4C8DFF", cursor: "pointer" }}>Mark all read</span>
|
||||
<span style={{ fontFamily: FONT.ui, fontSize: 13, fontWeight: 700, color: COLORS.textPrimary, flex: 1 }}>Event Center</span>
|
||||
<span onClick={onMarkRead} style={{ fontFamily: FONT.ui, fontSize: 11, color: COLORS.accent, cursor: "pointer" }}>Mark all read</span>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{feed.length === 0 && <div style={{ color: "#5A6573", fontSize: 12 }}>No events yet.</div>}
|
||||
{feed.map((e) => (
|
||||
|
||||
<div style={{ display: "flex", gap: 6, marginBottom: 12 }}>
|
||||
{TABS.map((t) => {
|
||||
const on = t.id === tab;
|
||||
return (
|
||||
<button key={t.id} onClick={() => setTab(t.id)}
|
||||
style={{
|
||||
height: 22, padding: "0 9px", borderRadius: 11, fontFamily: FONT.ui, fontSize: 11, fontWeight: on ? 600 : 400,
|
||||
background: on ? COLORS.bgElevated : "transparent",
|
||||
border: `1px solid ${on ? COLORS.borderStrong : "transparent"}`,
|
||||
color: on ? COLORS.textPrimary : COLORS.textMuted,
|
||||
}}>
|
||||
{t.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: 8, minHeight: 0 }}>
|
||||
{shown.length === 0 && <div style={{ color: COLORS.textMuted, fontSize: 12 }}>No events yet.</div>}
|
||||
{shown.map((e) => (
|
||||
<div key={e.id} onClick={() => onSelect(e.surfaceId)}
|
||||
style={{ display: "flex", gap: 9, padding: 10, borderRadius: 8, border: "1px solid #232A33", cursor: "pointer" }}>
|
||||
<span style={{ color: COLOR[e.kind] }}>{ICON[e.kind]}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontFamily: "monospace", fontSize: 11, color: "#8B97A6" }}>{e.workspace} · {e.agent}</div>
|
||||
<div style={{ fontFamily: "Inter", fontSize: 12, color: "#E6EDF3" }}>{e.kind} <span style={{ color: "#5A6573" }}>{e.time}</span></div>
|
||||
style={{ display: "flex", gap: 9, padding: 10, borderRadius: 8, border: `1px solid ${COLORS.borderSubtle}`, cursor: "pointer" }}>
|
||||
<span style={{ color: COLOR[e.kind], display: "flex", alignItems: "center" }}>{ICON[e.kind]}</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontFamily: FONT.mono, fontSize: 11, color: COLORS.textSecondary, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{e.workspace} · {e.agent}</div>
|
||||
<div style={{ fontFamily: FONT.ui, fontSize: 12, color: COLORS.textPrimary }}>{e.kind} <span style={{ color: COLORS.textMuted }}>{e.time}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* External notification channels — mocked until the daemon subscriber lands (M5). */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 10, paddingTop: 10, borderTop: `1px solid ${COLORS.borderSubtle}` }}>
|
||||
<span style={{ fontFamily: FONT.ui, fontSize: 10, fontWeight: 700, letterSpacing: 0.5, color: COLORS.textMuted }}>EXTERNAL NOTIFY</span>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
{[
|
||||
{ name: "Telegram", icon: <Send size={13} /> },
|
||||
{ name: "MAX", icon: <MessageSquare size={13} /> },
|
||||
].map((c) => (
|
||||
<div key={c.name} style={{ display: "flex", alignItems: "center", gap: 7, flex: 1, height: 30, padding: "0 10px", borderRadius: 7, background: COLORS.bgPanel }}>
|
||||
<span style={{ color: COLORS.textMuted, display: "flex" }}>{c.icon}</span>
|
||||
<span style={{ fontFamily: FONT.ui, fontSize: 12, color: COLORS.textSecondary, flex: 1 }}>{c.name}</span>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: COLORS.textMuted }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user