feat: add ButtonGroup, theme toggle, dark theme fixes, remove shift references
Some checks failed
Build & Publish / publish (push) Failing after 45s

- Add ButtonGroup primitive: multi-select toggle with colored dot indicators
- Replace FilterPill status filters with ButtonGroup in TopBar and EventFeed
- Add light/dark mode toggle to TopBar (moon/sun icon)
- Fix dark theme: add --purple/--purple-bg tokens, replace all hardcoded
  #F3EEFA/#7C3AED with tokens, fix --amber-light text contrast in sidebar,
  brighten --sidebar-text/--sidebar-muted tokens, use color-mix for
  ProcessorTimeline bar fills
- Remove all "shift" references (presets, labels, badges)
- Shrink SegmentedTabs height to match search bar and ButtonGroup
- Update COMPONENT_GUIDE.md with new components and updated descriptions
- Add ButtonGroup demo to Inventory
- Add README.md with setup instructions and navigation guide

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-19 16:33:34 +01:00
parent 5bd965e59a
commit 91788737b0
21 changed files with 361 additions and 158 deletions

View File

@@ -55,7 +55,8 @@
- Categorical comparison → **BarChart**
- Inline trend → **Sparkline**
- Event log → **EventFeed**
- Processing pipeline → **ProcessorTimeline**
- Processing pipeline (Gantt view)**ProcessorTimeline**
- Processing pipeline (flow diagram) → **RouteFlow**
### "I need to organize content"
- Collapsible sections (standalone) → **Collapsible**
@@ -77,11 +78,13 @@
- Single user avatar → **Avatar**
- Stacked user avatars → **AvatarGroup**
### "I need to group buttons"
- Connected button strip (toggle group, segmented control)**ButtonGroup** (horizontal or vertical)
### "I need to group buttons or toggle selections"
- Multi-select toggle group with colored indicators**ButtonGroup** (e.g., status filters)
- Tab switching with pill/segment style → **SegmentedTabs**
### "I need filtering"
- Filter pill/chip → **FilterPill**
- Multi-select status/category filter → **ButtonGroup** (toggle items on/off)
- Filter pill/chip → **FilterPill** (individual toggleable pills)
- Full filter bar with search → **FilterBar**
- Select multiple from a list → **MultiSelect**
@@ -114,9 +117,11 @@ Below: charts (AreaChart, LineChart, BarChart)
### Detail/inspector pattern
```
DetailPanel (right slide) with Tabs for sections
Each tab: Cards with data, CodeBlock for payloads,
ProcessorTimeline for exchange flow
DetailPanel (right slide) with Tabs for sections OR children for scrollable content
Tabbed: use tabs prop for multiple panels
Scrollable: use children for stacked sections (overview, errors, route flow, timeline)
Each section: Cards with data, CodeBlock for payloads,
ProcessorTimeline or RouteFlow for exchange flow
```
### Feedback flow
@@ -156,7 +161,7 @@ URL-driven progressive filtering: /agents → /agents/:appId → /agents/:appId/
| BarChart | composite | Categorical data comparison, optional stacking |
| Breadcrumb | composite | Navigation path showing current location |
| Button | primitive | Action trigger (primary, secondary, danger, ghost) |
| ButtonGroup | primitive | Groups buttons into a connected strip with shared borders (horizontal/vertical) |
| ButtonGroup | primitive | Multi-select toggle group with optional colored dot indicators. Props: items (value, label, color?), value (Set), onChange |
| Card | primitive | Content container with optional accent border |
| Checkbox | primitive | Boolean input with label |
| CodeBlock | primitive | Syntax-highlighted code/JSON display |
@@ -166,7 +171,7 @@ URL-driven progressive filtering: /agents → /agents/:appId → /agents/:appId/
| DataTable | composite | Sortable, paginated data table with row actions. Use `flush` prop when embedded inside a container that provides its own border/radius |
| DateRangePicker | primitive | Date range selection with presets |
| DateTimePicker | primitive | Single date/time input |
| DetailPanel | composite | Slide-in side panel with tabs |
| DetailPanel | composite | Slide-in side panel with tabs or children for scrollable content |
| Dropdown | composite | Action menu triggered by any element |
| EmptyState | primitive | Placeholder for empty content areas |
| EventFeed | composite | Chronological event log with severity |
@@ -186,7 +191,8 @@ URL-driven progressive filtering: /agents → /agents/:appId → /agents/:appId/
| MonoText | primitive | Inline monospace text (xs, sm, md) |
| Pagination | primitive | Page navigation controls |
| Popover | composite | Click-triggered floating panel with arrow |
| ProcessorTimeline | composite | Pipeline exchange visualization |
| ProcessorTimeline | composite | Gantt-style pipeline visualization with selectable rows. Props: processors, totalMs, onProcessorClick?, selectedIndex? |
| RouteFlow | composite | Vertical processor node flow diagram with status coloring, connectors, and click support. Props: nodes, onNodeClick?, selectedIndex? |
| ProgressBar | primitive | Determinate/indeterminate progress indicator |
| RadioGroup | primitive | Single-select option group (use with RadioItem) |
| RadioItem | primitive | Individual radio option within RadioGroup |
@@ -212,8 +218,8 @@ URL-driven progressive filtering: /agents → /agents/:appId → /agents/:appId/
| Component | Purpose |
|-----------|---------|
| AppShell | Page shell: sidebar + topbar + main + optional detail panel |
| Sidebar | Hierarchical navigation with Applications/Agents trees, starring, search filter, bottom links. Props: `apps: SidebarApp[]` (hierarchical — apps contain routes and agents) |
| TopBar | Header bar with breadcrumb, environment, user info |
| Sidebar | Hierarchical navigation with Applications/Agents/Routes trees, starring, search filter, bottom links. Props: `apps: SidebarApp[]` (hierarchical — apps contain routes and agents) |
| TopBar | Header bar with breadcrumb, search trigger, ButtonGroup status filters, time range selector, theme toggle, environment badge, user avatar |
## Import Paths

