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:
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* 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')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
{children}
|
||||
<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}
|
||||
>
|
||||
{item.color && (
|
||||
<span
|
||||
className={`${styles.dot} ${active ? '' : styles.dotMuted}`}
|
||||
style={{ background: item.color }}
|
||||
/>
|
||||
)}
|
||||
{item.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -333,8 +333,8 @@
|
||||
}
|
||||
|
||||
.typeRouter {
|
||||
background: #F3EEFA;
|
||||
color: #7C3AED;
|
||||
background: var(--purple-bg);
|
||||
color: var(--purple);
|
||||
}
|
||||
|
||||
.typeProcessor {
|
||||
|
||||
@@ -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}`}>▲ +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
|
||||
|
||||
Reference in New Issue
Block a user