Plan 1: KpiStrip + StatusText + Card title (metrics) Plan 2: SplitPane + EntityList (admin) Plan 3: LogViewer + AgentHealth DataTable refactor (observability) Plan 4: COMPONENT_GUIDE.md + Inventory updates (documentation) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
704 lines
21 KiB
Markdown
704 lines
21 KiB
Markdown
# Metrics Components Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Add StatusText primitive, Card title prop, and KpiStrip composite to eliminate ~320 lines of duplicated KPI layout code across Dashboard, Routes, and AgentHealth pages.
|
|
|
|
**Architecture:** StatusText is a tiny inline span primitive with semantic color variants. Card gets an optional title prop for a header row. KpiStrip is a new composite that renders a horizontal row of metric cards with labels, values, trends, subtitles, and sparklines.
|
|
|
|
**Tech Stack:** React, TypeScript, CSS Modules, Vitest, React Testing Library
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-03-24-mock-deviations-design.md` (Sections 1, 5, 6)
|
|
|
|
---
|
|
|
|
## File Map
|
|
|
|
| Action | File | Task |
|
|
|--------|------|------|
|
|
| CREATE | `src/design-system/primitives/StatusText/StatusText.tsx` | 1 |
|
|
| CREATE | `src/design-system/primitives/StatusText/StatusText.module.css` | 1 |
|
|
| CREATE | `src/design-system/primitives/StatusText/StatusText.test.tsx` | 1 |
|
|
| MODIFY | `src/design-system/primitives/index.ts` | 1 |
|
|
| MODIFY | `src/design-system/primitives/Card/Card.tsx` | 2 |
|
|
| MODIFY | `src/design-system/primitives/Card/Card.module.css` | 2 |
|
|
| CREATE | `src/design-system/primitives/Card/Card.test.tsx` | 2 |
|
|
| CREATE | `src/design-system/composites/KpiStrip/KpiStrip.tsx` | 3 |
|
|
| CREATE | `src/design-system/composites/KpiStrip/KpiStrip.module.css` | 3 |
|
|
| CREATE | `src/design-system/composites/KpiStrip/KpiStrip.test.tsx` | 3 |
|
|
| MODIFY | `src/design-system/composites/index.ts` | 3 |
|
|
|
|
---
|
|
|
|
## Task 1: StatusText Primitive
|
|
|
|
**Files:**
|
|
- CREATE `src/design-system/primitives/StatusText/StatusText.tsx`
|
|
- CREATE `src/design-system/primitives/StatusText/StatusText.module.css`
|
|
- CREATE `src/design-system/primitives/StatusText/StatusText.test.tsx`
|
|
- MODIFY `src/design-system/primitives/index.ts`
|
|
|
|
### Step 1.1 — Write test (RED)
|
|
|
|
- [ ] Create `src/design-system/primitives/StatusText/StatusText.test.tsx`:
|
|
|
|
```tsx
|
|
import { describe, it, expect } from 'vitest'
|
|
import { render, screen } from '@testing-library/react'
|
|
import { StatusText } from './StatusText'
|
|
|
|
describe('StatusText', () => {
|
|
it('renders children text', () => {
|
|
render(<StatusText variant="success">OK</StatusText>)
|
|
expect(screen.getByText('OK')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders as a span element', () => {
|
|
render(<StatusText variant="success">OK</StatusText>)
|
|
expect(screen.getByText('OK').tagName).toBe('SPAN')
|
|
})
|
|
|
|
it('applies variant class', () => {
|
|
render(<StatusText variant="error">BREACH</StatusText>)
|
|
expect(screen.getByText('BREACH')).toHaveClass('error')
|
|
})
|
|
|
|
it('applies bold class when bold=true', () => {
|
|
render(<StatusText variant="warning" bold>HIGH</StatusText>)
|
|
expect(screen.getByText('HIGH')).toHaveClass('bold')
|
|
})
|
|
|
|
it('does not apply bold class by default', () => {
|
|
render(<StatusText variant="muted">idle</StatusText>)
|
|
expect(screen.getByText('idle')).not.toHaveClass('bold')
|
|
})
|
|
|
|
it('accepts custom className', () => {
|
|
render(<StatusText variant="running" className="custom">active</StatusText>)
|
|
expect(screen.getByText('active')).toHaveClass('custom')
|
|
})
|
|
|
|
it('renders all variant classes correctly', () => {
|
|
const { rerender } = render(<StatusText variant="success">text</StatusText>)
|
|
expect(screen.getByText('text')).toHaveClass('success')
|
|
|
|
rerender(<StatusText variant="warning">text</StatusText>)
|
|
expect(screen.getByText('text')).toHaveClass('warning')
|
|
|
|
rerender(<StatusText variant="error">text</StatusText>)
|
|
expect(screen.getByText('text')).toHaveClass('error')
|
|
|
|
rerender(<StatusText variant="running">text</StatusText>)
|
|
expect(screen.getByText('text')).toHaveClass('running')
|
|
|
|
rerender(<StatusText variant="muted">text</StatusText>)
|
|
expect(screen.getByText('text')).toHaveClass('muted')
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] Run test — expect FAIL (module not found):
|
|
|
|
```bash
|
|
npx vitest run src/design-system/primitives/StatusText/StatusText.test.tsx
|
|
```
|
|
|
|
### Step 1.2 — Implement (GREEN)
|
|
|
|
- [ ] Create `src/design-system/primitives/StatusText/StatusText.module.css`:
|
|
|
|
```css
|
|
.statusText {
|
|
/* Inherits font-size from parent */
|
|
}
|
|
|
|
.success { color: var(--success); }
|
|
.warning { color: var(--warning); }
|
|
.error { color: var(--error); }
|
|
.running { color: var(--running); }
|
|
.muted { color: var(--text-muted); }
|
|
|
|
.bold { font-weight: 600; }
|
|
```
|
|
|
|
- [ ] Create `src/design-system/primitives/StatusText/StatusText.tsx`:
|
|
|
|
```tsx
|
|
import styles from './StatusText.module.css'
|
|
import type { ReactNode } from 'react'
|
|
|
|
interface StatusTextProps {
|
|
variant: 'success' | 'warning' | 'error' | 'running' | 'muted'
|
|
bold?: boolean
|
|
children: ReactNode
|
|
className?: string
|
|
}
|
|
|
|
export function StatusText({ variant, bold = false, children, className }: StatusTextProps) {
|
|
const classes = [
|
|
styles.statusText,
|
|
styles[variant],
|
|
bold ? styles.bold : '',
|
|
className ?? '',
|
|
].filter(Boolean).join(' ')
|
|
|
|
return <span className={classes}>{children}</span>
|
|
}
|
|
```
|
|
|
|
- [ ] Run test — expect PASS:
|
|
|
|
```bash
|
|
npx vitest run src/design-system/primitives/StatusText/StatusText.test.tsx
|
|
```
|
|
|
|
### Step 1.3 — Barrel export
|
|
|
|
- [ ] Add to `src/design-system/primitives/index.ts` (alphabetical, after `StatusDot`):
|
|
|
|
```ts
|
|
export { StatusText } from './StatusText/StatusText'
|
|
```
|
|
|
|
### Step 1.4 — Commit
|
|
|
|
```bash
|
|
git add src/design-system/primitives/StatusText/ src/design-system/primitives/index.ts
|
|
git commit -m "feat: add StatusText primitive with semantic color variants"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Card Title Extension
|
|
|
|
**Files:**
|
|
- MODIFY `src/design-system/primitives/Card/Card.tsx`
|
|
- MODIFY `src/design-system/primitives/Card/Card.module.css`
|
|
- CREATE `src/design-system/primitives/Card/Card.test.tsx`
|
|
|
|
### Step 2.1 — Write test (RED)
|
|
|
|
- [ ] Create `src/design-system/primitives/Card/Card.test.tsx`:
|
|
|
|
```tsx
|
|
import { describe, it, expect } from 'vitest'
|
|
import { render, screen } from '@testing-library/react'
|
|
import { Card } from './Card'
|
|
|
|
describe('Card', () => {
|
|
it('renders children', () => {
|
|
render(<Card>Card content</Card>)
|
|
expect(screen.getByText('Card content')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders title when provided', () => {
|
|
render(<Card title="Section Title">content</Card>)
|
|
expect(screen.getByText('Section Title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('does not render title header when title is omitted', () => {
|
|
const { container } = render(<Card>content</Card>)
|
|
expect(container.querySelector('.titleHeader')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('wraps children in body div when title is provided', () => {
|
|
render(<Card title="Header">body text</Card>)
|
|
const body = screen.getByText('body text').closest('div')
|
|
expect(body).toHaveClass('body')
|
|
})
|
|
|
|
it('renders with accent and title together', () => {
|
|
const { container } = render(
|
|
<Card accent="success" title="Status">
|
|
details
|
|
</Card>
|
|
)
|
|
expect(container.firstChild).toHaveClass('accent-success')
|
|
expect(screen.getByText('Status')).toBeInTheDocument()
|
|
expect(screen.getByText('details')).toBeInTheDocument()
|
|
})
|
|
|
|
it('accepts className prop', () => {
|
|
const { container } = render(<Card className="custom">content</Card>)
|
|
expect(container.firstChild).toHaveClass('custom')
|
|
})
|
|
|
|
it('renders children directly when no title (no wrapper div)', () => {
|
|
const { container } = render(<Card><span data-testid="direct">hi</span></Card>)
|
|
expect(screen.getByTestId('direct')).toBeInTheDocument()
|
|
// Should not have a body wrapper when there is no title
|
|
expect(container.querySelector('.body')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] Run test — expect FAIL (title prop not supported yet, body class missing):
|
|
|
|
```bash
|
|
npx vitest run src/design-system/primitives/Card/Card.test.tsx
|
|
```
|
|
|
|
### Step 2.2 — Implement (GREEN)
|
|
|
|
- [ ] Add to `src/design-system/primitives/Card/Card.module.css` (append after existing rules):
|
|
|
|
```css
|
|
.titleHeader {
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
}
|
|
|
|
.titleText {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
font-family: var(--font-mono);
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
letter-spacing: 0.5px;
|
|
margin: 0;
|
|
}
|
|
|
|
.body {
|
|
padding: 16px;
|
|
}
|
|
```
|
|
|
|
- [ ] Replace `src/design-system/primitives/Card/Card.tsx` with:
|
|
|
|
```tsx
|
|
import styles from './Card.module.css'
|
|
import type { ReactNode } from 'react'
|
|
|
|
interface CardProps {
|
|
children: ReactNode
|
|
accent?: 'amber' | 'success' | 'warning' | 'error' | 'running' | 'none'
|
|
title?: string
|
|
className?: string
|
|
}
|
|
|
|
export function Card({ children, accent = 'none', title, className }: CardProps) {
|
|
const classes = [
|
|
styles.card,
|
|
accent !== 'none' ? styles[`accent-${accent}`] : '',
|
|
className ?? '',
|
|
].filter(Boolean).join(' ')
|
|
|
|
return (
|
|
<div className={classes}>
|
|
{title && (
|
|
<div className={styles.titleHeader}>
|
|
<h3 className={styles.titleText}>{title}</h3>
|
|
</div>
|
|
)}
|
|
{title ? <div className={styles.body}>{children}</div> : children}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
- [ ] Run test — expect PASS:
|
|
|
|
```bash
|
|
npx vitest run src/design-system/primitives/Card/Card.test.tsx
|
|
```
|
|
|
|
### Step 2.3 — Commit
|
|
|
|
```bash
|
|
git add src/design-system/primitives/Card/
|
|
git commit -m "feat: add optional title prop to Card primitive"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: KpiStrip Composite
|
|
|
|
**Files:**
|
|
- CREATE `src/design-system/composites/KpiStrip/KpiStrip.tsx`
|
|
- CREATE `src/design-system/composites/KpiStrip/KpiStrip.module.css`
|
|
- CREATE `src/design-system/composites/KpiStrip/KpiStrip.test.tsx`
|
|
- MODIFY `src/design-system/composites/index.ts`
|
|
|
|
### Step 3.1 — Write test (RED)
|
|
|
|
- [ ] Create `src/design-system/composites/KpiStrip/KpiStrip.test.tsx`:
|
|
|
|
```tsx
|
|
import { describe, it, expect } from 'vitest'
|
|
import { render, screen } from '@testing-library/react'
|
|
import { KpiStrip } from './KpiStrip'
|
|
|
|
const sampleItems = [
|
|
{
|
|
label: 'Total Throughput',
|
|
value: '12,847',
|
|
trend: { label: '\u25B2 +8%', variant: 'success' as const },
|
|
subtitle: '35.7 msg/s',
|
|
sparkline: [44, 46, 45, 47, 48, 46, 47],
|
|
borderColor: 'var(--amber)',
|
|
},
|
|
{
|
|
label: 'Error Rate',
|
|
value: '0.42%',
|
|
trend: { label: '\u25BC -0.1%', variant: 'success' as const },
|
|
subtitle: '54 errors / 12,847 total',
|
|
},
|
|
{
|
|
label: 'Active Routes',
|
|
value: 14,
|
|
},
|
|
]
|
|
|
|
describe('KpiStrip', () => {
|
|
it('renders all items', () => {
|
|
render(<KpiStrip items={sampleItems} />)
|
|
expect(screen.getByText('Total Throughput')).toBeInTheDocument()
|
|
expect(screen.getByText('Error Rate')).toBeInTheDocument()
|
|
expect(screen.getByText('Active Routes')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders labels and values', () => {
|
|
render(<KpiStrip items={sampleItems} />)
|
|
expect(screen.getByText('12,847')).toBeInTheDocument()
|
|
expect(screen.getByText('0.42%')).toBeInTheDocument()
|
|
expect(screen.getByText('14')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders trend with correct text', () => {
|
|
render(<KpiStrip items={sampleItems} />)
|
|
expect(screen.getByText('\u25B2 +8%')).toBeInTheDocument()
|
|
expect(screen.getByText('\u25BC -0.1%')).toBeInTheDocument()
|
|
})
|
|
|
|
it('applies variant class to trend', () => {
|
|
render(<KpiStrip items={sampleItems} />)
|
|
const trend = screen.getByText('\u25B2 +8%')
|
|
expect(trend).toHaveClass('trendSuccess')
|
|
})
|
|
|
|
it('hides trend when omitted', () => {
|
|
render(<KpiStrip items={[{ label: 'Routes', value: 14 }]} />)
|
|
// Should only have label and value, no trend element
|
|
const card = screen.getByText('Routes').closest('[class*="kpiCard"]')
|
|
expect(card?.querySelector('[class*="trend"]')).toBeNull()
|
|
})
|
|
|
|
it('renders subtitle', () => {
|
|
render(<KpiStrip items={sampleItems} />)
|
|
expect(screen.getByText('35.7 msg/s')).toBeInTheDocument()
|
|
expect(screen.getByText('54 errors / 12,847 total')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders sparkline when data provided', () => {
|
|
const { container } = render(<KpiStrip items={sampleItems} />)
|
|
// Sparkline renders an SVG with aria-hidden
|
|
const svgs = container.querySelectorAll('svg[aria-hidden="true"]')
|
|
expect(svgs.length).toBe(1) // Only first item has sparkline
|
|
})
|
|
|
|
it('accepts className prop', () => {
|
|
const { container } = render(<KpiStrip items={sampleItems} className="custom" />)
|
|
expect(container.firstChild).toHaveClass('custom')
|
|
})
|
|
|
|
it('handles empty items array', () => {
|
|
const { container } = render(<KpiStrip items={[]} />)
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
// No cards rendered
|
|
expect(container.querySelectorAll('[class*="kpiCard"]').length).toBe(0)
|
|
})
|
|
|
|
it('uses default border color when borderColor is omitted', () => {
|
|
const { container } = render(
|
|
<KpiStrip items={[{ label: 'Test', value: 100 }]} />
|
|
)
|
|
const card = container.querySelector('[class*="kpiCard"]')
|
|
expect(card).toBeInTheDocument()
|
|
// The default borderColor is applied via inline style
|
|
expect(card).toHaveStyle({ '--kpi-border-color': 'var(--amber)' })
|
|
})
|
|
|
|
it('applies custom borderColor', () => {
|
|
const { container } = render(
|
|
<KpiStrip items={[{ label: 'Errors', value: 5, borderColor: 'var(--error)' }]} />
|
|
)
|
|
const card = container.querySelector('[class*="kpiCard"]')
|
|
expect(card).toHaveStyle({ '--kpi-border-color': 'var(--error)' })
|
|
})
|
|
|
|
it('renders trend with muted variant by default', () => {
|
|
render(
|
|
<KpiStrip items={[{ label: 'Test', value: 1, trend: { label: '~ stable' } }]} />
|
|
)
|
|
const trend = screen.getByText('~ stable')
|
|
expect(trend).toHaveClass('trendMuted')
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] Run test — expect FAIL (module not found):
|
|
|
|
```bash
|
|
npx vitest run src/design-system/composites/KpiStrip/KpiStrip.test.tsx
|
|
```
|
|
|
|
### Step 3.2 — Implement (GREEN)
|
|
|
|
- [ ] Create `src/design-system/composites/KpiStrip/KpiStrip.module.css`:
|
|
|
|
```css
|
|
/* KpiStrip — horizontal row of metric cards */
|
|
.kpiStrip {
|
|
display: grid;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
/* ── Individual card ─────────────────────────────────────────────── */
|
|
.kpiCard {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: var(--radius-lg);
|
|
padding: 16px 18px 12px;
|
|
box-shadow: var(--shadow-card);
|
|
position: relative;
|
|
overflow: hidden;
|
|
transition: box-shadow 0.15s;
|
|
}
|
|
|
|
.kpiCard:hover {
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
/* Top gradient border — color driven by CSS custom property */
|
|
.kpiCard::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, var(--kpi-border-color), transparent);
|
|
}
|
|
|
|
/* ── Label ───────────────────────────────────────────────────────── */
|
|
.label {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.6px;
|
|
color: var(--text-muted);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
/* ── Value row ───────────────────────────────────────────────────── */
|
|
.valueRow {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 6px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.value {
|
|
font-family: var(--font-mono);
|
|
font-size: 26px;
|
|
font-weight: 600;
|
|
line-height: 1.2;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* ── Trend ────────────────────────────────────────────────────────── */
|
|
.trend {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.trendSuccess { color: var(--success); }
|
|
.trendWarning { color: var(--warning); }
|
|
.trendError { color: var(--error); }
|
|
.trendMuted { color: var(--text-muted); }
|
|
|
|
/* ── Subtitle ─────────────────────────────────────────────────────── */
|
|
.subtitle {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* ── Sparkline ────────────────────────────────────────────────────── */
|
|
.sparkline {
|
|
margin-top: 8px;
|
|
height: 32px;
|
|
}
|
|
```
|
|
|
|
- [ ] Create `src/design-system/composites/KpiStrip/KpiStrip.tsx`:
|
|
|
|
```tsx
|
|
import styles from './KpiStrip.module.css'
|
|
import { Sparkline } from '../../primitives/Sparkline/Sparkline'
|
|
import type { CSSProperties, ReactNode } from 'react'
|
|
|
|
export interface KpiItem {
|
|
label: string
|
|
value: string | number
|
|
trend?: { label: string; variant?: 'success' | 'warning' | 'error' | 'muted' }
|
|
subtitle?: string
|
|
sparkline?: number[]
|
|
borderColor?: string
|
|
}
|
|
|
|
export interface KpiStripProps {
|
|
items: KpiItem[]
|
|
className?: string
|
|
}
|
|
|
|
const trendClassMap: Record<string, string> = {
|
|
success: styles.trendSuccess,
|
|
warning: styles.trendWarning,
|
|
error: styles.trendError,
|
|
muted: styles.trendMuted,
|
|
}
|
|
|
|
export function KpiStrip({ items, className }: KpiStripProps) {
|
|
const stripClasses = [styles.kpiStrip, className ?? ''].filter(Boolean).join(' ')
|
|
const gridStyle: CSSProperties = {
|
|
gridTemplateColumns: items.length > 0 ? `repeat(${items.length}, 1fr)` : undefined,
|
|
}
|
|
|
|
return (
|
|
<div className={stripClasses} style={gridStyle}>
|
|
{items.map((item) => {
|
|
const borderColor = item.borderColor ?? 'var(--amber)'
|
|
const cardStyle: CSSProperties & Record<string, string> = {
|
|
'--kpi-border-color': borderColor,
|
|
}
|
|
const trendVariant = item.trend?.variant ?? 'muted'
|
|
const trendClass = trendClassMap[trendVariant] ?? styles.trendMuted
|
|
|
|
return (
|
|
<div key={item.label} className={styles.kpiCard} style={cardStyle}>
|
|
<div className={styles.label}>{item.label}</div>
|
|
<div className={styles.valueRow}>
|
|
<span className={styles.value}>{item.value}</span>
|
|
{item.trend && (
|
|
<span className={`${styles.trend} ${trendClass}`}>
|
|
{item.trend.label}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{item.subtitle && (
|
|
<div className={styles.subtitle}>{item.subtitle}</div>
|
|
)}
|
|
{item.sparkline && item.sparkline.length >= 2 && (
|
|
<div className={styles.sparkline}>
|
|
<Sparkline
|
|
data={item.sparkline}
|
|
color={borderColor}
|
|
width={200}
|
|
height={32}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
- [ ] Run test — expect PASS:
|
|
|
|
```bash
|
|
npx vitest run src/design-system/composites/KpiStrip/KpiStrip.test.tsx
|
|
```
|
|
|
|
### Step 3.3 — Barrel export
|
|
|
|
- [ ] Add to `src/design-system/composites/index.ts` (alphabetical, after `GroupCard`):
|
|
|
|
```ts
|
|
export { KpiStrip } from './KpiStrip/KpiStrip'
|
|
export type { KpiItem, KpiStripProps } from './KpiStrip/KpiStrip'
|
|
```
|
|
|
|
### Step 3.4 — Commit
|
|
|
|
```bash
|
|
git add src/design-system/composites/KpiStrip/ src/design-system/composites/index.ts
|
|
git commit -m "feat: add KpiStrip composite for reusable metric card rows"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Barrel Exports Verification & Full Test Run
|
|
|
|
**Files:**
|
|
- VERIFY `src/design-system/primitives/index.ts` (modified in Task 1)
|
|
- VERIFY `src/design-system/composites/index.ts` (modified in Task 3)
|
|
|
|
### Step 4.1 — Verify barrel exports
|
|
|
|
- [ ] Confirm `src/design-system/primitives/index.ts` contains:
|
|
|
|
```ts
|
|
export { StatusText } from './StatusText/StatusText'
|
|
```
|
|
|
|
- [ ] Confirm `src/design-system/composites/index.ts` contains:
|
|
|
|
```ts
|
|
export { KpiStrip } from './KpiStrip/KpiStrip'
|
|
export type { KpiItem, KpiStripProps } from './KpiStrip/KpiStrip'
|
|
```
|
|
|
|
### Step 4.2 — Run full test suite
|
|
|
|
- [ ] Run all tests to confirm nothing is broken:
|
|
|
|
```bash
|
|
npx vitest run
|
|
```
|
|
|
|
- [ ] Verify zero failures. If any test fails, fix and re-run before proceeding.
|
|
|
|
### Step 4.3 — Final commit (if barrel-only changes remain)
|
|
|
|
If the barrel export changes were not already committed in their respective tasks:
|
|
|
|
```bash
|
|
git add src/design-system/primitives/index.ts src/design-system/composites/index.ts
|
|
git commit -m "chore: add StatusText and KpiStrip to barrel exports"
|
|
```
|
|
|
|
---
|
|
|
|
## Summary of Expected Barrel Export Additions
|
|
|
|
**`src/design-system/primitives/index.ts`** — insert after `StatusDot` line:
|
|
```ts
|
|
export { StatusText } from './StatusText/StatusText'
|
|
```
|
|
|
|
**`src/design-system/composites/index.ts`** — insert after `GroupCard` line:
|
|
```ts
|
|
export { KpiStrip } from './KpiStrip/KpiStrip'
|
|
export type { KpiItem, KpiStripProps } from './KpiStrip/KpiStrip'
|
|
```
|
|
|
|
---
|
|
|
|
## Test Commands Quick Reference
|
|
|
|
| Scope | Command |
|
|
|-------|---------|
|
|
| StatusText only | `npx vitest run src/design-system/primitives/StatusText/StatusText.test.tsx` |
|
|
| Card only | `npx vitest run src/design-system/primitives/Card/Card.test.tsx` |
|
|
| KpiStrip only | `npx vitest run src/design-system/composites/KpiStrip/KpiStrip.test.tsx` |
|
|
| All tests | `npx vitest run` |
|