111
README.md Normal file
View File

@@ -0,0 +1,111 @@
# Cameleer3 Design System
A component library and interactive UI prototype for the Cameleer3 monitoring platform. This project contains both the reusable design system (primitives, composites, layout components) and a fully functional mock application demonstrating all pages and interactions.
## Prerequisites
- **Node.js** 20 or later (tested with 22.x) — [https://nodejs.org](https://nodejs.org)
- **Git** — [https://git-scm.com](https://git-scm.com)
No other tools, accounts, or access to external registries are required. All dependencies are published on the public npm registry.
## Getting Started
```bash
# 1. Clone the repository
git clone <repo-url> cameleer-design-system
cd cameleer-design-system
# 2. Install dependencies
npm install
# 3. Start the development server
npm run dev
```
The dev server will start at **http://localhost:5173** (Vite will print the exact URL).
## Available Scripts
| Command | Description |
|---------|-------------|
| `npm run dev` | Start the development server with hot reload |
| `npm run build` | Type-check and build the production bundle |
| `npm run preview` | Serve the production build locally |
| `npm test` | Run the test suite (Vitest, 332 tests) |
| `npm run lint` | Run ESLint |
## Navigating the Prototype
Once the dev server is running, open **http://localhost:5173** in your browser. The application includes these sections:
### Sidebar Navigation
- **Applications** — Exchange monitoring dashboard
- `/apps` — All exchanges across all applications
- `/apps/:appId` — Filtered by application
- `/apps/:appId/:routeId` — Filtered by application and route
- Click the ↗ icon on any row to open the full **Exchange Detail** page
- **Agents** — JVM agent health monitoring
- `/agents` — Overview of all agent instances grouped by application
- `/agents/:appId` — Single application's agents
- `/agents/:appId/:instanceId` — Instance detail with CPU, memory, threads, GC charts
- **Routes** — Per-route performance metrics
- `/routes` — Aggregated KPI cards, route performance table, charts
- `/routes/:appId` — Filtered by application
- `/routes/:appId/:routeId` — Per-processor statistics and route flow diagram
- **Admin** — User management (RBAC), OIDC configuration, audit log
- `/admin/rbac` — Users, groups, roles with inline editing
- `/admin/oidc` — OIDC provider configuration form
- `/admin/audit` — Searchable audit log table
- **Inventory** — Component showcase
- `/inventory` — Interactive demos of every design system component
### Top Bar Controls
- **Search** (Ctrl+K) — Full-text search across applications, routes, agents, exchanges
- **Status Filters** — Toggle OK / Warn / Error / Running to filter exchanges
- **Time Range** — Preset time ranges (1h, 3h, 6h, Today, 24h, 7d) or custom date/time picker
- **Theme Toggle** (☾/☀) — Switch between light and dark mode
### Key Interactions
- **Exchange slide-in panel** — Click any row in the exchanges table to open a detail panel on the right showing overview, errors, route flow, and processor timeline
- **Exchange detail page** — Click the ↗ icon or "Open full details" link for the full inspector with Message IN/OUT panels and correlation chain
- **Processor selection** — On the exchange detail page, click any processor in the timeline or flow diagram to see its message snapshots
- **Starring** — Hover any item in the sidebar trees and click the star to pin it to the Starred section
- **Dark mode** — Click the moon/sun icon in the top bar to toggle themes
## Project Structure
```
src/
design-system/
primitives/ # Atomic components (Button, Input, Badge, StatusDot, ...)
composites/ # Composed components (DataTable, Modal, EventFeed, RouteFlow, ...)
layout/ # Page-level layout (AppShell, Sidebar, TopBar)
providers/ # React context providers (Theme, CommandPalette, GlobalFilter)
tokens.css # Design tokens (colors, spacing, typography, shadows)
utils/ # Shared utilities (hashColor, timePresets)
pages/ # Application pages (Dashboard, Routes, AgentHealth, Admin, ...)
mocks/ # Static mock data (exchanges, routes, agents, metrics, sidebar)
```
## Tech Stack
- **React 19** + **TypeScript**
- **Vite** for development and bundling
- **CSS Modules** for styling (all colors via design tokens)
- **Vitest** + **React Testing Library** for tests
- No runtime CSS-in-JS, no Tailwind, no external component libraries
## Notes
- All data is static mock data — no backend or API calls required
- The prototype is fully self-contained and works offline after `npm install`
- Light and dark themes are supported throughout
- Fonts (DM Sans, JetBrains Mono) are loaded from Google Fonts and require an internet connection on first load

View File

@@ -1,6 +1,7 @@
import { type ReactNode, useEffect, useRef, useState, useCallback } from 'react'
import styles from './EventFeed.module.css'
import { FilterPill } from '../../primitives/FilterPill/FilterPill'
import { ButtonGroup } from '../../primitives/ButtonGroup/ButtonGroup'
import type { ButtonGroupItem } from '../../primitives/ButtonGroup/ButtonGroup'
export interface FeedEvent {
id: string
@@ -140,21 +141,15 @@ export function EventFeed({ events, maxItems = 200, className }: EventFeedProps)
)}
</div>
<div className={styles.filters}>
{allSeverities.map((sev) => {
const count = events.filter((e) => e.severity === sev).length
return (
<FilterPill
key={sev}
label={SEVERITY_LABELS[sev]}
count={count}
dot
dotColor={SEVERITY_COLORS[sev]}
activeColor={SEVERITY_COLORS[sev]}
active={activeFilters.has(sev)}
onClick={() => toggleFilter(sev)}
<ButtonGroup
items={allSeverities.map((sev): ButtonGroupItem => ({
value: sev,
label: SEVERITY_LABELS[sev],
color: SEVERITY_COLORS[sev],
}))}
value={activeFilters as Set<string>}
onChange={(next) => setActiveFilters(next as Set<SeverityFilter>)}
/>
)
})}
{activeFilters.size > 0 && (
<button
className={styles.clearBtn}

View File

@@ -16,12 +16,12 @@
.item:hover {
background: var(--sidebar-hover);
color: #E8DFD4;
color: var(--sidebar-text);
}
.item.active {
background: var(--sidebar-active);
color: var(--amber-light);
color: var(--amber);
border-left-color: var(--amber);
}
@@ -69,5 +69,5 @@
.item.active .count {
background: rgba(198, 130, 14, 0.2);
color: var(--amber-light);
color: var(--amber);
}

View File

@@ -69,15 +69,15 @@
}
.ok {
background: rgba(61, 124, 71, 0.5);
background: color-mix(in srgb, var(--success) 50%, transparent);
}
.slow {
background: rgba(194, 117, 22, 0.5);
background: color-mix(in srgb, var(--warning) 50%, transparent);
}
.fail {
background: rgba(192, 57, 43, 0.5);
background: color-mix(in srgb, var(--error) 50%, transparent);
}
.dur {

View File

@@ -72,8 +72,8 @@
}
.iconChoice {
background: #F3EEFA;
color: #7C3AED;
background: var(--purple-bg);
color: var(--purple);
}
.iconErrorHandler {
@@ -81,10 +81,6 @@
color: var(--error);
}
[data-theme="dark"] .iconChoice {
background: rgba(124, 58, 237, 0.15);
}
/* Node info */
.info {
flex: 1;

View File

@@ -4,15 +4,15 @@
align-items: center;
border-radius: var(--radius-md);
background: var(--bg-inset);
padding: 3px;
padding: 2px;
gap: 1px;
}
/* Sliding indicator behind the active tab */
.indicator {
position: absolute;
top: 3px;
bottom: 3px;
top: 2px;
bottom: 2px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: calc(var(--radius-md) - 2px);
@@ -28,8 +28,8 @@
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 14px;
font-size: 13px;
padding: 3px 12px;
font-size: 12px;
font-weight: 500;
font-family: var(--font-body);
color: var(--text-muted);
@@ -39,7 +39,7 @@
cursor: pointer;
transition: color 0.15s;
white-space: nowrap;
line-height: 1.2;
line-height: 1.5;
}
.tab:hover {
@@ -75,5 +75,5 @@
.trailingTab {
cursor: default;
gap: 4px;
padding: 6px 10px;
padding: 3px 10px;
}

View File

@@ -20,7 +20,7 @@
.logoImg {
width: 28px;
height: 24px;
color: var(--amber-light);
color: var(--amber);
filter: brightness(0) saturate(100%) invert(76%) sepia(30%) saturate(400%) hue-rotate(5deg) brightness(95%);
}
@@ -28,7 +28,7 @@
font-family: var(--font-mono);
font-weight: 600;
font-size: 15px;
color: var(--amber-light);
color: var(--amber);
letter-spacing: -0.3px;
}
@@ -151,7 +151,7 @@
.item.active {
background: var(--sidebar-active);
color: var(--amber-light);
color: var(--amber);
border-left-color: var(--amber);
}
@@ -164,7 +164,7 @@
}
.item.active .navIcon {
color: var(--amber-light);
color: var(--amber);
}
.routeArrow {
@@ -249,11 +249,11 @@
}
.treeSectionLabel:hover {
color: var(--amber-light);
color: var(--amber);
}
.treeSectionLabelActive {
color: var(--amber-light);
color: var(--amber);
}
.tree {
@@ -290,13 +290,13 @@
.treeRowActive {
background: var(--sidebar-active);
color: var(--amber-light);
color: var(--amber);
border-left-color: var(--amber);
}
.treeRowActive .treeBadge {
background: rgba(198, 130, 14, 0.2);
color: var(--amber-light);
color: var(--amber);
}
/* Chevron */
@@ -380,7 +380,7 @@
}
.treeStar:hover {
color: var(--amber-light);
color: var(--amber);
}
/* ── Starred section ─────────────────────────────────────────────────────── */
@@ -500,7 +500,7 @@
.bottomItemActive {
background: var(--sidebar-active);
color: var(--amber-light);
color: var(--amber);
border-left-color: var(--amber);
}

View File

@@ -81,6 +81,27 @@
flex-shrink: 0;
}
.themeToggle {
background: none;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-muted);
cursor: pointer;
font-size: 16px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.15s, border-color 0.15s;
line-height: 1;
}
.themeToggle:hover {
color: var(--amber);
border-color: var(--amber);
}
.env {
font-family: var(--font-mono);
font-size: 10px;

View File

@@ -1,10 +1,12 @@
import styles from './TopBar.module.css'
import { Breadcrumb } from '../../composites/Breadcrumb/Breadcrumb'
import { Avatar } from '../../primitives/Avatar/Avatar'
import { FilterPill } from '../../primitives/FilterPill/FilterPill'
import { ButtonGroup } from '../../primitives/ButtonGroup/ButtonGroup'
import type { ButtonGroupItem } from '../../primitives/ButtonGroup/ButtonGroup'
import { TimeRangeDropdown } from '../../primitives/TimeRangeDropdown/TimeRangeDropdown'
import { useGlobalFilters, type ExchangeStatus } from '../../providers/GlobalFilterProvider'
import { useGlobalFilters } from '../../providers/GlobalFilterProvider'
import { useCommandPalette } from '../../providers/CommandPaletteProvider'
import { useTheme } from '../../providers/ThemeProvider'
interface BreadcrumbItem {
label: string
@@ -18,11 +20,11 @@ interface TopBarProps {
className?: string
}
const STATUS_PILLS: { status: ExchangeStatus; label: string; color: string }[] = [
{ status: 'completed', label: 'OK', color: 'var(--success)' },
{ status: 'warning', label: 'Warn', color: 'var(--warning)' },
{ status: 'failed', label: 'Error', color: 'var(--error)' },
{ status: 'running', label: 'Running', color: 'var(--running)' },
const STATUS_ITEMS: ButtonGroupItem[] = [
{ value: 'completed', label: 'OK', color: 'var(--success)' },
{ value: 'warning', label: 'Warn', color: 'var(--warning)' },
{ value: 'failed', label: 'Error', color: 'var(--error)' },
{ value: 'running', label: 'Running', color: 'var(--running)' },
]
export function TopBar({
@@ -33,6 +35,7 @@ export function TopBar({
}: TopBarProps) {
const globalFilters = useGlobalFilters()
const commandPalette = useCommandPalette()
const { theme, toggleTheme } = useTheme()
return (
<header className={`${styles.topbar} ${className ?? ''}`}>
@@ -56,20 +59,21 @@ export function TopBar({
<span className={styles.kbd}>Ctrl+K</span>
</button>
{/* Status pills */}
<div className={styles.filters}>
{STATUS_PILLS.map(({ status, label, color }) => (
<FilterPill
key={status}
label={label}
dot
dotColor={color}
activeColor={color}
active={globalFilters.statusFilters.has(status)}
onClick={() => globalFilters.toggleStatus(status)}
{/* Status filter group */}
<ButtonGroup
items={STATUS_ITEMS}
value={globalFilters.statusFilters}
onChange={(selected) => {
// Sync with global filter by toggling the diff
const current = globalFilters.statusFilters
for (const v of selected) {
if (!current.has(v)) globalFilters.toggleStatus(v as 'completed' | 'warning' | 'failed' | 'running')
}
for (const v of current) {
if (!selected.has(v)) globalFilters.toggleStatus(v as 'completed' | 'warning' | 'failed' | 'running')
}
}}
/>
))}
</div>
{/* Time range pills */}
<TimeRangeDropdown
@@ -77,8 +81,17 @@ export function TopBar({
onChange={globalFilters.setTimeRange}
/>
{/* Right: env badge, user */}
{/* Right: theme toggle, env badge, user */}
<div className={styles.right}>
<button
className={styles.themeToggle}
onClick={toggleTheme}
type="button"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{theme === 'light' ? '\u263E' : '\u2600'}
</button>
{environment && (
<span className={styles.env}>{environment}</span>
)}

View File

@@ -1,60 +1,59 @@
.group {
display: inline-flex;
isolation: isolate;
}
/* Horizontal (default) */
.horizontal {
flex-direction: row;
}
.horizontal > :global(*) {
border-radius: 0;
margin-left: -1px;
position: relative;
}
.horizontal > :global(*:first-child) {
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
margin-left: 0;
}
.horizontal > :global(*:last-child) {
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.horizontal > :global(*:only-child) {
border-radius: var(--radius-sm);
overflow: hidden;
border: 1px solid var(--border);
background: var(--bg-surface);
}
/* Vertical */
.vertical {
flex-direction: column;
.btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
border: none;
border-right: 1px solid var(--border);
background: transparent;
color: var(--text-muted);
font-family: var(--font-body);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background 0.12s, color 0.12s;
white-space: nowrap;
line-height: 1.5;
}
.vertical > :global(*) {
border-radius: 0;
margin-top: -1px;
position: relative;
.btn:last-child {
border-right: none;
}
.vertical > :global(*:first-child) {
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
margin-top: 0;
.btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.vertical > :global(*:last-child) {
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}
.vertical > :global(*:only-child) {
border-radius: var(--radius-sm);
}
/* Active/hovered items sit above siblings so their borders win */
.group > :global(*:hover),
.group > :global(*:focus-visible),
.group > :global(*[data-active="true"]),
.group > :global(*.active) {
.btn:focus-visible {
outline: 2px solid var(--amber);
outline-offset: -2px;
z-index: 1;
}
/* Active state — default (no color override) */
.active {
background: var(--amber-bg);
color: var(--amber);
font-weight: 600;
}
/* Dot indicator */
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.dotMuted {
opacity: 0.4;
}

View File

@@ -1,23 +1,60 @@
import { type ReactNode } from 'react'
import styles from './ButtonGroup.module.css'
export interface ButtonGroupItem {
value: string
label: ReactNode
/** Optional color for dot indicator and active tint */
color?: string
}
interface ButtonGroupProps {
children: ReactNode
orientation?: 'horizontal' | 'vertical'
items: ButtonGroupItem[]
/** Currently selected values (multi-select) */
value: Set<string>
onChange: (value: Set<string>) => void
className?: string
}
export function ButtonGroup({
children,
orientation = 'horizontal',
className,
}: ButtonGroupProps) {
export function ButtonGroup({ items, value, onChange, className }: ButtonGroupProps) {
function handleClick(itemValue: string) {
const next = new Set(value)
if (next.has(itemValue)) {
next.delete(itemValue)
} else {
next.add(itemValue)
}
onChange(next)
}
return (
<div
className={`${styles.group} ${styles[orientation]} ${className ?? ''}`}
role="group"
<div className={`${styles.group} ${className ?? ''}`} role="group">
{items.map((item) => {
const active = value.has(item.value)
return (
<button
key={item.value}
type="button"
className={`${styles.btn} ${active ? styles.active : ''}`}
style={active && item.color ? {
borderColor: item.color,
color: item.color,
background: `color-mix(in srgb, ${item.color} 10%, transparent)`,
} : undefined}
onClick={() => handleClick(item.value)}
aria-pressed={active}
>
{children}
{item.color && (
<span
className={`${styles.dot} ${active ? '' : styles.dotMuted}`}
style={{ background: item.color }}
/>
)}
{item.label}
</button>
)
})}
</div>
)
}

View File

@@ -3,6 +3,7 @@ export { Avatar } from './Avatar/Avatar'
export { Badge } from './Badge/Badge'
export { Button } from './Button/Button'
export { ButtonGroup } from './ButtonGroup/ButtonGroup'
export type { ButtonGroupItem } from './ButtonGroup/ButtonGroup'
export { Card } from './Card/Card'
export { Checkbox } from './Checkbox/Checkbox'
export { CodeBlock } from './CodeBlock/CodeBlock'

View File

@@ -10,8 +10,8 @@
--sidebar-bg: #2C2520;
--sidebar-hover: #3A322C;
--sidebar-active: #4A3F38;
--sidebar-text: #BFB5A8;
--sidebar-muted: #7A6F63;
--sidebar-text: #D8D0C6;
--sidebar-muted: #9C9184;
/* Text */
--text-primary: #1A1612;
@@ -58,6 +58,10 @@
--shadow-lg: 0 4px 16px rgba(44, 37, 32, 0.10);
--shadow-card: 0 1px 3px rgba(44, 37, 32, 0.04), 0 0 0 1px rgba(44, 37, 32, 0.04);
/* Accent: purple (for choice/router elements) */
--purple: #7C3AED;
--purple-bg: #F3EEFA;
/* Chart palette */
--chart-1: #C6820E;
--chart-2: #3D7C47;
@@ -80,7 +84,7 @@
--sidebar-bg: #141210;
--sidebar-hover: #1E1B17;
--sidebar-active: #28241E;
--sidebar-text: #A89E92;
--sidebar-text: #CCC4B8;
--sidebar-muted: #6A6058;
--text-primary: #E8E0D6;
@@ -109,6 +113,9 @@
--running-bg: #1A2628;
--running-border: #243A3E;
--purple: #A78BFA;
--purple-bg: rgba(124, 58, 237, 0.15);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.4);

View File

@@ -12,7 +12,6 @@ export const DEFAULT_PRESETS: Preset[] = [
{ label: 'Last 1h', value: 'last-1h' },
{ label: 'Last 6h', value: 'last-6h' },
{ label: 'Today', value: 'today' },
{ label: 'This shift', value: 'shift' },
{ label: 'Last 24h', value: 'last-24h' },
{ label: 'Last 7d', value: 'last-7d' },
{ label: 'Custom', value: 'custom' },
@@ -23,7 +22,6 @@ export const PRESET_SHORT_LABELS: Record<string, string> = {
'last-3h': '3h',
'last-6h': '6h',
'today': 'Today',
'shift': 'Shift',
'last-24h': '24h',
'last-7d': '7d',
'custom': 'Custom',
@@ -45,10 +43,6 @@ export function computePresetRange(preset: string): DateRange {
start.setHours(0, 0, 0, 0)
return { start, end }
}
case 'shift': {
// "This shift" = last 8 hours
return { start: new Date(now.getTime() - 8 * 60 * 60 * 1000), end }
}
case 'last-24h':
return { start: new Date(now.getTime() - 24 * 60 * 60 * 1000), end }
case 'last-7d':

View File

@@ -20,7 +20,7 @@ export interface MetricSeries {
data: TimeSeriesPoint[]
}
// Generate a realistic time series for the past shift (06:00 - now ~09:15)
// Generate a realistic time series for the past hours (06:00 - now ~09:15)
function generateTimeSeries(
baseValue: number,
variance: number,
@@ -44,12 +44,12 @@ function generateTimeSeries(
// KPI stat cards data
export const kpiMetrics: KpiMetric[] = [
{
label: 'Exchanges (shift)',
label: 'Exchanges',
value: '3,241',
trend: 'up',
trendValue: '+12%',
trendSentiment: 'good',
detail: '97.1% success since 06:00',
detail: '97.1% success rate',
accent: 'amber',
sparkline: [28, 32, 29, 35, 38, 41, 37, 44, 42, 47, 45, 51, 48, 52],
},
@@ -64,12 +64,12 @@ export const kpiMetrics: KpiMetric[] = [
sparkline: [98.2, 97.9, 98.1, 97.8, 97.5, 97.6, 97.4, 97.2, 97.3, 97.1, 97.0, 97.1, 97.2, 97.1],
},
{
label: 'Errors (shift)',
label: 'Errors',
value: 38,
trend: 'up',
trendValue: '+5',
trendSentiment: 'bad',
detail: '23 overnight · 15 since 06:00',
detail: '38 errors in selected period',
accent: 'error',
sparkline: [1, 2, 1, 3, 2, 4, 3, 5, 4, 6, 5, 7, 6, 8],
},

View File

@@ -13,6 +13,7 @@ const NAV_SECTIONS = [
{ label: 'Avatar', href: '#avatar' },
{ label: 'Badge', href: '#badge' },
{ label: 'Button', href: '#button' },
{ label: 'ButtonGroup', href: '#buttongroup' },
{ label: 'Card', href: '#card' },
{ label: 'Checkbox', href: '#checkbox' },
{ label: 'CodeBlock', href: '#codeblock' },

View File

@@ -76,7 +76,7 @@ export function LayoutSection() {
>
<div className={styles.shellDiagram}>
<div className={styles.shellDiagramTop}>
TopBar breadcrumb · search · env badge · shift · user avatar
TopBar breadcrumb · search · filters · time range · env badge · user avatar
</div>
<div className={styles.shellDiagramBody}>
<div className={styles.shellDiagramSide}>
@@ -110,7 +110,7 @@ export function LayoutSection() {
<DemoCard
id="topbar"
title="TopBar"
description="Top navigation bar with breadcrumb, search trigger, environment badge, shift info, and user avatar."
description="Top navigation bar with breadcrumb, search trigger, status filters, time range, environment badge, and user avatar."
>
<div className={styles.topbarPreview}>
<TopBar

View File

@@ -5,6 +5,7 @@ import {
Avatar,
Badge,
Button,
ButtonGroup,
Card,
Checkbox,
CodeBlock,
@@ -72,6 +73,9 @@ export function PrimitivesSection() {
// Alert state
const [alertDismissed, setAlertDismissed] = useState(false)
// ButtonGroup state
const [bgSelection, setBgSelection] = useState<Set<string>>(new Set(['warn']))
// Checkbox state
const [checked1, setChecked1] = useState(false)
const [checked2, setChecked2] = useState(true)
@@ -178,6 +182,24 @@ export function PrimitivesSection() {
</div>
</DemoCard>
{/* 4b. ButtonGroup */}
<DemoCard
id="buttongroup"
title="ButtonGroup"
description="Multi-select toggle group with optional colored dot indicators. Used for status filters."
>
<ButtonGroup
items={[
{ value: 'ok', label: 'OK', color: 'var(--success)' },
{ value: 'warn', label: 'Warn', color: 'var(--warning)' },
{ value: 'error', label: 'Error', color: 'var(--error)' },
{ value: 'running', label: 'Running', color: 'var(--running)' },
]}
value={bgSelection}
onChange={setBgSelection}
/>
</DemoCard>
{/* 5. Card */}
<DemoCard
id="card"

View File

@@ -333,8 +333,8 @@
}
.typeRouter {
background: #F3EEFA;
color: #7C3AED;
background: var(--purple-bg);
color: var(--purple);
}
.typeProcessor {

View File

@@ -59,7 +59,7 @@ function KpiHeader({ scopedMetrics }: { scopedMetrics: RouteMetricRow[] }) {
<div className={styles.kpiLabel}>Total Throughput</div>
<div className={styles.kpiValueRow}>
<span className={`${styles.kpiValue} ${styles.kpiValueAmber}`}>{totalExchanges.toLocaleString()}</span>
<span className={styles.kpiUnit}>msg/shift</span>
<span className={styles.kpiUnit}>exchanges</span>
<span className={`${styles.kpiTrend} ${styles.trendUpGood}`}>&#9650; +8%</span>
</div>
<div className={styles.kpiDetail}>
@@ -483,7 +483,7 @@ export function Routes() {
<span className={styles.tableTitle}>Processor Performance</span>
<div className={styles.tableRight}>
<span className={styles.tableMeta}>{processorMetrics.length} processors</span>
<Badge label="SHIFT" color="primary" />
<Badge label="LIVE" color="success" />
</div>
</div>
<DataTable