feat: AppShell, Sidebar, TopBar layout components
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
src/design-system/layout/AppShell/AppShell.module.css
Normal file
13
src/design-system/layout/AppShell/AppShell.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.app {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
20
src/design-system/layout/AppShell/AppShell.tsx
Normal file
20
src/design-system/layout/AppShell/AppShell.tsx
Normal file
@@ -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 (
|
||||
<div className={styles.app}>
|
||||
{sidebar}
|
||||
<div className={styles.main}>
|
||||
{children}
|
||||
</div>
|
||||
{detail}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
319
src/design-system/layout/Sidebar/Sidebar.module.css
Normal file
319
src/design-system/layout/Sidebar/Sidebar.module.css
Normal file
@@ -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;
|
||||
}
|
||||
241
src/design-system/layout/Sidebar/Sidebar.tsx
Normal file
241
src/design-system/layout/Sidebar/Sidebar.tsx
Normal file
@@ -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 (
|
||||
<span
|
||||
className={[
|
||||
styles.healthDot,
|
||||
status === 'live' ? styles.healthLive : '',
|
||||
status === 'stale' ? styles.healthStale : '',
|
||||
status === 'dead' ? styles.healthDead : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Camel SVG silhouette
|
||||
function CamelIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="28"
|
||||
height="24"
|
||||
viewBox="0 0 28 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Simple camel silhouette */}
|
||||
<path
|
||||
d="M4 20 L4 16 L6 14 L6 12 C6 10 7 9 9 9 L10 8 C10 6 11 5 12 5 C12 3 13 2 14 2 C15 2 15.5 3 15 5 C16 5 17 6 17 8 L18 9 C20 9 21 10 21 12 L21 14 L22 14 L22 12 C22 10 23 9 24 9 L24 20"
|
||||
fill="var(--amber-light)"
|
||||
stroke="var(--amber)"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
<path
|
||||
d="M4 20 L24 20"
|
||||
stroke="var(--amber)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<aside className={styles.sidebar}>
|
||||
{/* Logo */}
|
||||
<div className={styles.logo}>
|
||||
<CamelIcon />
|
||||
<div>
|
||||
<span className={styles.brand}>cameleer</span>
|
||||
<span className={styles.version}>v3.2.1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className={styles.searchWrap}>
|
||||
<div className={styles.searchInner}>
|
||||
<span className={styles.searchIcon} aria-hidden="true">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
className={styles.searchInput}
|
||||
type="text"
|
||||
placeholder="Filter apps..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable nav area */}
|
||||
<div className={styles.navArea}>
|
||||
{/* Applications section */}
|
||||
<div className={styles.section}>Applications</div>
|
||||
<div className={styles.items}>
|
||||
{filteredApps.map((app) => (
|
||||
<div
|
||||
key={app.id}
|
||||
className={[
|
||||
styles.item,
|
||||
activeItem === app.id ? styles.active : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={() => onItemClick?.(app.id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') onItemClick?.(app.id)
|
||||
}}
|
||||
>
|
||||
<HealthDot status={app.health} />
|
||||
<div className={styles.itemInfo}>
|
||||
<div className={styles.itemName}>{app.name}</div>
|
||||
<div className={styles.itemMeta}>{app.agentCount} agent{app.agentCount !== 1 ? 's' : ''}</div>
|
||||
</div>
|
||||
<span className={styles.itemCount}>{app.execCount.toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className={styles.divider} />
|
||||
|
||||
{/* Routes section */}
|
||||
<div className={styles.section}>Routes</div>
|
||||
<div className={styles.items}>
|
||||
{routes.map((route) => (
|
||||
<div
|
||||
key={route.id}
|
||||
className={[
|
||||
styles.item,
|
||||
styles.indented,
|
||||
activeItem === route.id ? styles.active : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={() => onItemClick?.(route.id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') onItemClick?.(route.id)
|
||||
}}
|
||||
>
|
||||
<span className={styles.routeArrow}>▸</span>
|
||||
<div className={styles.itemInfo}>
|
||||
<div className={styles.itemName}>{route.name}</div>
|
||||
</div>
|
||||
<span className={styles.itemCount}>{route.execCount.toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent health section */}
|
||||
<div className={styles.agentsHeader}>
|
||||
<span>Agents</span>
|
||||
<span className={styles.agentBadge}>{agentBadge}</span>
|
||||
</div>
|
||||
<div className={styles.agentsList}>
|
||||
{agents.map((agent) => (
|
||||
<div key={agent.id} className={styles.agentItem}>
|
||||
<span
|
||||
className={[
|
||||
styles.agentDot,
|
||||
agent.status === 'live' ? styles.healthLive : '',
|
||||
agent.status === 'stale' ? styles.healthStale : '',
|
||||
agent.status === 'dead' ? styles.healthDead : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
/>
|
||||
<div className={styles.agentInfo}>
|
||||
<div className={styles.agentName}>{agent.name}</div>
|
||||
<div className={styles.agentDetail}>{agent.service} {agent.version}</div>
|
||||
</div>
|
||||
<div className={styles.agentStats}>
|
||||
<div className={styles.agentTps}>{agent.tps}</div>
|
||||
<div
|
||||
className={agent.errorRate ? styles.agentError : styles.agentLastSeen}
|
||||
>
|
||||
{agent.errorRate ?? agent.lastSeen}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom links */}
|
||||
<div className={styles.bottom}>
|
||||
<div className={styles.bottomItem}>
|
||||
<span className={styles.bottomIcon}>⚙</span>
|
||||
<div className={styles.itemInfo}>
|
||||
<div className={styles.itemName}>Admin</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.bottomItem}>
|
||||
<span className={styles.bottomIcon}>☰</span>
|
||||
<div className={styles.itemInfo}>
|
||||
<div className={styles.itemName}>API Docs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
110
src/design-system/layout/TopBar/TopBar.module.css
Normal file
110
src/design-system/layout/TopBar/TopBar.module.css
Normal file
@@ -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;
|
||||
}
|
||||
66
src/design-system/layout/TopBar/TopBar.tsx
Normal file
66
src/design-system/layout/TopBar/TopBar.tsx
Normal file
@@ -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 (
|
||||
<header className={`${styles.topbar} ${className ?? ''}`}>
|
||||
{/* Left: Breadcrumb */}
|
||||
<Breadcrumb items={breadcrumb} className={styles.breadcrumb} />
|
||||
|
||||
{/* Center: Search trigger */}
|
||||
<button
|
||||
className={styles.search}
|
||||
onClick={onSearchClick}
|
||||
type="button"
|
||||
aria-label="Open search"
|
||||
>
|
||||
<span className={styles.searchIcon} aria-hidden="true">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className={styles.searchPlaceholder}>Search by Order ID, route, error...</span>
|
||||
<span className={styles.kbd}>Ctrl+K</span>
|
||||
</button>
|
||||
|
||||
{/* Right: env badge, shift, user */}
|
||||
<div className={styles.right}>
|
||||
{environment && (
|
||||
<span className={styles.env}>{environment}</span>
|
||||
)}
|
||||
{shift && (
|
||||
<span className={styles.shift}>Shift: {shift}</span>
|
||||
)}
|
||||
{user && (
|
||||
<div className={styles.user}>
|
||||
<span className={styles.userName}>{user.name}</span>
|
||||
<Avatar name={user.name} size="md" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
4
src/design-system/layout/index.ts
Normal file
4
src/design-system/layout/index.ts
Normal file
@@ -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'
|
||||
Reference in New Issue
Block a user