Merge branch 'feature/design-system-gap-fill'
This commit is contained in:
87
src/pages/Inventory/Inventory.module.css
Normal file
87
src/pages/Inventory/Inventory.module.css
Normal file
@@ -0,0 +1,87 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-body);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.backLink {
|
||||
font-size: 13px;
|
||||
color: var(--amber);
|
||||
text-decoration: none;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.backLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 57px;
|
||||
height: calc(100vh - 57px);
|
||||
overflow-y: auto;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.navSection {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.navLabel {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted);
|
||||
padding: 8px 8px 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navLink {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.navLink:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
41
src/pages/Inventory/Inventory.tsx
Normal file
41
src/pages/Inventory/Inventory.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import styles from './Inventory.module.css'
|
||||
import { PrimitivesSection } from './sections/PrimitivesSection'
|
||||
import { CompositesSection } from './sections/CompositesSection'
|
||||
import { LayoutSection } from './sections/LayoutSection'
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ label: 'Primitives', href: '#primitives' },
|
||||
{ label: 'Composites', href: '#composites' },
|
||||
{ label: 'Layout', href: '#layout' },
|
||||
]
|
||||
|
||||
export function Inventory() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<header className={styles.header}>
|
||||
<h1 className={styles.headerTitle}>Component Inventory</h1>
|
||||
<Link to="/" className={styles.backLink}>← Back to app</Link>
|
||||
</header>
|
||||
|
||||
<div className={styles.body}>
|
||||
<nav className={styles.nav} aria-label="Component categories">
|
||||
<div className={styles.navSection}>
|
||||
<span className={styles.navLabel}>Categories</span>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<a key={item.href} href={item.href} className={styles.navLink}>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className={styles.content}>
|
||||
<PrimitivesSection />
|
||||
<CompositesSection />
|
||||
<LayoutSection />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
74
src/pages/Inventory/sections/CompositesSection.module.css
Normal file
74
src/pages/Inventory/sections/CompositesSection.module.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.componentCard {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.componentTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.componentDesc {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.demoArea {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.demoAreaColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.demoAreaRow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demoLabel {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demoGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.demoGroupRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
598
src/pages/Inventory/sections/CompositesSection.tsx
Normal file
598
src/pages/Inventory/sections/CompositesSection.tsx
Normal file
@@ -0,0 +1,598 @@
|
||||
import { useState } from 'react'
|
||||
import styles from './CompositesSection.module.css'
|
||||
import {
|
||||
Accordion,
|
||||
AlertDialog,
|
||||
AreaChart,
|
||||
AvatarGroup,
|
||||
BarChart,
|
||||
Breadcrumb,
|
||||
CommandPalette,
|
||||
DataTable,
|
||||
DetailPanel,
|
||||
Dropdown,
|
||||
EventFeed,
|
||||
FilterBar,
|
||||
LineChart,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Popover,
|
||||
ProcessorTimeline,
|
||||
ShortcutsBar,
|
||||
Tabs,
|
||||
ToastProvider,
|
||||
useToast,
|
||||
TreeView,
|
||||
} from '../../../design-system/composites'
|
||||
import type { SearchResult } from '../../../design-system/composites'
|
||||
import { Button } from '../../../design-system/primitives'
|
||||
|
||||
// ── DemoCard helper ──────────────────────────────────────────────────────────
|
||||
|
||||
interface DemoCardProps {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function DemoCard({ id, title, description, children }: DemoCardProps) {
|
||||
return (
|
||||
<div id={id} className={styles.componentCard}>
|
||||
<h3 className={styles.componentTitle}>{title}</h3>
|
||||
<p className={styles.componentDesc}>{description}</p>
|
||||
<div className={styles.demoArea}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Toast demo inner component (must be inside ToastProvider) ─────────────────
|
||||
|
||||
function ToastDemo() {
|
||||
const { toast } = useToast()
|
||||
return (
|
||||
<div className={styles.demoAreaRow}>
|
||||
<Button size="sm" variant="primary" onClick={() => toast({ title: 'Success!', description: 'Operation completed.', variant: 'success' })}>
|
||||
Success toast
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => toast({ title: 'Info', description: 'Something to note.', variant: 'info' })}>
|
||||
Info toast
|
||||
</Button>
|
||||
<Button size="sm" variant="danger" onClick={() => toast({ title: 'Error', description: 'Something went wrong.', variant: 'error' })}>
|
||||
Error toast
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => toast({ title: 'Warning', description: 'Proceed with caution.', variant: 'warning' })}>
|
||||
Warning toast
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Sample data ───────────────────────────────────────────────────────────────
|
||||
|
||||
const CHART_SERIES = [
|
||||
{
|
||||
label: 'Requests',
|
||||
data: [
|
||||
{ x: 0, y: 120 }, { x: 1, y: 180 }, { x: 2, y: 150 },
|
||||
{ x: 3, y: 210 }, { x: 4, y: 190 }, { x: 5, y: 240 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Errors',
|
||||
data: [
|
||||
{ x: 0, y: 5 }, { x: 1, y: 12 }, { x: 2, y: 8 },
|
||||
{ x: 3, y: 15 }, { x: 4, y: 7 }, { x: 5, y: 10 },
|
||||
],
|
||||
color: 'var(--error)',
|
||||
},
|
||||
]
|
||||
|
||||
const BAR_SERIES = [
|
||||
{
|
||||
label: 'GET',
|
||||
data: [
|
||||
{ x: 'Mon', y: 80 }, { x: 'Tue', y: 95 }, { x: 'Wed', y: 110 },
|
||||
{ x: 'Thu', y: 72 }, { x: 'Fri', y: 130 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'POST',
|
||||
data: [
|
||||
{ x: 'Mon', y: 40 }, { x: 'Tue', y: 55 }, { x: 'Wed', y: 60 },
|
||||
{ x: 'Thu', y: 38 }, { x: 'Fri', y: 75 },
|
||||
],
|
||||
color: 'var(--success)',
|
||||
},
|
||||
]
|
||||
|
||||
const COMMAND_PALETTE_DATA: SearchResult[] = [
|
||||
{ id: 'r1', category: 'route', title: 'order-ingest', meta: 'POST /orders/ingest' },
|
||||
{ id: 'r2', category: 'route', title: 'payment-validate', meta: 'POST /payments/validate' },
|
||||
{ id: 'e1', category: 'execution', title: 'exec-001', meta: 'Started 2m ago' },
|
||||
{ id: 'e2', category: 'execution', title: 'exec-002', meta: 'Completed 5m ago' },
|
||||
{ id: 'a1', category: 'agent', title: 'camel-agent-prod-1', meta: 'live · 42 tps' },
|
||||
{ id: 'x1', category: 'exchange', title: 'exch-aabb1122', meta: 'route: order-ingest' },
|
||||
]
|
||||
|
||||
interface TableRow {
|
||||
id: string
|
||||
name: string
|
||||
method: string
|
||||
status: string
|
||||
exchanges: number
|
||||
}
|
||||
|
||||
const TABLE_DATA: TableRow[] = [
|
||||
{ id: '1', name: 'order-ingest', method: 'POST', status: 'live', exchanges: 1243 },
|
||||
{ id: '2', name: 'payment-validate', method: 'POST', status: 'live', exchanges: 987 },
|
||||
{ id: '3', name: 'inventory-check', method: 'GET', status: 'stale', exchanges: 432 },
|
||||
{ id: '4', name: 'notify-customer', method: 'POST', status: 'live', exchanges: 876 },
|
||||
{ id: '5', name: 'archive-order', method: 'PUT', status: 'dead', exchanges: 54 },
|
||||
]
|
||||
|
||||
const NOW = new Date()
|
||||
const minsAgo = (n: number) => new Date(NOW.getTime() - n * 60 * 1000)
|
||||
|
||||
const FEED_EVENTS = [
|
||||
{ id: 'ev1', severity: 'success' as const, message: 'Route order-ingest started successfully', timestamp: minsAgo(1) },
|
||||
{ id: 'ev2', severity: 'warning' as const, message: 'Agent camel-agent-prod-2 response time elevated', timestamp: minsAgo(3) },
|
||||
{ id: 'ev3', severity: 'error' as const, message: 'Exchange exch-aabb1122 failed: timeout', timestamp: minsAgo(7) },
|
||||
{ id: 'ev4', severity: 'running' as const, message: 'Processor payment-validate processing batch', timestamp: minsAgo(10) },
|
||||
{ id: 'ev5', severity: 'success' as const, message: 'Deployment v3.2.1 completed', timestamp: minsAgo(15) },
|
||||
]
|
||||
|
||||
const TREE_NODES = [
|
||||
{
|
||||
id: 'app1',
|
||||
label: 'cameleer-prod',
|
||||
icon: '⬡',
|
||||
children: [
|
||||
{
|
||||
id: 'route1',
|
||||
label: 'order-ingest',
|
||||
icon: '→',
|
||||
children: [
|
||||
{ id: 'proc1', label: 'ValidateOrder', icon: '◈', meta: '12ms' },
|
||||
{ id: 'proc2', label: 'EnrichPayload', icon: '◈', meta: '8ms' },
|
||||
{ id: 'proc3', label: 'RouteToQueue', icon: '◈', meta: '3ms' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'route2',
|
||||
label: 'payment-validate',
|
||||
icon: '→',
|
||||
children: [
|
||||
{ id: 'proc4', label: 'TokenizeCard', icon: '◈', meta: '22ms' },
|
||||
{ id: 'proc5', label: 'AuthorizePayment', icon: '◈', meta: '45ms' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// ── CompositesSection ─────────────────────────────────────────────────────────
|
||||
|
||||
export function CompositesSection() {
|
||||
// 1. Accordion
|
||||
const accordionItems = [
|
||||
{ id: 'a1', title: 'What is Apache Camel?', content: 'Apache Camel is an open-source integration framework based on enterprise integration patterns.' },
|
||||
{ id: 'a2', title: 'How do routes work?', content: 'Routes define the path a message takes through the system, from consumer to producer.', defaultOpen: true },
|
||||
{ id: 'a3', title: 'What are processors?', content: 'Processors transform, filter, enrich, or route messages as they flow through a route.' },
|
||||
]
|
||||
|
||||
// 2. AlertDialog
|
||||
const [alertOpen, setAlertOpen] = useState(false)
|
||||
const [alertVariant, setAlertVariant] = useState<'danger' | 'warning' | 'info'>('danger')
|
||||
|
||||
// 7. CommandPalette
|
||||
const [cmdOpen, setCmdOpen] = useState(false)
|
||||
|
||||
// 8. DataTable
|
||||
const tableColumns = [
|
||||
{ key: 'name', header: 'Route', sortable: true },
|
||||
{ key: 'method', header: 'Method', sortable: true },
|
||||
{ key: 'status', header: 'Status', sortable: true },
|
||||
{ key: 'exchanges', header: 'Exchanges', sortable: true },
|
||||
]
|
||||
|
||||
// 9. DetailPanel
|
||||
const [panelOpen, setPanelOpen] = useState(false)
|
||||
|
||||
// 12. FilterBar
|
||||
const filterOptions = [
|
||||
{ label: 'Live', value: 'live', color: 'success' as const, count: 12 },
|
||||
{ label: 'Stale', value: 'stale', count: 3 },
|
||||
{ label: 'Dead', value: 'dead', color: 'error' as const, count: 1 },
|
||||
]
|
||||
const [activeFilters, setActiveFilters] = useState([{ label: 'Live', value: 'live' }])
|
||||
|
||||
// 15. Modal
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
|
||||
// 19. Tabs
|
||||
const tabItems = [
|
||||
{ label: 'Overview', value: 'overview', count: undefined },
|
||||
{ label: 'Routes', value: 'routes', count: 14 },
|
||||
{ label: 'Agents', value: 'agents', count: 6 },
|
||||
]
|
||||
const [activeTab, setActiveTab] = useState('overview')
|
||||
|
||||
// 21. TreeView
|
||||
const [selectedNode, setSelectedNode] = useState<string | undefined>('proc1')
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<section id="composites" className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Composites</h2>
|
||||
|
||||
{/* 1. Accordion */}
|
||||
<DemoCard
|
||||
id="accordion"
|
||||
title="Accordion"
|
||||
description="Collapsible panels with single or multiple open mode."
|
||||
>
|
||||
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
|
||||
<span className={styles.demoLabel}>Single mode (default)</span>
|
||||
<Accordion items={accordionItems} />
|
||||
<span className={styles.demoLabel}>Multiple mode</span>
|
||||
<Accordion
|
||||
items={[
|
||||
{ id: 'm1', title: 'Section A', content: 'Content for section A.' },
|
||||
{ id: 'm2', title: 'Section B', content: 'Content for section B.', defaultOpen: true },
|
||||
{ id: 'm3', title: 'Section C', content: 'Content for section C.', defaultOpen: true },
|
||||
]}
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 2. AlertDialog */}
|
||||
<DemoCard
|
||||
id="alertdialog"
|
||||
title="AlertDialog"
|
||||
description="Modal confirmation dialog in danger, warning, and info variants."
|
||||
>
|
||||
<div className={styles.demoAreaRow}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={() => { setAlertVariant('danger'); setAlertOpen(true) }}
|
||||
>
|
||||
Danger dialog
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => { setAlertVariant('warning'); setAlertOpen(true) }}
|
||||
>
|
||||
Warning dialog
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => { setAlertVariant('info'); setAlertOpen(true) }}
|
||||
>
|
||||
Info dialog
|
||||
</Button>
|
||||
</div>
|
||||
<AlertDialog
|
||||
open={alertOpen}
|
||||
onClose={() => setAlertOpen(false)}
|
||||
onConfirm={() => setAlertOpen(false)}
|
||||
title={alertVariant === 'danger' ? 'Delete route?' : alertVariant === 'warning' ? 'Proceed with caution?' : 'Confirm action'}
|
||||
description={
|
||||
alertVariant === 'danger'
|
||||
? 'This will permanently delete the route and all its exchange history. This action cannot be undone.'
|
||||
: alertVariant === 'warning'
|
||||
? 'This operation will restart all active processors. Running exchanges may be interrupted.'
|
||||
: 'This will update the route configuration and apply changes immediately.'
|
||||
}
|
||||
variant={alertVariant}
|
||||
confirmLabel={alertVariant === 'danger' ? 'Delete' : 'Confirm'}
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 3. AreaChart */}
|
||||
<DemoCard
|
||||
id="areachart"
|
||||
title="AreaChart"
|
||||
description="SVG area chart with hover tooltip, threshold line, and legend."
|
||||
>
|
||||
<AreaChart series={CHART_SERIES} xLabel="Time (minutes)" yLabel="Count" width={420} height={180} />
|
||||
</DemoCard>
|
||||
|
||||
{/* 4. AvatarGroup */}
|
||||
<DemoCard
|
||||
id="avatargroup"
|
||||
title="AvatarGroup"
|
||||
description="Stacked avatar group showing up to max avatars with an overflow count."
|
||||
>
|
||||
<div className={styles.demoAreaColumn}>
|
||||
<span className={styles.demoLabel}>max=3, size=sm</span>
|
||||
<AvatarGroup names={['Alice Johnson', 'Bob Smith', 'Carol White', 'David Lee', 'Eve Brown']} max={3} size="sm" />
|
||||
<span className={styles.demoLabel}>max=4, size=md</span>
|
||||
<AvatarGroup names={['Alice Johnson', 'Bob Smith', 'Carol White', 'David Lee', 'Eve Brown']} max={4} size="md" />
|
||||
<span className={styles.demoLabel}>max=2, size=lg</span>
|
||||
<AvatarGroup names={['Alice Johnson', 'Bob Smith', 'Carol White', 'David Lee', 'Eve Brown']} max={2} size="lg" />
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 5. BarChart */}
|
||||
<DemoCard
|
||||
id="barchart"
|
||||
title="BarChart"
|
||||
description="Bar chart with grouped and stacked modes, hover tooltips, and legend."
|
||||
>
|
||||
<div className={styles.demoAreaColumn}>
|
||||
<span className={styles.demoLabel}>Grouped</span>
|
||||
<BarChart series={BAR_SERIES} width={420} height={180} xLabel="Day" />
|
||||
<span className={styles.demoLabel}>Stacked</span>
|
||||
<BarChart series={BAR_SERIES} stacked width={420} height={180} xLabel="Day" />
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 6. Breadcrumb */}
|
||||
<DemoCard
|
||||
id="breadcrumb"
|
||||
title="Breadcrumb"
|
||||
description="Slash-separated navigation breadcrumb with linked and plain segments."
|
||||
>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: 'Dashboard', href: '#' },
|
||||
{ label: 'Applications', href: '#' },
|
||||
{ label: 'order-ingest' },
|
||||
]}
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 7. CommandPalette */}
|
||||
<DemoCard
|
||||
id="commandpalette"
|
||||
title="CommandPalette"
|
||||
description="Full-screen search palette with category tabs, keyboard navigation, and highlight."
|
||||
>
|
||||
<Button size="sm" variant="secondary" onClick={() => setCmdOpen(true)}>
|
||||
Open CommandPalette
|
||||
</Button>
|
||||
<CommandPalette
|
||||
open={cmdOpen}
|
||||
onClose={() => setCmdOpen(false)}
|
||||
onSelect={() => setCmdOpen(false)}
|
||||
data={COMMAND_PALETTE_DATA}
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 8. DataTable */}
|
||||
<DemoCard
|
||||
id="datatable"
|
||||
title="DataTable"
|
||||
description="Sortable, paginated table with row click, accent rows, and page size selector."
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<DataTable
|
||||
columns={tableColumns}
|
||||
data={TABLE_DATA}
|
||||
sortable
|
||||
pageSize={5}
|
||||
/>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 9. DetailPanel */}
|
||||
<DemoCard
|
||||
id="detailpanel"
|
||||
title="DetailPanel"
|
||||
description="Slide-in side panel with tabbed content and close button."
|
||||
>
|
||||
<Button size="sm" variant="secondary" onClick={() => setPanelOpen(true)}>
|
||||
Open DetailPanel
|
||||
</Button>
|
||||
<DetailPanel
|
||||
open={panelOpen}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
title="Route: order-ingest"
|
||||
tabs={[
|
||||
{ label: 'Overview', value: 'overview', content: <div style={{ padding: '12px 0', fontSize: 13 }}>Route processes ~1,243 exchanges/day with avg latency 42ms.</div> },
|
||||
{ label: 'Processors', value: 'processors', content: <div style={{ padding: '12px 0', fontSize: 13 }}>ValidateOrder → EnrichPayload → RouteToQueue</div> },
|
||||
{ label: 'Errors', value: 'errors', content: <div style={{ padding: '12px 0', fontSize: 13 }}>3 errors in last 24h. Last: timeout at EnrichPayload.</div> },
|
||||
]}
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 10. Dropdown */}
|
||||
<DemoCard
|
||||
id="dropdown"
|
||||
title="Dropdown"
|
||||
description="Click-triggered dropdown menu with icons, dividers, and disabled items."
|
||||
>
|
||||
<Dropdown
|
||||
trigger={<Button size="sm" variant="secondary">Actions ▾</Button>}
|
||||
items={[
|
||||
{ label: 'View details', icon: '👁', onClick: () => undefined },
|
||||
{ label: 'Edit route', icon: '✏', onClick: () => undefined },
|
||||
{ divider: true, label: '' },
|
||||
{ label: 'Restart', icon: '↺', onClick: () => undefined },
|
||||
{ label: 'Delete', icon: '✕', onClick: () => undefined, disabled: true },
|
||||
]}
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 11. EventFeed */}
|
||||
<DemoCard
|
||||
id="eventfeed"
|
||||
title="EventFeed"
|
||||
description="Scrollable live event log with severity filters and auto-scroll."
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<EventFeed events={FEED_EVENTS} maxItems={10} />
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 12. FilterBar */}
|
||||
<DemoCard
|
||||
id="filterbar"
|
||||
title="FilterBar"
|
||||
description="Search input combined with filter pills and active filter tag chips."
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<FilterBar
|
||||
filters={filterOptions}
|
||||
activeFilters={activeFilters}
|
||||
onFilterChange={setActiveFilters}
|
||||
searchPlaceholder="Search routes..."
|
||||
/>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 13. LineChart */}
|
||||
<DemoCard
|
||||
id="linechart"
|
||||
title="LineChart"
|
||||
description="Multi-series SVG line chart with hover crosshair, tooltip, and legend."
|
||||
>
|
||||
<LineChart series={CHART_SERIES} xLabel="Time (minutes)" yLabel="Count" width={420} height={180} />
|
||||
</DemoCard>
|
||||
|
||||
{/* 14. MenuItem */}
|
||||
<DemoCard
|
||||
id="menuitem"
|
||||
title="MenuItem"
|
||||
description="Navigation menu row with health dot, meta text, count badge, and active state."
|
||||
>
|
||||
<div className={styles.demoAreaColumn} style={{ minWidth: 200 }}>
|
||||
<MenuItem label="order-ingest" meta="POST route" count={1243} health="live" />
|
||||
<MenuItem label="payment-validate" meta="POST route" count={987} health="live" active />
|
||||
<MenuItem label="inventory-check" meta="GET route" count={432} health="stale" />
|
||||
<MenuItem label="archive-order" meta="PUT route" count={54} health="dead" indent={1} />
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 15. Modal */}
|
||||
<DemoCard
|
||||
id="modal"
|
||||
title="Modal"
|
||||
description="Portal-rendered modal dialog in sm, md, and lg sizes with backdrop dismiss."
|
||||
>
|
||||
<Button size="sm" variant="secondary" onClick={() => setModalOpen(true)}>
|
||||
Open Modal
|
||||
</Button>
|
||||
<Modal
|
||||
open={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
title="Configure Route"
|
||||
size="md"
|
||||
>
|
||||
<div style={{ fontSize: 13, lineHeight: 1.6 }}>
|
||||
<p style={{ margin: '0 0 12px' }}>Adjust the route settings below. Changes will take effect immediately after saving.</p>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)' }}>Route: order-ingest · Processor chain: 3 steps · Avg latency: 42ms</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</DemoCard>
|
||||
|
||||
{/* 16. Popover */}
|
||||
<DemoCard
|
||||
id="popover"
|
||||
title="Popover"
|
||||
description="Portal-positioned popover in all four positions with arrow indicator."
|
||||
>
|
||||
<div className={styles.demoAreaRow} style={{ paddingTop: 8, paddingBottom: 8 }}>
|
||||
<Popover
|
||||
position="top"
|
||||
trigger={<Button size="sm" variant="secondary">Top</Button>}
|
||||
content={<div style={{ padding: '8px 12px', fontSize: 12 }}>Popover on top</div>}
|
||||
/>
|
||||
<Popover
|
||||
position="bottom"
|
||||
trigger={<Button size="sm" variant="secondary">Bottom</Button>}
|
||||
content={<div style={{ padding: '8px 12px', fontSize: 12 }}>Popover on bottom</div>}
|
||||
/>
|
||||
<Popover
|
||||
position="left"
|
||||
trigger={<Button size="sm" variant="secondary">Left</Button>}
|
||||
content={<div style={{ padding: '8px 12px', fontSize: 12 }}>Popover on left</div>}
|
||||
/>
|
||||
<Popover
|
||||
position="right"
|
||||
trigger={<Button size="sm" variant="secondary">Right</Button>}
|
||||
content={<div style={{ padding: '8px 12px', fontSize: 12 }}>Popover on right</div>}
|
||||
/>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 17. ProcessorTimeline */}
|
||||
<DemoCard
|
||||
id="processortimeline"
|
||||
title="ProcessorTimeline"
|
||||
description="Horizontal Gantt-style timeline showing processor execution order, duration, and status."
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<ProcessorTimeline
|
||||
totalMs={120}
|
||||
processors={[
|
||||
{ name: 'ValidateOrder', type: 'validator', durationMs: 12, status: 'ok', startMs: 0 },
|
||||
{ name: 'EnrichPayload', type: 'enricher', durationMs: 35, status: 'slow', startMs: 12 },
|
||||
{ name: 'RouteToQueue', type: 'router', durationMs: 8, status: 'ok', startMs: 47 },
|
||||
{ name: 'AuditLog', type: 'logger', durationMs: 65, status: 'fail', startMs: 55 },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 18. ShortcutsBar */}
|
||||
<DemoCard
|
||||
id="shortcutsbar"
|
||||
title="ShortcutsBar"
|
||||
description="Row of keyboard shortcut hints, each pairing a key badge with a label."
|
||||
>
|
||||
<ShortcutsBar
|
||||
shortcuts={[
|
||||
{ keys: 'Ctrl+K', label: 'Search' },
|
||||
{ keys: '↑↓', label: 'Navigate' },
|
||||
{ keys: 'Enter', label: 'Open' },
|
||||
{ keys: 'Esc', label: 'Close' },
|
||||
{ keys: '?', label: 'Help' },
|
||||
]}
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 19. Tabs */}
|
||||
<DemoCard
|
||||
id="tabs"
|
||||
title="Tabs"
|
||||
description="Tab bar with optional count badges and active indicator."
|
||||
>
|
||||
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
|
||||
<Tabs tabs={tabItems} active={activeTab} onChange={setActiveTab} />
|
||||
<div style={{ fontSize: 13, color: 'var(--text-muted)' }}>
|
||||
Active tab: <strong>{activeTab}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 20. Toast */}
|
||||
<DemoCard
|
||||
id="toast"
|
||||
title="Toast"
|
||||
description="Stacked portal toast notifications in four variants with auto-dismiss and manual close."
|
||||
>
|
||||
<ToastDemo />
|
||||
</DemoCard>
|
||||
|
||||
{/* 21. TreeView */}
|
||||
<DemoCard
|
||||
id="treeview"
|
||||
title="TreeView"
|
||||
description="Keyboard-navigable tree showing App → Routes → Processors hierarchy."
|
||||
>
|
||||
<TreeView
|
||||
nodes={TREE_NODES}
|
||||
selectedId={selectedNode}
|
||||
onSelect={setSelectedNode}
|
||||
/>
|
||||
</DemoCard>
|
||||
</section>
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
139
src/pages/Inventory/sections/LayoutSection.module.css
Normal file
139
src/pages/Inventory/sections/LayoutSection.module.css
Normal file
@@ -0,0 +1,139 @@
|
||||
.section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.componentCard {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.componentTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.componentDesc {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.demoArea {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.demoAreaColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.demoAreaRow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demoLabel {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demoGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.demoGroupRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* AppShell diagram */
|
||||
.shellDiagram {
|
||||
width: 100%;
|
||||
background: var(--bg-canvas);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.shellDiagramTop {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-overlay);
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.shellDiagramBody {
|
||||
display: flex;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.shellDiagramSide {
|
||||
width: 140px;
|
||||
border-right: 1px solid var(--border);
|
||||
background: var(--bg-overlay);
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.shellDiagramMain {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Sidebar preview container */
|
||||
.sidebarPreview {
|
||||
width: 220px;
|
||||
height: 400px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* TopBar preview container */
|
||||
.topbarPreview {
|
||||
width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
140
src/pages/Inventory/sections/LayoutSection.tsx
Normal file
140
src/pages/Inventory/sections/LayoutSection.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import styles from './LayoutSection.module.css'
|
||||
import { Sidebar } from '../../../design-system/layout/Sidebar/Sidebar'
|
||||
import { TopBar } from '../../../design-system/layout/TopBar/TopBar'
|
||||
|
||||
// ── DemoCard helper ──────────────────────────────────────────────────────────
|
||||
|
||||
interface DemoCardProps {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function DemoCard({ id, title, description, children }: DemoCardProps) {
|
||||
return (
|
||||
<div id={id} className={styles.componentCard}>
|
||||
<h3 className={styles.componentTitle}>{title}</h3>
|
||||
<p className={styles.componentDesc}>{description}</p>
|
||||
<div className={styles.demoArea}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Sample data ───────────────────────────────────────────────────────────────
|
||||
|
||||
const SAMPLE_APPS = [
|
||||
{ id: 'app1', name: 'cameleer-prod', agentCount: 3, health: 'live' as const, execCount: 14320 },
|
||||
{ id: 'app2', name: 'cameleer-staging', agentCount: 2, health: 'stale' as const, execCount: 871 },
|
||||
{ id: 'app3', name: 'cameleer-dev', agentCount: 1, health: 'dead' as const, execCount: 42 },
|
||||
]
|
||||
|
||||
const SAMPLE_ROUTES = [
|
||||
{ id: 'r1', name: 'order-ingest', execCount: 5421 },
|
||||
{ id: 'r2', name: 'payment-validate', execCount: 3102 },
|
||||
{ id: 'r3', name: 'notify-customer', execCount: 2201 },
|
||||
]
|
||||
|
||||
const SAMPLE_AGENTS = [
|
||||
{
|
||||
id: 'ag1',
|
||||
name: 'agent-prod-1',
|
||||
service: 'camel-core',
|
||||
version: 'v3.2.1',
|
||||
tps: '42 tps',
|
||||
lastSeen: '1m ago',
|
||||
status: 'live' as const,
|
||||
},
|
||||
{
|
||||
id: 'ag2',
|
||||
name: 'agent-prod-2',
|
||||
service: 'camel-core',
|
||||
version: 'v3.2.1',
|
||||
tps: '38 tps',
|
||||
lastSeen: '2m ago',
|
||||
status: 'live' as const,
|
||||
errorRate: '0.4%',
|
||||
},
|
||||
{
|
||||
id: 'ag3',
|
||||
name: 'agent-staging-1',
|
||||
service: 'camel-core',
|
||||
version: 'v3.1.9',
|
||||
tps: '5 tps',
|
||||
lastSeen: '8m ago',
|
||||
status: 'stale' as const,
|
||||
},
|
||||
]
|
||||
|
||||
// ── LayoutSection ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function LayoutSection() {
|
||||
return (
|
||||
<section id="layout" className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Layout</h2>
|
||||
|
||||
{/* 1. AppShell */}
|
||||
<DemoCard
|
||||
id="appshell"
|
||||
title="AppShell"
|
||||
description="Full-page shell that composes Sidebar + TopBar + main content area. Cannot be nested — shown as a structural diagram."
|
||||
>
|
||||
<div className={styles.shellDiagram}>
|
||||
<div className={styles.shellDiagramTop}>
|
||||
TopBar — breadcrumb · search · env badge · shift · user avatar
|
||||
</div>
|
||||
<div className={styles.shellDiagramBody}>
|
||||
<div className={styles.shellDiagramSide}>
|
||||
<span>Sidebar</span>
|
||||
<span style={{ fontWeight: 400, fontSize: 10, marginTop: 4 }}>Logo</span>
|
||||
<span style={{ fontWeight: 400, fontSize: 10 }}>Search</span>
|
||||
<span style={{ fontWeight: 400, fontSize: 10 }}>Navigation</span>
|
||||
<span style={{ fontWeight: 400, fontSize: 10 }}>Applications</span>
|
||||
<span style={{ fontWeight: 400, fontSize: 10 }}>Routes</span>
|
||||
<span style={{ fontWeight: 400, fontSize: 10 }}>Agents</span>
|
||||
</div>
|
||||
<div className={styles.shellDiagramMain}>
|
||||
<children> — page content rendered here
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 2. Sidebar */}
|
||||
<DemoCard
|
||||
id="sidebar"
|
||||
title="Sidebar"
|
||||
description="Navigation sidebar with app/route/agent sections, search filter, health dots, and exec counts."
|
||||
>
|
||||
<div className={styles.sidebarPreview}>
|
||||
<Sidebar
|
||||
apps={SAMPLE_APPS}
|
||||
routes={SAMPLE_ROUTES}
|
||||
agents={SAMPLE_AGENTS}
|
||||
/>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 3. TopBar */}
|
||||
<DemoCard
|
||||
id="topbar"
|
||||
title="TopBar"
|
||||
description="Top navigation bar with breadcrumb, search trigger, environment badge, shift info, and user avatar."
|
||||
>
|
||||
<div className={styles.topbarPreview}>
|
||||
<TopBar
|
||||
breadcrumb={[
|
||||
{ label: 'Dashboard', href: '#' },
|
||||
{ label: 'Applications', href: '#' },
|
||||
{ label: 'order-ingest' },
|
||||
]}
|
||||
environment="production"
|
||||
shift="Morning"
|
||||
user={{ name: 'Hendrik' }}
|
||||
onSearchClick={() => undefined}
|
||||
/>
|
||||
</div>
|
||||
</DemoCard>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
74
src/pages/Inventory/sections/PrimitivesSection.module.css
Normal file
74
src/pages/Inventory/sections/PrimitivesSection.module.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.componentCard {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.componentTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.componentDesc {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.demoArea {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.demoAreaColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.demoAreaRow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demoLabel {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demoGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.demoGroupRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
591
src/pages/Inventory/sections/PrimitivesSection.tsx
Normal file
591
src/pages/Inventory/sections/PrimitivesSection.tsx
Normal file
@@ -0,0 +1,591 @@
|
||||
import { useState } from 'react'
|
||||
import styles from './PrimitivesSection.module.css'
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
CodeBlock,
|
||||
Collapsible,
|
||||
DateRangePicker,
|
||||
DateTimePicker,
|
||||
EmptyState,
|
||||
FilterPill,
|
||||
FormField,
|
||||
InfoCallout,
|
||||
Input,
|
||||
KeyboardHint,
|
||||
Label,
|
||||
MonoText,
|
||||
Pagination,
|
||||
ProgressBar,
|
||||
RadioGroup,
|
||||
RadioItem,
|
||||
SectionHeader,
|
||||
Select,
|
||||
Skeleton,
|
||||
Sparkline,
|
||||
Spinner,
|
||||
StatCard,
|
||||
StatusDot,
|
||||
Tag,
|
||||
Textarea,
|
||||
Toggle,
|
||||
Tooltip,
|
||||
} from '../../../design-system/primitives'
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
interface DemoCardProps {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function DemoCard({ id, title, description, children }: DemoCardProps) {
|
||||
return (
|
||||
<div id={id} className={styles.componentCard}>
|
||||
<h3 className={styles.componentTitle}>{title}</h3>
|
||||
<p className={styles.componentDesc}>{description}</p>
|
||||
<div className={styles.demoArea}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Sample data ───────────────────────────────────────────────────────────────
|
||||
|
||||
const SPARKLINE_DATA = [10, 25, 15, 30, 20, 35, 28]
|
||||
|
||||
const CODE_JSON = JSON.stringify(
|
||||
{ status: 'ok', version: '2.4.1', routes: 42 },
|
||||
null,
|
||||
2,
|
||||
)
|
||||
|
||||
// ── PrimitivesSection ─────────────────────────────────────────────────────────
|
||||
|
||||
export function PrimitivesSection() {
|
||||
// Alert state
|
||||
const [alertDismissed, setAlertDismissed] = useState(false)
|
||||
|
||||
// Checkbox state
|
||||
const [checked1, setChecked1] = useState(false)
|
||||
const [checked2, setChecked2] = useState(true)
|
||||
|
||||
// Toggle state
|
||||
const [toggleOn, setToggleOn] = useState(true)
|
||||
const [toggleOff, setToggleOff] = useState(false)
|
||||
|
||||
// Radio state
|
||||
const [radioV, setRadioV] = useState('option-a')
|
||||
const [radioH, setRadioH] = useState('beta')
|
||||
|
||||
// Pagination state
|
||||
const [page, setPage] = useState(5)
|
||||
|
||||
// DateTimePicker state
|
||||
const [dtValue, setDtValue] = useState<Date>(new Date('2026-03-18T09:00'))
|
||||
|
||||
// DateRangePicker state
|
||||
const [dateRange, setDateRange] = useState({
|
||||
start: new Date('2026-03-11T00:00'),
|
||||
end: new Date('2026-03-18T23:59'),
|
||||
})
|
||||
|
||||
return (
|
||||
<section id="primitives" className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Primitives</h2>
|
||||
|
||||
{/* 1. Alert */}
|
||||
<DemoCard
|
||||
id="alert"
|
||||
title="Alert"
|
||||
description="Contextual feedback messages in four variants, optionally dismissible."
|
||||
>
|
||||
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
|
||||
<Alert variant="info" title="Info">This is an informational message.</Alert>
|
||||
<Alert variant="success" title="Success">Operation completed successfully.</Alert>
|
||||
<Alert variant="warning" title="Warning">This action may have side effects.</Alert>
|
||||
{!alertDismissed && (
|
||||
<Alert
|
||||
variant="error"
|
||||
title="Error"
|
||||
dismissible
|
||||
onDismiss={() => setAlertDismissed(true)}
|
||||
>
|
||||
Something went wrong. Dismiss to clear.
|
||||
</Alert>
|
||||
)}
|
||||
{alertDismissed && (
|
||||
<Button size="sm" variant="ghost" onClick={() => setAlertDismissed(false)}>
|
||||
Reset dismissed alert
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 2. Avatar */}
|
||||
<DemoCard
|
||||
id="avatar"
|
||||
title="Avatar"
|
||||
description="Initials-based avatar with hash-derived colour, three sizes."
|
||||
>
|
||||
<Avatar name="Alice Johnson" size="sm" />
|
||||
<Avatar name="Bob Smith" size="md" />
|
||||
<Avatar name="Carol White" size="lg" />
|
||||
</DemoCard>
|
||||
|
||||
{/* 3. Badge */}
|
||||
<DemoCard
|
||||
id="badge"
|
||||
title="Badge"
|
||||
description="Compact label with semantic colours and a dashed variant."
|
||||
>
|
||||
<Badge label="primary" color="primary" />
|
||||
<Badge label="success" color="success" />
|
||||
<Badge label="warning" color="warning" />
|
||||
<Badge label="error" color="error" />
|
||||
<Badge label="running" color="running" />
|
||||
<Badge label="auto-hash" color="auto" />
|
||||
<Badge label="dashed" color="primary" variant="dashed" />
|
||||
</DemoCard>
|
||||
|
||||
{/* 4. Button */}
|
||||
<DemoCard
|
||||
id="button"
|
||||
title="Button"
|
||||
description="Primary, secondary, danger, and ghost variants across two sizes plus loading state."
|
||||
>
|
||||
<div className={styles.demoAreaColumn}>
|
||||
<div className={styles.demoAreaRow}>
|
||||
<Button variant="primary">Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="danger">Danger</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
</div>
|
||||
<div className={styles.demoAreaRow}>
|
||||
<Button variant="primary" size="sm">Small</Button>
|
||||
<Button variant="primary" size="md">Medium</Button>
|
||||
<Button variant="primary" loading>Loading</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 5. Card */}
|
||||
<DemoCard
|
||||
id="card"
|
||||
title="Card"
|
||||
description="Surface container with optional left-border accent colour."
|
||||
>
|
||||
<Card><div style={{ padding: '8px 12px', fontSize: 13 }}>Plain card</div></Card>
|
||||
<Card accent="amber"><div style={{ padding: '8px 12px', fontSize: 13 }}>Amber accent</div></Card>
|
||||
<Card accent="success"><div style={{ padding: '8px 12px', fontSize: 13 }}>Success accent</div></Card>
|
||||
<Card accent="error"><div style={{ padding: '8px 12px', fontSize: 13 }}>Error accent</div></Card>
|
||||
</DemoCard>
|
||||
|
||||
{/* 6. Checkbox */}
|
||||
<DemoCard
|
||||
id="checkbox"
|
||||
title="Checkbox"
|
||||
description="Accessible checkbox with optional label, controlled and disabled states."
|
||||
>
|
||||
<Checkbox
|
||||
label="Unchecked"
|
||||
checked={checked1}
|
||||
onChange={(e) => setChecked1(e.target.checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Checked"
|
||||
checked={checked2}
|
||||
onChange={(e) => setChecked2(e.target.checked)}
|
||||
/>
|
||||
<Checkbox label="Disabled" disabled />
|
||||
<Checkbox label="Disabled checked" disabled defaultChecked />
|
||||
</DemoCard>
|
||||
|
||||
{/* 7. CodeBlock */}
|
||||
<DemoCard
|
||||
id="codeblock"
|
||||
title="CodeBlock"
|
||||
description="Syntax-highlighted code block with line numbers and copy button."
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<CodeBlock
|
||||
content={CODE_JSON}
|
||||
language="json"
|
||||
lineNumbers
|
||||
copyable
|
||||
/>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 8. Collapsible */}
|
||||
<DemoCard
|
||||
id="collapsible"
|
||||
title="Collapsible"
|
||||
description="Animated accordion-style disclosure, open and closed by default."
|
||||
>
|
||||
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
|
||||
<Collapsible title="Collapsed by default">
|
||||
<p style={{ margin: 0, fontSize: 13 }}>Hidden content revealed on expand.</p>
|
||||
</Collapsible>
|
||||
<Collapsible title="Open by default" defaultOpen>
|
||||
<p style={{ margin: 0, fontSize: 13 }}>This content is visible from the start.</p>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 9. DateTimePicker */}
|
||||
<DemoCard
|
||||
id="datetimepicker"
|
||||
title="DateTimePicker"
|
||||
description="Native datetime-local input wrapped with label and controlled value."
|
||||
>
|
||||
<DateTimePicker
|
||||
label="Pick a date & time"
|
||||
value={dtValue}
|
||||
onChange={(d) => d && setDtValue(d)}
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 10. DateRangePicker */}
|
||||
<DemoCard
|
||||
id="daterangepicker"
|
||||
title="DateRangePicker"
|
||||
description="Preset pills combined with two DateTimePicker inputs for a start/end range."
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<DateRangePicker value={dateRange} onChange={setDateRange} />
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 11. EmptyState */}
|
||||
<DemoCard
|
||||
id="emptystate"
|
||||
title="EmptyState"
|
||||
description="Zero-data placeholder with icon, title, description, and action slot."
|
||||
>
|
||||
<EmptyState
|
||||
icon={<span style={{ fontSize: 28 }}>📭</span>}
|
||||
title="No results found"
|
||||
description="Try adjusting your filters or search query."
|
||||
action={<Button size="sm" variant="secondary">Clear filters</Button>}
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 12. FilterPill */}
|
||||
<DemoCard
|
||||
id="filterpill"
|
||||
title="FilterPill"
|
||||
description="Toggle-style pill for filter UIs, with optional dot indicator and count."
|
||||
>
|
||||
<FilterPill label="Active" active />
|
||||
<FilterPill label="Inactive" />
|
||||
<FilterPill label="With dot" dot active />
|
||||
<FilterPill label="Count" count={42} />
|
||||
</DemoCard>
|
||||
|
||||
{/* 13. FormField */}
|
||||
<DemoCard
|
||||
id="formfield"
|
||||
title="FormField"
|
||||
description="Field wrapper providing a label, hint text, and inline error messaging."
|
||||
>
|
||||
<div className={styles.demoAreaColumn} style={{ minWidth: 240 }}>
|
||||
<FormField label="Username" htmlFor="inv-username" hint="Letters and numbers only.">
|
||||
<Input id="inv-username" placeholder="e.g. alice42" />
|
||||
</FormField>
|
||||
<FormField label="Email" htmlFor="inv-email" required error="Invalid email address.">
|
||||
<Input id="inv-email" placeholder="you@example.com" />
|
||||
</FormField>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 14. InfoCallout */}
|
||||
<DemoCard
|
||||
id="infocallout"
|
||||
title="InfoCallout"
|
||||
description="Bordered callout block in amber, success, warning, and error flavours."
|
||||
>
|
||||
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
|
||||
<InfoCallout variant="amber" title="Amber">Review before publishing.</InfoCallout>
|
||||
<InfoCallout variant="success" title="Success">Deployment completed.</InfoCallout>
|
||||
<InfoCallout variant="warning" title="Warning">Rate limit approaching.</InfoCallout>
|
||||
<InfoCallout variant="error" title="Error">Build failed — check logs.</InfoCallout>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 15. Input */}
|
||||
<DemoCard
|
||||
id="input"
|
||||
title="Input"
|
||||
description="Text input with optional leading icon and placeholder."
|
||||
>
|
||||
<Input placeholder="Plain input" />
|
||||
<Input icon="🔍" placeholder="With icon" />
|
||||
</DemoCard>
|
||||
|
||||
{/* 16. KeyboardHint */}
|
||||
<DemoCard
|
||||
id="keyboardhint"
|
||||
title="KeyboardHint"
|
||||
description="Styled keyboard shortcut badge."
|
||||
>
|
||||
<KeyboardHint keys="Ctrl+K" />
|
||||
<KeyboardHint keys="⌘+P" />
|
||||
<KeyboardHint keys="Esc" />
|
||||
</DemoCard>
|
||||
|
||||
{/* 17. Label */}
|
||||
<DemoCard
|
||||
id="label"
|
||||
title="Label"
|
||||
description="Form label with optional required asterisk."
|
||||
>
|
||||
<Label>Plain label</Label>
|
||||
<Label required>Required label</Label>
|
||||
</DemoCard>
|
||||
|
||||
{/* 18. MonoText */}
|
||||
<DemoCard
|
||||
id="monotext"
|
||||
title="MonoText"
|
||||
description="Monospaced text in xs, sm, and md sizes."
|
||||
>
|
||||
<MonoText size="xs">xs: route-id-001</MonoText>
|
||||
<MonoText size="sm">sm: route-id-001</MonoText>
|
||||
<MonoText size="md">md: route-id-001</MonoText>
|
||||
</DemoCard>
|
||||
|
||||
{/* 19. Pagination */}
|
||||
<DemoCard
|
||||
id="pagination"
|
||||
title="Pagination"
|
||||
description="Page navigation with ellipsis and sibling-window algorithm."
|
||||
>
|
||||
<Pagination
|
||||
page={page}
|
||||
totalPages={20}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 20. ProgressBar */}
|
||||
<DemoCard
|
||||
id="progressbar"
|
||||
title="ProgressBar"
|
||||
description="Determinate and indeterminate bars across variants and sizes."
|
||||
>
|
||||
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
|
||||
<ProgressBar value={60} label="60% complete" />
|
||||
<ProgressBar value={60} variant="success" label="Success variant" />
|
||||
<ProgressBar value={60} variant="warning" label="Warning variant" />
|
||||
<ProgressBar value={60} variant="error" label="Error variant" />
|
||||
<ProgressBar value={60} variant="running" label="Running variant" />
|
||||
<ProgressBar indeterminate label="Indeterminate" />
|
||||
<ProgressBar value={60} size="sm" label="Small size" />
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 21. Radio */}
|
||||
<DemoCard
|
||||
id="radio"
|
||||
title="Radio"
|
||||
description="RadioGroup with vertical and horizontal orientations."
|
||||
>
|
||||
<RadioGroup name="inv-radio-v" value={radioV} onChange={setRadioV} orientation="vertical">
|
||||
<RadioItem value="option-a" label="Option A" />
|
||||
<RadioItem value="option-b" label="Option B" />
|
||||
<RadioItem value="option-c" label="Option C (disabled)" disabled />
|
||||
</RadioGroup>
|
||||
<RadioGroup name="inv-radio-h" value={radioH} onChange={setRadioH} orientation="horizontal">
|
||||
<RadioItem value="alpha" label="Alpha" />
|
||||
<RadioItem value="beta" label="Beta" />
|
||||
<RadioItem value="gamma" label="Gamma" />
|
||||
</RadioGroup>
|
||||
</DemoCard>
|
||||
|
||||
{/* 22. SectionHeader */}
|
||||
<DemoCard
|
||||
id="sectionheader"
|
||||
title="SectionHeader"
|
||||
description="Labelled horizontal divider with an optional right-side action slot."
|
||||
>
|
||||
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
|
||||
<SectionHeader>Without action</SectionHeader>
|
||||
<SectionHeader action={<Button size="sm" variant="ghost">Add item</Button>}>
|
||||
With action
|
||||
</SectionHeader>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 23. Select */}
|
||||
<DemoCard
|
||||
id="select"
|
||||
title="Select"
|
||||
description="Styled native select with custom chevron."
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'opt1', label: 'Option 1' },
|
||||
{ value: 'opt2', label: 'Option 2' },
|
||||
{ value: 'opt3', label: 'Option 3' },
|
||||
]}
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 24. Skeleton */}
|
||||
<DemoCard
|
||||
id="skeleton"
|
||||
title="Skeleton"
|
||||
description="Loading placeholder in text, circular, and rectangular variants."
|
||||
>
|
||||
<div className={styles.demoAreaColumn}>
|
||||
<Skeleton variant="text" lines={3} width={200} />
|
||||
<Skeleton variant="circular" />
|
||||
<Skeleton variant="rectangular" width={200} height={60} />
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 25. Sparkline */}
|
||||
<DemoCard
|
||||
id="sparkline"
|
||||
title="Sparkline"
|
||||
description="Lightweight SVG line chart for inline trend visualisation."
|
||||
>
|
||||
<Sparkline data={SPARKLINE_DATA} width={120} height={30} />
|
||||
<Sparkline data={SPARKLINE_DATA} color="var(--success)" width={120} height={30} />
|
||||
<Sparkline data={SPARKLINE_DATA} color="var(--error)" width={120} height={30} />
|
||||
</DemoCard>
|
||||
|
||||
{/* 26. Spinner */}
|
||||
<DemoCard
|
||||
id="spinner"
|
||||
title="Spinner"
|
||||
description="Animated loading indicator in sm, md, and lg sizes."
|
||||
>
|
||||
<Spinner size="sm" />
|
||||
<Spinner size="md" />
|
||||
<Spinner size="lg" />
|
||||
</DemoCard>
|
||||
|
||||
{/* 27. StatCard */}
|
||||
<DemoCard
|
||||
id="statcard"
|
||||
title="StatCard"
|
||||
description="Metric tile with label, value, trend indicator, and inline sparkline."
|
||||
>
|
||||
<StatCard
|
||||
label="Throughput"
|
||||
value="1,433"
|
||||
detail="msg/s"
|
||||
trend="up"
|
||||
trendValue="+12%"
|
||||
accent="amber"
|
||||
sparkline={SPARKLINE_DATA}
|
||||
/>
|
||||
<StatCard
|
||||
label="Error rate"
|
||||
value="0.4%"
|
||||
trend="down"
|
||||
trendValue="-0.1pp"
|
||||
accent="error"
|
||||
/>
|
||||
<StatCard
|
||||
label="Agents"
|
||||
value="6"
|
||||
detail="all healthy"
|
||||
trend="neutral"
|
||||
trendValue="±0"
|
||||
accent="success"
|
||||
/>
|
||||
</DemoCard>
|
||||
|
||||
{/* 28. StatusDot */}
|
||||
<DemoCard
|
||||
id="statusdot"
|
||||
title="StatusDot"
|
||||
description="Small coloured dot for all status variants, with optional pulse animation."
|
||||
>
|
||||
{(['live', 'stale', 'dead', 'success', 'warning', 'error', 'running'] as const).map(
|
||||
(v) => (
|
||||
<div key={v} className={styles.demoGroupRow}>
|
||||
<StatusDot variant={v} />
|
||||
<span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{v}</span>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
<div className={styles.demoGroupRow}>
|
||||
<StatusDot variant="live" pulse />
|
||||
<span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>live + pulse</span>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
{/* 29. Tag */}
|
||||
<DemoCard
|
||||
id="tag"
|
||||
title="Tag"
|
||||
description="Keyword label with semantic or auto hash colour, and removable variant."
|
||||
>
|
||||
<Tag label="primary" color="primary" />
|
||||
<Tag label="success" color="success" />
|
||||
<Tag label="warning" color="warning" />
|
||||
<Tag label="error" color="error" />
|
||||
<Tag label="auto-hash" color="auto" />
|
||||
<Tag label="removable" color="primary" onRemove={() => undefined} />
|
||||
</DemoCard>
|
||||
|
||||
{/* 30. Textarea */}
|
||||
<DemoCard
|
||||
id="textarea"
|
||||
title="Textarea"
|
||||
description="Multi-line text input with vertical resize by default."
|
||||
>
|
||||
<Textarea placeholder="Enter a description…" style={{ width: 280 }} />
|
||||
</DemoCard>
|
||||
|
||||
{/* 31. Toggle */}
|
||||
<DemoCard
|
||||
id="toggle"
|
||||
title="Toggle"
|
||||
description="On/off switch with optional label."
|
||||
>
|
||||
<Toggle
|
||||
label="Enabled"
|
||||
checked={toggleOn}
|
||||
onChange={(e) => setToggleOn(e.target.checked)}
|
||||
/>
|
||||
<Toggle
|
||||
label="Disabled off"
|
||||
checked={toggleOff}
|
||||
onChange={(e) => setToggleOff(e.target.checked)}
|
||||
/>
|
||||
<Toggle label="Locked on" checked disabled />
|
||||
<Toggle label="Locked off" disabled />
|
||||
</DemoCard>
|
||||
|
||||
{/* 32. Tooltip */}
|
||||
<DemoCard
|
||||
id="tooltip"
|
||||
title="Tooltip"
|
||||
description="CSS-only hover tooltip in all four positions."
|
||||
>
|
||||
<Tooltip content="Top tooltip" position="top">
|
||||
<Button size="sm" variant="secondary">Top</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Bottom tooltip" position="bottom">
|
||||
<Button size="sm" variant="secondary">Bottom</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Left tooltip" position="left">
|
||||
<Button size="sm" variant="secondary">Left</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Right tooltip" position="right">
|
||||
<Button size="sm" variant="secondary">Right</Button>
|
||||
</Tooltip>
|
||||
</DemoCard>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user