feat: add ButtonGroup, theme toggle, dark theme fixes, remove shift references
Some checks failed
Build & Publish / publish (push) Failing after 45s
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:
@@ -55,7 +55,8 @@
|
|||||||
- Categorical comparison → **BarChart**
|
- Categorical comparison → **BarChart**
|
||||||
- Inline trend → **Sparkline**
|
- Inline trend → **Sparkline**
|
||||||
- Event log → **EventFeed**
|
- Event log → **EventFeed**
|
||||||
- Processing pipeline → **ProcessorTimeline**
|
- Processing pipeline (Gantt view) → **ProcessorTimeline**
|
||||||
|
- Processing pipeline (flow diagram) → **RouteFlow**
|
||||||
|
|
||||||
### "I need to organize content"
|
### "I need to organize content"
|
||||||
- Collapsible sections (standalone) → **Collapsible**
|
- Collapsible sections (standalone) → **Collapsible**
|
||||||
@@ -77,11 +78,13 @@
|
|||||||
- Single user avatar → **Avatar**
|
- Single user avatar → **Avatar**
|
||||||
- Stacked user avatars → **AvatarGroup**
|
- Stacked user avatars → **AvatarGroup**
|
||||||
|
|
||||||
### "I need to group buttons"
|
### "I need to group buttons or toggle selections"
|
||||||
- Connected button strip (toggle group, segmented control) → **ButtonGroup** (horizontal or vertical)
|
- Multi-select toggle group with colored indicators → **ButtonGroup** (e.g., status filters)
|
||||||
|
- Tab switching with pill/segment style → **SegmentedTabs**
|
||||||
|
|
||||||
### "I need filtering"
|
### "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**
|
- Full filter bar with search → **FilterBar**
|
||||||
- Select multiple from a list → **MultiSelect**
|
- Select multiple from a list → **MultiSelect**
|
||||||
|
|
||||||
@@ -114,9 +117,11 @@ Below: charts (AreaChart, LineChart, BarChart)
|
|||||||
|
|
||||||
### Detail/inspector pattern
|
### Detail/inspector pattern
|
||||||
```
|
```
|
||||||
DetailPanel (right slide) with Tabs for sections
|
DetailPanel (right slide) with Tabs for sections OR children for scrollable content
|
||||||
Each tab: Cards with data, CodeBlock for payloads,
|
Tabbed: use tabs prop for multiple panels
|
||||||
ProcessorTimeline for exchange flow
|
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
|
### Feedback flow
|
||||||
@@ -156,7 +161,7 @@ URL-driven progressive filtering: /agents → /agents/:appId → /agents/:appId/
|
|||||||
| BarChart | composite | Categorical data comparison, optional stacking |
|
| BarChart | composite | Categorical data comparison, optional stacking |
|
||||||
| Breadcrumb | composite | Navigation path showing current location |
|
| Breadcrumb | composite | Navigation path showing current location |
|
||||||
| Button | primitive | Action trigger (primary, secondary, danger, ghost) |
|
| 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 |
|
| Card | primitive | Content container with optional accent border |
|
||||||
| Checkbox | primitive | Boolean input with label |
|
| Checkbox | primitive | Boolean input with label |
|
||||||
| CodeBlock | primitive | Syntax-highlighted code/JSON display |
|
| 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 |
|
| 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 |
|
| DateRangePicker | primitive | Date range selection with presets |
|
||||||
| DateTimePicker | primitive | Single date/time input |
|
| 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 |
|
| Dropdown | composite | Action menu triggered by any element |
|
||||||
| EmptyState | primitive | Placeholder for empty content areas |
|
| EmptyState | primitive | Placeholder for empty content areas |
|
||||||
| EventFeed | composite | Chronological event log with severity |
|
| 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) |
|
| MonoText | primitive | Inline monospace text (xs, sm, md) |
|
||||||
| Pagination | primitive | Page navigation controls |
|
| Pagination | primitive | Page navigation controls |
|
||||||
| Popover | composite | Click-triggered floating panel with arrow |
|
| 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 |
|
| ProgressBar | primitive | Determinate/indeterminate progress indicator |
|
||||||
| RadioGroup | primitive | Single-select option group (use with RadioItem) |
|
| RadioGroup | primitive | Single-select option group (use with RadioItem) |
|
||||||
| RadioItem | primitive | Individual radio option within RadioGroup |
|
| RadioItem | primitive | Individual radio option within RadioGroup |
|
||||||
@@ -212,8 +218,8 @@ URL-driven progressive filtering: /agents → /agents/:appId → /agents/:appId/
|
|||||||
| Component | Purpose |
|
| Component | Purpose |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
| AppShell | Page shell: sidebar + topbar + main + optional detail panel |
|
| 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) |
|
| 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, environment, user info |
|
| TopBar | Header bar with breadcrumb, search trigger, ButtonGroup status filters, time range selector, theme toggle, environment badge, user avatar |
|
||||||
|
|
||||||
## Import Paths
|
## Import Paths
|
||||||
|
|
||||||
|
|||||||
111
README.md
Normal file
111
README.md
Normal 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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type ReactNode, useEffect, useRef, useState, useCallback } from 'react'
|
import { type ReactNode, useEffect, useRef, useState, useCallback } from 'react'
|
||||||
import styles from './EventFeed.module.css'
|
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 {
|
export interface FeedEvent {
|
||||||
id: string
|
id: string
|
||||||
@@ -140,21 +141,15 @@ export function EventFeed({ events, maxItems = 200, className }: EventFeedProps)
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.filters}>
|
<div className={styles.filters}>
|
||||||
{allSeverities.map((sev) => {
|
<ButtonGroup
|
||||||
const count = events.filter((e) => e.severity === sev).length
|
items={allSeverities.map((sev): ButtonGroupItem => ({
|
||||||
return (
|
value: sev,
|
||||||
<FilterPill
|
label: SEVERITY_LABELS[sev],
|
||||||
key={sev}
|
color: SEVERITY_COLORS[sev],
|
||||||
label={SEVERITY_LABELS[sev]}
|
}))}
|
||||||
count={count}
|
value={activeFilters as Set<string>}
|
||||||
dot
|
onChange={(next) => setActiveFilters(next as Set<SeverityFilter>)}
|
||||||
dotColor={SEVERITY_COLORS[sev]}
|
|
||||||
activeColor={SEVERITY_COLORS[sev]}
|
|
||||||
active={activeFilters.has(sev)}
|
|
||||||
onClick={() => toggleFilter(sev)}
|
|
||||||
/>
|
/>
|
||||||
)
|
|
||||||
})}
|
|
||||||
{activeFilters.size > 0 && (
|
{activeFilters.size > 0 && (
|
||||||
<button
|
<button
|
||||||
className={styles.clearBtn}
|
className={styles.clearBtn}
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
|
|
||||||
.item:hover {
|
.item:hover {
|
||||||
background: var(--sidebar-hover);
|
background: var(--sidebar-hover);
|
||||||
color: #E8DFD4;
|
color: var(--sidebar-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.item.active {
|
.item.active {
|
||||||
background: var(--sidebar-active);
|
background: var(--sidebar-active);
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
border-left-color: var(--amber);
|
border-left-color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,5 +69,5 @@
|
|||||||
|
|
||||||
.item.active .count {
|
.item.active .count {
|
||||||
background: rgba(198, 130, 14, 0.2);
|
background: rgba(198, 130, 14, 0.2);
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,15 +69,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ok {
|
.ok {
|
||||||
background: rgba(61, 124, 71, 0.5);
|
background: color-mix(in srgb, var(--success) 50%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slow {
|
.slow {
|
||||||
background: rgba(194, 117, 22, 0.5);
|
background: color-mix(in srgb, var(--warning) 50%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fail {
|
.fail {
|
||||||
background: rgba(192, 57, 43, 0.5);
|
background: color-mix(in srgb, var(--error) 50%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dur {
|
.dur {
|
||||||
|
|||||||
@@ -72,8 +72,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.iconChoice {
|
.iconChoice {
|
||||||
background: #F3EEFA;
|
background: var(--purple-bg);
|
||||||
color: #7C3AED;
|
color: var(--purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconErrorHandler {
|
.iconErrorHandler {
|
||||||
@@ -81,10 +81,6 @@
|
|||||||
color: var(--error);
|
color: var(--error);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .iconChoice {
|
|
||||||
background: rgba(124, 58, 237, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Node info */
|
/* Node info */
|
||||||
.info {
|
.info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: var(--bg-inset);
|
background: var(--bg-inset);
|
||||||
padding: 3px;
|
padding: 2px;
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sliding indicator behind the active tab */
|
/* Sliding indicator behind the active tab */
|
||||||
.indicator {
|
.indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3px;
|
top: 2px;
|
||||||
bottom: 3px;
|
bottom: 2px;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
border-radius: calc(var(--radius-md) - 2px);
|
border-radius: calc(var(--radius-md) - 2px);
|
||||||
@@ -28,8 +28,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 14px;
|
padding: 3px 12px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.15s;
|
transition: color 0.15s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
line-height: 1.2;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:hover {
|
.tab:hover {
|
||||||
@@ -75,5 +75,5 @@
|
|||||||
.trailingTab {
|
.trailingTab {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 6px 10px;
|
padding: 3px 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
.logoImg {
|
.logoImg {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 24px;
|
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%);
|
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-family: var(--font-mono);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
letter-spacing: -0.3px;
|
letter-spacing: -0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
|
|
||||||
.item.active {
|
.item.active {
|
||||||
background: var(--sidebar-active);
|
background: var(--sidebar-active);
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
border-left-color: var(--amber);
|
border-left-color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item.active .navIcon {
|
.item.active .navIcon {
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
.routeArrow {
|
.routeArrow {
|
||||||
@@ -249,11 +249,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.treeSectionLabel:hover {
|
.treeSectionLabel:hover {
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
.treeSectionLabelActive {
|
.treeSectionLabelActive {
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree {
|
.tree {
|
||||||
@@ -290,13 +290,13 @@
|
|||||||
|
|
||||||
.treeRowActive {
|
.treeRowActive {
|
||||||
background: var(--sidebar-active);
|
background: var(--sidebar-active);
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
border-left-color: var(--amber);
|
border-left-color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
.treeRowActive .treeBadge {
|
.treeRowActive .treeBadge {
|
||||||
background: rgba(198, 130, 14, 0.2);
|
background: rgba(198, 130, 14, 0.2);
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chevron */
|
/* Chevron */
|
||||||
@@ -380,7 +380,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.treeStar:hover {
|
.treeStar:hover {
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Starred section ─────────────────────────────────────────────────────── */
|
/* ── Starred section ─────────────────────────────────────────────────────── */
|
||||||
@@ -500,7 +500,7 @@
|
|||||||
|
|
||||||
.bottomItemActive {
|
.bottomItemActive {
|
||||||
background: var(--sidebar-active);
|
background: var(--sidebar-active);
|
||||||
color: var(--amber-light);
|
color: var(--amber);
|
||||||
border-left-color: var(--amber);
|
border-left-color: var(--amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,27 @@
|
|||||||
flex-shrink: 0;
|
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 {
|
.env {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import styles from './TopBar.module.css'
|
import styles from './TopBar.module.css'
|
||||||
import { Breadcrumb } from '../../composites/Breadcrumb/Breadcrumb'
|
import { Breadcrumb } from '../../composites/Breadcrumb/Breadcrumb'
|
||||||
import { Avatar } from '../../primitives/Avatar/Avatar'
|
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 { TimeRangeDropdown } from '../../primitives/TimeRangeDropdown/TimeRangeDropdown'
|
||||||
import { useGlobalFilters, type ExchangeStatus } from '../../providers/GlobalFilterProvider'
|
import { useGlobalFilters } from '../../providers/GlobalFilterProvider'
|
||||||
import { useCommandPalette } from '../../providers/CommandPaletteProvider'
|
import { useCommandPalette } from '../../providers/CommandPaletteProvider'
|
||||||
|
import { useTheme } from '../../providers/ThemeProvider'
|
||||||
|
|
||||||
interface BreadcrumbItem {
|
interface BreadcrumbItem {
|
||||||
label: string
|
label: string
|
||||||
@@ -18,11 +20,11 @@ interface TopBarProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_PILLS: { status: ExchangeStatus; label: string; color: string }[] = [
|
const STATUS_ITEMS: ButtonGroupItem[] = [
|
||||||
{ status: 'completed', label: 'OK', color: 'var(--success)' },
|
{ value: 'completed', label: 'OK', color: 'var(--success)' },
|
||||||
{ status: 'warning', label: 'Warn', color: 'var(--warning)' },
|
{ value: 'warning', label: 'Warn', color: 'var(--warning)' },
|
||||||
{ status: 'failed', label: 'Error', color: 'var(--error)' },
|
{ value: 'failed', label: 'Error', color: 'var(--error)' },
|
||||||
{ status: 'running', label: 'Running', color: 'var(--running)' },
|
{ value: 'running', label: 'Running', color: 'var(--running)' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function TopBar({
|
export function TopBar({
|
||||||
@@ -33,6 +35,7 @@ export function TopBar({
|
|||||||
}: TopBarProps) {
|
}: TopBarProps) {
|
||||||
const globalFilters = useGlobalFilters()
|
const globalFilters = useGlobalFilters()
|
||||||
const commandPalette = useCommandPalette()
|
const commandPalette = useCommandPalette()
|
||||||
|
const { theme, toggleTheme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={`${styles.topbar} ${className ?? ''}`}>
|
<header className={`${styles.topbar} ${className ?? ''}`}>
|
||||||
@@ -56,20 +59,21 @@ export function TopBar({
|
|||||||
<span className={styles.kbd}>Ctrl+K</span>
|
<span className={styles.kbd}>Ctrl+K</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Status pills */}
|
{/* Status filter group */}
|
||||||
<div className={styles.filters}>
|
<ButtonGroup
|
||||||
{STATUS_PILLS.map(({ status, label, color }) => (
|
items={STATUS_ITEMS}
|
||||||
<FilterPill
|
value={globalFilters.statusFilters}
|
||||||
key={status}
|
onChange={(selected) => {
|
||||||
label={label}
|
// Sync with global filter by toggling the diff
|
||||||
dot
|
const current = globalFilters.statusFilters
|
||||||
dotColor={color}
|
for (const v of selected) {
|
||||||
activeColor={color}
|
if (!current.has(v)) globalFilters.toggleStatus(v as 'completed' | 'warning' | 'failed' | 'running')
|
||||||
active={globalFilters.statusFilters.has(status)}
|
}
|
||||||
onClick={() => globalFilters.toggleStatus(status)}
|
for (const v of current) {
|
||||||
|
if (!selected.has(v)) globalFilters.toggleStatus(v as 'completed' | 'warning' | 'failed' | 'running')
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Time range pills */}
|
{/* Time range pills */}
|
||||||
<TimeRangeDropdown
|
<TimeRangeDropdown
|
||||||
@@ -77,8 +81,17 @@ export function TopBar({
|
|||||||
onChange={globalFilters.setTimeRange}
|
onChange={globalFilters.setTimeRange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Right: env badge, user */}
|
{/* Right: theme toggle, env badge, user */}
|
||||||
<div className={styles.right}>
|
<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 && (
|
{environment && (
|
||||||
<span className={styles.env}>{environment}</span>
|
<span className={styles.env}>{environment}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,60 +1,59 @@
|
|||||||
.group {
|
.group {
|
||||||
display: inline-flex;
|
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);
|
border-radius: var(--radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Vertical */
|
.btn {
|
||||||
.vertical {
|
display: inline-flex;
|
||||||
flex-direction: column;
|
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(*) {
|
.btn:last-child {
|
||||||
border-radius: 0;
|
border-right: none;
|
||||||
margin-top: -1px;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical > :global(*:first-child) {
|
.btn:hover {
|
||||||
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
background: var(--bg-hover);
|
||||||
margin-top: 0;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical > :global(*:last-child) {
|
.btn:focus-visible {
|
||||||
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
|
outline: 2px solid var(--amber);
|
||||||
}
|
outline-offset: -2px;
|
||||||
|
|
||||||
.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) {
|
|
||||||
z-index: 1;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,60 @@
|
|||||||
import { type ReactNode } from 'react'
|
import { type ReactNode } from 'react'
|
||||||
import styles from './ButtonGroup.module.css'
|
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 {
|
interface ButtonGroupProps {
|
||||||
children: ReactNode
|
items: ButtonGroupItem[]
|
||||||
orientation?: 'horizontal' | 'vertical'
|
/** Currently selected values (multi-select) */
|
||||||
|
value: Set<string>
|
||||||
|
onChange: (value: Set<string>) => void
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ButtonGroup({
|
export function ButtonGroup({ items, value, onChange, className }: ButtonGroupProps) {
|
||||||
children,
|
function handleClick(itemValue: string) {
|
||||||
orientation = 'horizontal',
|
const next = new Set(value)
|
||||||
className,
|
if (next.has(itemValue)) {
|
||||||
}: ButtonGroupProps) {
|
next.delete(itemValue)
|
||||||
|
} else {
|
||||||
|
next.add(itemValue)
|
||||||
|
}
|
||||||
|
onChange(next)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`${styles.group} ${className ?? ''}`} role="group">
|
||||||
className={`${styles.group} ${styles[orientation]} ${className ?? ''}`}
|
{items.map((item) => {
|
||||||
role="group"
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export { Avatar } from './Avatar/Avatar'
|
|||||||
export { Badge } from './Badge/Badge'
|
export { Badge } from './Badge/Badge'
|
||||||
export { Button } from './Button/Button'
|
export { Button } from './Button/Button'
|
||||||
export { ButtonGroup } from './ButtonGroup/ButtonGroup'
|
export { ButtonGroup } from './ButtonGroup/ButtonGroup'
|
||||||
|
export type { ButtonGroupItem } from './ButtonGroup/ButtonGroup'
|
||||||
export { Card } from './Card/Card'
|
export { Card } from './Card/Card'
|
||||||
export { Checkbox } from './Checkbox/Checkbox'
|
export { Checkbox } from './Checkbox/Checkbox'
|
||||||
export { CodeBlock } from './CodeBlock/CodeBlock'
|
export { CodeBlock } from './CodeBlock/CodeBlock'
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
--sidebar-bg: #2C2520;
|
--sidebar-bg: #2C2520;
|
||||||
--sidebar-hover: #3A322C;
|
--sidebar-hover: #3A322C;
|
||||||
--sidebar-active: #4A3F38;
|
--sidebar-active: #4A3F38;
|
||||||
--sidebar-text: #BFB5A8;
|
--sidebar-text: #D8D0C6;
|
||||||
--sidebar-muted: #7A6F63;
|
--sidebar-muted: #9C9184;
|
||||||
|
|
||||||
/* Text */
|
/* Text */
|
||||||
--text-primary: #1A1612;
|
--text-primary: #1A1612;
|
||||||
@@ -58,6 +58,10 @@
|
|||||||
--shadow-lg: 0 4px 16px rgba(44, 37, 32, 0.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);
|
--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 palette */
|
||||||
--chart-1: #C6820E;
|
--chart-1: #C6820E;
|
||||||
--chart-2: #3D7C47;
|
--chart-2: #3D7C47;
|
||||||
@@ -80,7 +84,7 @@
|
|||||||
--sidebar-bg: #141210;
|
--sidebar-bg: #141210;
|
||||||
--sidebar-hover: #1E1B17;
|
--sidebar-hover: #1E1B17;
|
||||||
--sidebar-active: #28241E;
|
--sidebar-active: #28241E;
|
||||||
--sidebar-text: #A89E92;
|
--sidebar-text: #CCC4B8;
|
||||||
--sidebar-muted: #6A6058;
|
--sidebar-muted: #6A6058;
|
||||||
|
|
||||||
--text-primary: #E8E0D6;
|
--text-primary: #E8E0D6;
|
||||||
@@ -109,6 +113,9 @@
|
|||||||
--running-bg: #1A2628;
|
--running-bg: #1A2628;
|
||||||
--running-border: #243A3E;
|
--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-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
|
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.4);
|
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const DEFAULT_PRESETS: Preset[] = [
|
|||||||
{ label: 'Last 1h', value: 'last-1h' },
|
{ label: 'Last 1h', value: 'last-1h' },
|
||||||
{ label: 'Last 6h', value: 'last-6h' },
|
{ label: 'Last 6h', value: 'last-6h' },
|
||||||
{ label: 'Today', value: 'today' },
|
{ label: 'Today', value: 'today' },
|
||||||
{ label: 'This shift', value: 'shift' },
|
|
||||||
{ label: 'Last 24h', value: 'last-24h' },
|
{ label: 'Last 24h', value: 'last-24h' },
|
||||||
{ label: 'Last 7d', value: 'last-7d' },
|
{ label: 'Last 7d', value: 'last-7d' },
|
||||||
{ label: 'Custom', value: 'custom' },
|
{ label: 'Custom', value: 'custom' },
|
||||||
@@ -23,7 +22,6 @@ export const PRESET_SHORT_LABELS: Record<string, string> = {
|
|||||||
'last-3h': '3h',
|
'last-3h': '3h',
|
||||||
'last-6h': '6h',
|
'last-6h': '6h',
|
||||||
'today': 'Today',
|
'today': 'Today',
|
||||||
'shift': 'Shift',
|
|
||||||
'last-24h': '24h',
|
'last-24h': '24h',
|
||||||
'last-7d': '7d',
|
'last-7d': '7d',
|
||||||
'custom': 'Custom',
|
'custom': 'Custom',
|
||||||
@@ -45,10 +43,6 @@ export function computePresetRange(preset: string): DateRange {
|
|||||||
start.setHours(0, 0, 0, 0)
|
start.setHours(0, 0, 0, 0)
|
||||||
return { start, end }
|
return { start, end }
|
||||||
}
|
}
|
||||||
case 'shift': {
|
|
||||||
// "This shift" = last 8 hours
|
|
||||||
return { start: new Date(now.getTime() - 8 * 60 * 60 * 1000), end }
|
|
||||||
}
|
|
||||||
case 'last-24h':
|
case 'last-24h':
|
||||||
return { start: new Date(now.getTime() - 24 * 60 * 60 * 1000), end }
|
return { start: new Date(now.getTime() - 24 * 60 * 60 * 1000), end }
|
||||||
case 'last-7d':
|
case 'last-7d':
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface MetricSeries {
|
|||||||
data: TimeSeriesPoint[]
|
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(
|
function generateTimeSeries(
|
||||||
baseValue: number,
|
baseValue: number,
|
||||||
variance: number,
|
variance: number,
|
||||||
@@ -44,12 +44,12 @@ function generateTimeSeries(
|
|||||||
// KPI stat cards data
|
// KPI stat cards data
|
||||||
export const kpiMetrics: KpiMetric[] = [
|
export const kpiMetrics: KpiMetric[] = [
|
||||||
{
|
{
|
||||||
label: 'Exchanges (shift)',
|
label: 'Exchanges',
|
||||||
value: '3,241',
|
value: '3,241',
|
||||||
trend: 'up',
|
trend: 'up',
|
||||||
trendValue: '+12%',
|
trendValue: '+12%',
|
||||||
trendSentiment: 'good',
|
trendSentiment: 'good',
|
||||||
detail: '97.1% success since 06:00',
|
detail: '97.1% success rate',
|
||||||
accent: 'amber',
|
accent: 'amber',
|
||||||
sparkline: [28, 32, 29, 35, 38, 41, 37, 44, 42, 47, 45, 51, 48, 52],
|
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],
|
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,
|
value: 38,
|
||||||
trend: 'up',
|
trend: 'up',
|
||||||
trendValue: '+5',
|
trendValue: '+5',
|
||||||
trendSentiment: 'bad',
|
trendSentiment: 'bad',
|
||||||
detail: '23 overnight · 15 since 06:00',
|
detail: '38 errors in selected period',
|
||||||
accent: 'error',
|
accent: 'error',
|
||||||
sparkline: [1, 2, 1, 3, 2, 4, 3, 5, 4, 6, 5, 7, 6, 8],
|
sparkline: [1, 2, 1, 3, 2, 4, 3, 5, 4, 6, 5, 7, 6, 8],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const NAV_SECTIONS = [
|
|||||||
{ label: 'Avatar', href: '#avatar' },
|
{ label: 'Avatar', href: '#avatar' },
|
||||||
{ label: 'Badge', href: '#badge' },
|
{ label: 'Badge', href: '#badge' },
|
||||||
{ label: 'Button', href: '#button' },
|
{ label: 'Button', href: '#button' },
|
||||||
|
{ label: 'ButtonGroup', href: '#buttongroup' },
|
||||||
{ label: 'Card', href: '#card' },
|
{ label: 'Card', href: '#card' },
|
||||||
{ label: 'Checkbox', href: '#checkbox' },
|
{ label: 'Checkbox', href: '#checkbox' },
|
||||||
{ label: 'CodeBlock', href: '#codeblock' },
|
{ label: 'CodeBlock', href: '#codeblock' },
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function LayoutSection() {
|
|||||||
>
|
>
|
||||||
<div className={styles.shellDiagram}>
|
<div className={styles.shellDiagram}>
|
||||||
<div className={styles.shellDiagramTop}>
|
<div className={styles.shellDiagramTop}>
|
||||||
TopBar — breadcrumb · search · env badge · shift · user avatar
|
TopBar — breadcrumb · search · filters · time range · env badge · user avatar
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.shellDiagramBody}>
|
<div className={styles.shellDiagramBody}>
|
||||||
<div className={styles.shellDiagramSide}>
|
<div className={styles.shellDiagramSide}>
|
||||||
@@ -110,7 +110,7 @@ export function LayoutSection() {
|
|||||||
<DemoCard
|
<DemoCard
|
||||||
id="topbar"
|
id="topbar"
|
||||||
title="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}>
|
<div className={styles.topbarPreview}>
|
||||||
<TopBar
|
<TopBar
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
@@ -72,6 +73,9 @@ export function PrimitivesSection() {
|
|||||||
// Alert state
|
// Alert state
|
||||||
const [alertDismissed, setAlertDismissed] = useState(false)
|
const [alertDismissed, setAlertDismissed] = useState(false)
|
||||||
|
|
||||||
|
// ButtonGroup state
|
||||||
|
const [bgSelection, setBgSelection] = useState<Set<string>>(new Set(['warn']))
|
||||||
|
|
||||||
// Checkbox state
|
// Checkbox state
|
||||||
const [checked1, setChecked1] = useState(false)
|
const [checked1, setChecked1] = useState(false)
|
||||||
const [checked2, setChecked2] = useState(true)
|
const [checked2, setChecked2] = useState(true)
|
||||||
@@ -178,6 +182,24 @@ export function PrimitivesSection() {
|
|||||||
</div>
|
</div>
|
||||||
</DemoCard>
|
</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 */}
|
{/* 5. Card */}
|
||||||
<DemoCard
|
<DemoCard
|
||||||
id="card"
|
id="card"
|
||||||
|
|||||||
@@ -333,8 +333,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.typeRouter {
|
.typeRouter {
|
||||||
background: #F3EEFA;
|
background: var(--purple-bg);
|
||||||
color: #7C3AED;
|
color: var(--purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeProcessor {
|
.typeProcessor {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function KpiHeader({ scopedMetrics }: { scopedMetrics: RouteMetricRow[] }) {
|
|||||||
<div className={styles.kpiLabel}>Total Throughput</div>
|
<div className={styles.kpiLabel}>Total Throughput</div>
|
||||||
<div className={styles.kpiValueRow}>
|
<div className={styles.kpiValueRow}>
|
||||||
<span className={`${styles.kpiValue} ${styles.kpiValueAmber}`}>{totalExchanges.toLocaleString()}</span>
|
<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}`}>▲ +8%</span>
|
<span className={`${styles.kpiTrend} ${styles.trendUpGood}`}>▲ +8%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.kpiDetail}>
|
<div className={styles.kpiDetail}>
|
||||||
@@ -483,7 +483,7 @@ export function Routes() {
|
|||||||
<span className={styles.tableTitle}>Processor Performance</span>
|
<span className={styles.tableTitle}>Processor Performance</span>
|
||||||
<div className={styles.tableRight}>
|
<div className={styles.tableRight}>
|
||||||
<span className={styles.tableMeta}>{processorMetrics.length} processors</span>
|
<span className={styles.tableMeta}>{processorMetrics.length} processors</span>
|
||||||
<Badge label="SHIFT" color="primary" />
|
<Badge label="LIVE" color="success" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|||||||
Reference in New Issue
Block a user