diff --git a/src/design-system/layout/AppShell/AppShell.module.css b/src/design-system/layout/AppShell/AppShell.module.css new file mode 100644 index 0000000..866a9cd --- /dev/null +++ b/src/design-system/layout/AppShell/AppShell.module.css @@ -0,0 +1,13 @@ +.app { + display: flex; + height: 100vh; + overflow: hidden; +} + +.main { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; +} diff --git a/src/design-system/layout/AppShell/AppShell.tsx b/src/design-system/layout/AppShell/AppShell.tsx new file mode 100644 index 0000000..8083e52 --- /dev/null +++ b/src/design-system/layout/AppShell/AppShell.tsx @@ -0,0 +1,20 @@ +import styles from './AppShell.module.css' +import type { ReactNode } from 'react' + +interface AppShellProps { + sidebar: ReactNode + children: ReactNode + detail?: ReactNode +} + +export function AppShell({ sidebar, children, detail }: AppShellProps) { + return ( +
+ {sidebar} +
+ {children} +
+ {detail} +
+ ) +} diff --git a/src/design-system/layout/Sidebar/Sidebar.module.css b/src/design-system/layout/Sidebar/Sidebar.module.css new file mode 100644 index 0000000..045fae7 --- /dev/null +++ b/src/design-system/layout/Sidebar/Sidebar.module.css @@ -0,0 +1,319 @@ +.sidebar { + width: 220px; + flex-shrink: 0; + background: var(--sidebar-bg); + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Logo */ +.logo { + padding: 16px 18px; + display: flex; + align-items: center; + gap: 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + flex-shrink: 0; +} + +.brand { + font-family: var(--font-mono); + font-weight: 600; + font-size: 15px; + color: var(--amber-light); + letter-spacing: -0.3px; +} + +.version { + font-family: var(--font-mono); + font-size: 10px; + color: var(--sidebar-muted); + margin-left: 4px; +} + +/* Search */ +.searchWrap { + padding: 10px 12px; + flex-shrink: 0; +} + +.searchInner { + position: relative; +} + +.searchInput { + width: 100%; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--radius-sm); + padding: 6px 10px 6px 28px; + color: var(--sidebar-text); + font-family: var(--font-body); + font-size: 12px; + outline: none; + transition: border-color 0.15s; +} + +.searchInput::placeholder { + color: var(--sidebar-muted); +} + +.searchInput:focus { + border-color: rgba(198, 130, 14, 0.4); +} + +.searchIcon { + position: absolute; + left: 9px; + top: 50%; + transform: translateY(-50%); + color: var(--sidebar-muted); + display: flex; + align-items: center; +} + +/* Scrollable nav area */ +.navArea { + flex: 1; + overflow-y: auto; + min-height: 0; +} + +/* Section headers */ +.section { + padding: 14px 12px 5px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1.2px; + color: var(--sidebar-muted); +} + +/* Items container */ +.items { + padding: 0 6px; +} + +/* Nav item */ +.item { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 12px; + border-radius: var(--radius-sm); + color: var(--sidebar-text); + font-size: 13px; + cursor: pointer; + transition: all 0.12s; + border-left: 3px solid transparent; + margin-bottom: 1px; + user-select: none; +} + +.item:hover { + background: var(--sidebar-hover); + color: #e8dfd4; +} + +.item.active { + background: var(--sidebar-active); + color: var(--amber-light); + border-left-color: var(--amber); +} + +.item.active .itemCount { + background: rgba(198, 130, 14, 0.2); + color: var(--amber-light); +} + +/* Indented route items */ +.indented { + padding-left: 22px; +} + +.routeArrow { + color: var(--sidebar-muted); + font-size: 10px; +} + +/* Item sub-elements */ +.itemInfo { + flex: 1; + min-width: 0; +} + +.itemName { + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.itemMeta { + font-size: 11px; + color: var(--sidebar-muted); + font-family: var(--font-mono); +} + +.itemCount { + font-family: var(--font-mono); + font-size: 11px; + color: var(--sidebar-muted); + background: rgba(255, 255, 255, 0.06); + padding: 1px 6px; + border-radius: 10px; + flex-shrink: 0; +} + +/* Health dots */ +.healthDot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; +} + +.healthLive { + background: #5db866; + box-shadow: 0 0 6px rgba(93, 184, 102, 0.4); +} + +.healthStale { + background: var(--warning); +} + +.healthDead { + background: var(--sidebar-muted); +} + +/* Divider */ +.divider { + height: 1px; + background: rgba(255, 255, 255, 0.06); + margin: 6px 12px; +} + +/* Agents header */ +.agentsHeader { + padding: 14px 12px 6px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1.2px; + color: var(--sidebar-muted); + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; +} + +.agentBadge { + font-family: var(--font-mono); + font-size: 10px; + padding: 1px 6px; + border-radius: 10px; + background: rgba(93, 184, 102, 0.15); + color: #5db866; +} + +/* Agents list */ +.agentsList { + padding: 0 0 6px; + overflow-y: auto; + max-height: 180px; + flex-shrink: 0; +} + +.agentItem { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + margin: 0 6px 2px; + border-radius: var(--radius-sm); + font-size: 11px; + color: var(--sidebar-text); + transition: background 0.1s; +} + +.agentItem:hover { + background: var(--sidebar-hover); +} + +.agentDot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} + +.agentInfo { + flex: 1; + min-width: 0; +} + +.agentName { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.agentDetail { + font-size: 10px; + color: var(--sidebar-muted); +} + +.agentStats { + text-align: right; + font-family: var(--font-mono); + font-size: 10px; + color: var(--sidebar-muted); +} + +.agentTps { + color: var(--sidebar-text); +} + +.agentLastSeen { + color: var(--sidebar-muted); +} + +.agentError { + color: var(--error); +} + +/* Bottom links */ +.bottom { + border-top: 1px solid rgba(255, 255, 255, 0.06); + padding: 6px; + flex-shrink: 0; +} + +.bottomItem { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 12px; + border-radius: var(--radius-sm); + color: var(--sidebar-muted); + font-size: 12px; + cursor: pointer; + transition: all 0.12s; + border-left: 3px solid transparent; + margin-bottom: 1px; +} + +.bottomItem:hover { + background: var(--sidebar-hover); + color: var(--sidebar-text); +} + +.bottomIcon { + font-size: 13px; + width: 18px; + text-align: center; +} diff --git a/src/design-system/layout/Sidebar/Sidebar.tsx b/src/design-system/layout/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..d974280 --- /dev/null +++ b/src/design-system/layout/Sidebar/Sidebar.tsx @@ -0,0 +1,241 @@ +import { useState } from 'react' +import styles from './Sidebar.module.css' + +export interface App { + id: string + name: string + agentCount: number + health: 'live' | 'stale' | 'dead' + execCount: number +} + +export interface Route { + id: string + name: string + execCount: number +} + +export interface Agent { + id: string + name: string + service: string + version: string + tps: string + lastSeen: string + status: 'live' | 'stale' | 'dead' + errorRate?: string +} + +interface SidebarProps { + apps: App[] + routes: Route[] + agents: Agent[] + activeItem?: string + onItemClick?: (id: string) => void +} + +function HealthDot({ status }: { status: 'live' | 'stale' | 'dead' }) { + return ( + + ) +} + +// Camel SVG silhouette +function CamelIcon() { + return ( + + ) +} + +export function Sidebar({ + apps, + routes, + agents, + activeItem, + onItemClick, +}: SidebarProps) { + const [search, setSearch] = useState('') + + const liveCount = agents.filter((a) => a.status === 'live').length + const agentBadge = `${liveCount}/${agents.length} live` + + const filteredApps = search + ? apps.filter((a) => a.name.toLowerCase().includes(search.toLowerCase())) + : apps + + return ( + + ) +} diff --git a/src/design-system/layout/TopBar/TopBar.module.css b/src/design-system/layout/TopBar/TopBar.module.css new file mode 100644 index 0000000..e66a8ea --- /dev/null +++ b/src/design-system/layout/TopBar/TopBar.module.css @@ -0,0 +1,110 @@ +.topbar { + display: flex; + align-items: center; + gap: 12px; + padding: 0 24px; + height: 48px; + flex-shrink: 0; + background: var(--bg-surface); + border-bottom: 1px solid var(--border); +} + +/* Breadcrumb area - left side */ +.breadcrumb { + flex-shrink: 0; +} + +/* Center search trigger */ +.search { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-raised); + color: var(--text-muted); + font-size: 12px; + font-family: var(--font-body); + cursor: pointer; + transition: border-color 0.15s; + min-width: 280px; + flex: 1; + max-width: 400px; + text-align: left; +} + +.search:hover { + border-color: var(--text-faint); +} + +.searchIcon { + display: flex; + align-items: center; + color: var(--text-faint); + flex-shrink: 0; +} + +.searchPlaceholder { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.kbd { + font-family: var(--font-mono); + font-size: 10px; + background: var(--bg-surface); + border: 1px solid var(--border); + padding: 0 4px; + border-radius: 3px; + color: var(--text-faint); + flex-shrink: 0; + line-height: 1.6; +} + +/* Right section */ +.right { + margin-left: auto; + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.env { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 600; + padding: 3px 10px; + border-radius: 10px; + background: var(--success-bg); + color: var(--success); + border: 1px solid var(--success-border); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.shift { + font-family: var(--font-mono); + font-size: 10px; + padding: 3px 10px; + border-radius: 10px; + background: var(--running-bg); + color: var(--running); + border: 1px solid var(--running-border); +} + +.user { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--text-secondary); +} + +.userName { + color: var(--text-secondary); + font-size: 12px; +} diff --git a/src/design-system/layout/TopBar/TopBar.tsx b/src/design-system/layout/TopBar/TopBar.tsx new file mode 100644 index 0000000..abd165d --- /dev/null +++ b/src/design-system/layout/TopBar/TopBar.tsx @@ -0,0 +1,66 @@ +import styles from './TopBar.module.css' +import { Breadcrumb } from '../../composites/Breadcrumb/Breadcrumb' +import { Avatar } from '../../primitives/Avatar/Avatar' + +interface BreadcrumbItem { + label: string + href?: string +} + +interface TopBarProps { + breadcrumb: BreadcrumbItem[] + environment?: string + shift?: string + user?: { name: string } + onSearchClick?: () => void + className?: string +} + +export function TopBar({ + breadcrumb, + environment, + shift, + user, + onSearchClick, + className, +}: TopBarProps) { + return ( +
+ {/* Left: Breadcrumb */} + + + {/* Center: Search trigger */} + + + {/* Right: env badge, shift, user */} +
+ {environment && ( + {environment} + )} + {shift && ( + Shift: {shift} + )} + {user && ( +
+ {user.name} + +
+ )} +
+
+ ) +} diff --git a/src/design-system/layout/index.ts b/src/design-system/layout/index.ts new file mode 100644 index 0000000..b911ccd --- /dev/null +++ b/src/design-system/layout/index.ts @@ -0,0 +1,4 @@ +export { AppShell } from './AppShell/AppShell' +export { Sidebar } from './Sidebar/Sidebar' +export type { App, Route, Agent } from './Sidebar/Sidebar' +export { TopBar } from './TopBar/TopBar'