Files
design-system/src/pages/Inventory/sections/PrimitivesSection.tsx
hsiegeln 433d582da6
All checks were successful
Build & Publish / publish (push) Successful in 1m2s
feat: migrate all icons to Lucide React
Replace unicode characters, emoji, and inline SVGs with lucide-react
components across the entire design system and page layer. Update
tests to assert on SVG elements instead of text content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:25:43 +01:00

659 lines
22 KiB
TypeScript

import { useState } from 'react'
import { Search } from 'lucide-react'
import styles from './PrimitivesSection.module.css'
import {
Alert,
Avatar,
Badge,
Button,
ButtonGroup,
Card,
Checkbox,
CodeBlock,
Collapsible,
DateRangePicker,
DateTimePicker,
EmptyState,
FilterPill,
FormField,
InfoCallout,
InlineEdit,
Input,
KeyboardHint,
Label,
MonoText,
Pagination,
ProgressBar,
RadioGroup,
RadioItem,
SectionHeader,
Select,
Skeleton,
Sparkline,
Spinner,
StatCard,
StatusDot,
StatusText,
Tag,
Textarea,
Toggle,
Tooltip,
} from '../../../design-system/primitives'
// ── helpers ──────────────────────────────────────────────────────────────────
interface DemoCardProps {
id: string
title: string
description: string
children: React.ReactNode
}
function DemoCard({ id, title, description, children }: DemoCardProps) {
return (
<div id={id} className={styles.componentCard}>
<h3 className={styles.componentTitle}>{title}</h3>
<p className={styles.componentDesc}>{description}</p>
<div className={styles.demoArea}>{children}</div>
</div>
)
}
// ── Sample data ───────────────────────────────────────────────────────────────
const SPARKLINE_DATA = [10, 25, 15, 30, 20, 35, 28]
const CODE_JSON = JSON.stringify(
{ status: 'ok', version: '2.4.1', routes: 42 },
null,
2,
)
// ── PrimitivesSection ─────────────────────────────────────────────────────────
export function PrimitivesSection() {
// Alert state
const [alertDismissed, setAlertDismissed] = useState(false)
// ButtonGroup state
const [bgSelection, setBgSelection] = useState<Set<string>>(new Set(['warn']))
// Checkbox state
const [checked1, setChecked1] = useState(false)
const [checked2, setChecked2] = useState(true)
// Toggle state
const [toggleOn, setToggleOn] = useState(true)
const [toggleOff, setToggleOff] = useState(false)
// Radio state
const [radioV, setRadioV] = useState('option-a')
const [radioH, setRadioH] = useState('beta')
// Pagination state
const [page, setPage] = useState(5)
// DateTimePicker state
const [dtValue, setDtValue] = useState<Date>(new Date('2026-03-18T09:00'))
// DateRangePicker state
const [dateRange, setDateRange] = useState({
start: new Date('2026-03-11T00:00'),
end: new Date('2026-03-18T23:59'),
})
// InlineEdit state
const [inlineValue, setInlineValue] = useState('Alice Johnson')
return (
<section id="primitives" className={styles.section}>
<h2 className={styles.sectionTitle}>Primitives</h2>
{/* 1. Alert */}
<DemoCard
id="alert"
title="Alert"
description="Contextual feedback messages in four variants, optionally dismissible."
>
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
<Alert variant="info" title="Info">This is an informational message.</Alert>
<Alert variant="success" title="Success">Operation completed successfully.</Alert>
<Alert variant="warning" title="Warning">This action may have side effects.</Alert>
{!alertDismissed && (
<Alert
variant="error"
title="Error"
dismissible
onDismiss={() => setAlertDismissed(true)}
>
Something went wrong. Dismiss to clear.
</Alert>
)}
{alertDismissed && (
<Button size="sm" variant="ghost" onClick={() => setAlertDismissed(false)}>
Reset dismissed alert
</Button>
)}
</div>
</DemoCard>
{/* 2. Avatar */}
<DemoCard
id="avatar"
title="Avatar"
description="Initials-based avatar with hash-derived colour, three sizes."
>
<Avatar name="Alice Johnson" size="sm" />
<Avatar name="Bob Smith" size="md" />
<Avatar name="Carol White" size="lg" />
</DemoCard>
{/* 3. Badge */}
<DemoCard
id="badge"
title="Badge"
description="Compact label with semantic colours and a dashed variant."
>
<Badge label="primary" color="primary" />
<Badge label="success" color="success" />
<Badge label="warning" color="warning" />
<Badge label="error" color="error" />
<Badge label="running" color="running" />
<Badge label="auto-hash" color="auto" />
<Badge label="dashed" color="primary" variant="dashed" />
</DemoCard>
{/* 4. Button */}
<DemoCard
id="button"
title="Button"
description="Primary, secondary, danger, and ghost variants across two sizes plus loading state."
>
<div className={styles.demoAreaColumn}>
<div className={styles.demoAreaRow}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button variant="ghost">Ghost</Button>
</div>
<div className={styles.demoAreaRow}>
<Button variant="primary" size="sm">Small</Button>
<Button variant="primary" size="md">Medium</Button>
<Button variant="primary" loading>Loading</Button>
</div>
</div>
</DemoCard>
{/* 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"
title="Card"
description="Surface container with optional left-border accent colour and title header."
>
<Card><div style={{ padding: '8px 12px', fontSize: 13 }}>Plain card</div></Card>
<Card accent="amber"><div style={{ padding: '8px 12px', fontSize: 13 }}>Amber accent</div></Card>
<Card accent="success"><div style={{ padding: '8px 12px', fontSize: 13 }}>Success accent</div></Card>
<Card accent="error"><div style={{ padding: '8px 12px', fontSize: 13 }}>Error accent</div></Card>
<Card title="Throughput (msg/s)">
<div style={{ padding: '8px 12px', fontSize: 13 }}>Card with title header and separator</div>
</Card>
<Card accent="amber" title="Error Rate">
<div style={{ padding: '8px 12px', fontSize: 13 }}>Title + accent combined</div>
</Card>
</DemoCard>
{/* 6. Checkbox */}
<DemoCard
id="checkbox"
title="Checkbox"
description="Accessible checkbox with optional label, controlled and disabled states."
>
<Checkbox
label="Unchecked"
checked={checked1}
onChange={(e) => setChecked1(e.target.checked)}
/>
<Checkbox
label="Checked"
checked={checked2}
onChange={(e) => setChecked2(e.target.checked)}
/>
<Checkbox label="Disabled" disabled />
<Checkbox label="Disabled checked" disabled defaultChecked />
</DemoCard>
{/* 7. CodeBlock */}
<DemoCard
id="codeblock"
title="CodeBlock"
description="Syntax-highlighted code block with line numbers and copy button."
>
<div style={{ width: '100%' }}>
<CodeBlock
content={CODE_JSON}
language="json"
lineNumbers
copyable
/>
</div>
</DemoCard>
{/* 8. Collapsible */}
<DemoCard
id="collapsible"
title="Collapsible"
description="Animated accordion-style disclosure, open and closed by default."
>
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
<Collapsible title="Collapsed by default">
<p style={{ margin: 0, fontSize: 13 }}>Hidden content revealed on expand.</p>
</Collapsible>
<Collapsible title="Open by default" defaultOpen>
<p style={{ margin: 0, fontSize: 13 }}>This content is visible from the start.</p>
</Collapsible>
</div>
</DemoCard>
{/* 9. DateTimePicker */}
<DemoCard
id="datetimepicker"
title="DateTimePicker"
description="Native datetime-local input wrapped with label and controlled value."
>
<DateTimePicker
label="Pick a date & time"
value={dtValue}
onChange={(d) => d && setDtValue(d)}
/>
</DemoCard>
{/* 10. DateRangePicker */}
<DemoCard
id="daterangepicker"
title="DateRangePicker"
description="Preset pills combined with two DateTimePicker inputs for a start/end range."
>
<div style={{ width: '100%' }}>
<DateRangePicker value={dateRange} onChange={setDateRange} />
</div>
</DemoCard>
{/* 11. EmptyState */}
<DemoCard
id="emptystate"
title="EmptyState"
description="Zero-data placeholder with icon, title, description, and action slot."
>
<EmptyState
icon={<span style={{ fontSize: 28 }}>📭</span>}
title="No results found"
description="Try adjusting your filters or search query."
action={<Button size="sm" variant="secondary">Clear filters</Button>}
/>
</DemoCard>
{/* 12. FilterPill */}
<DemoCard
id="filterpill"
title="FilterPill"
description="Toggle-style pill for filter UIs, with optional dot indicator and count."
>
<FilterPill label="Active" active />
<FilterPill label="Inactive" />
<FilterPill label="With dot" dot active />
<FilterPill label="Count" count={42} />
</DemoCard>
{/* 13. FormField */}
<DemoCard
id="formfield"
title="FormField"
description="Field wrapper providing a label, hint text, and inline error messaging."
>
<div className={styles.demoAreaColumn} style={{ minWidth: 240 }}>
<FormField label="Username" htmlFor="inv-username" hint="Letters and numbers only.">
<Input id="inv-username" placeholder="e.g. alice42" />
</FormField>
<FormField label="Email" htmlFor="inv-email" required error="Invalid email address.">
<Input id="inv-email" placeholder="you@example.com" />
</FormField>
</div>
</DemoCard>
{/* 14. InfoCallout */}
<DemoCard
id="infocallout"
title="InfoCallout"
description="Bordered callout block in amber, success, warning, and error flavours."
>
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
<InfoCallout variant="amber" title="Amber">Review before publishing.</InfoCallout>
<InfoCallout variant="success" title="Success">Deployment completed.</InfoCallout>
<InfoCallout variant="warning" title="Warning">Rate limit approaching.</InfoCallout>
<InfoCallout variant="error" title="Error">Build failed check logs.</InfoCallout>
</div>
</DemoCard>
{/* 15. Input */}
<DemoCard
id="input"
title="Input"
description="Text input with optional leading icon and placeholder."
>
<Input placeholder="Plain input" />
<Input icon={<Search size={14} />} placeholder="With icon" />
</DemoCard>
{/* 15b. InlineEdit */}
<DemoCard id="inline-edit" title="InlineEdit" description="Click-to-edit text field. Enter saves, Escape/blur cancels.">
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<InlineEdit value={inlineValue} onSave={setInlineValue} />
<InlineEdit value="" onSave={() => {}} placeholder="Click to add name..." />
<InlineEdit value="Read only" onSave={() => {}} disabled />
</div>
</DemoCard>
{/* 16. KeyboardHint */}
<DemoCard
id="keyboardhint"
title="KeyboardHint"
description="Styled keyboard shortcut badge."
>
<KeyboardHint keys="Ctrl+K" />
<KeyboardHint keys="⌘+P" />
<KeyboardHint keys="Esc" />
</DemoCard>
{/* 17. Label */}
<DemoCard
id="label"
title="Label"
description="Form label with optional required asterisk."
>
<Label>Plain label</Label>
<Label required>Required label</Label>
</DemoCard>
{/* 18. MonoText */}
<DemoCard
id="monotext"
title="MonoText"
description="Monospaced text in xs, sm, and md sizes."
>
<MonoText size="xs">xs: route-id-001</MonoText>
<MonoText size="sm">sm: route-id-001</MonoText>
<MonoText size="md">md: route-id-001</MonoText>
</DemoCard>
{/* 19. Pagination */}
<DemoCard
id="pagination"
title="Pagination"
description="Page navigation with ellipsis and sibling-window algorithm."
>
<Pagination
page={page}
totalPages={20}
onPageChange={setPage}
/>
</DemoCard>
{/* 20. ProgressBar */}
<DemoCard
id="progressbar"
title="ProgressBar"
description="Determinate and indeterminate bars across variants and sizes."
>
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
<ProgressBar value={60} label="60% complete" />
<ProgressBar value={60} variant="success" label="Success variant" />
<ProgressBar value={60} variant="warning" label="Warning variant" />
<ProgressBar value={60} variant="error" label="Error variant" />
<ProgressBar value={60} variant="running" label="Running variant" />
<ProgressBar indeterminate label="Indeterminate" />
<ProgressBar value={60} size="sm" label="Small size" />
</div>
</DemoCard>
{/* 21. Radio */}
<DemoCard
id="radio"
title="Radio"
description="RadioGroup with vertical and horizontal orientations."
>
<RadioGroup name="inv-radio-v" value={radioV} onChange={setRadioV} orientation="vertical">
<RadioItem value="option-a" label="Option A" />
<RadioItem value="option-b" label="Option B" />
<RadioItem value="option-c" label="Option C (disabled)" disabled />
</RadioGroup>
<RadioGroup name="inv-radio-h" value={radioH} onChange={setRadioH} orientation="horizontal">
<RadioItem value="alpha" label="Alpha" />
<RadioItem value="beta" label="Beta" />
<RadioItem value="gamma" label="Gamma" />
</RadioGroup>
</DemoCard>
{/* 22. SectionHeader */}
<DemoCard
id="sectionheader"
title="SectionHeader"
description="Labelled horizontal divider with an optional right-side action slot."
>
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
<SectionHeader>Without action</SectionHeader>
<SectionHeader action={<Button size="sm" variant="ghost">Add item</Button>}>
With action
</SectionHeader>
</div>
</DemoCard>
{/* 23. Select */}
<DemoCard
id="select"
title="Select"
description="Styled native select with custom chevron."
>
<Select
options={[
{ value: 'opt1', label: 'Option 1' },
{ value: 'opt2', label: 'Option 2' },
{ value: 'opt3', label: 'Option 3' },
]}
/>
</DemoCard>
{/* 24. Skeleton */}
<DemoCard
id="skeleton"
title="Skeleton"
description="Loading placeholder in text, circular, and rectangular variants."
>
<div className={styles.demoAreaColumn}>
<Skeleton variant="text" lines={3} width={200} />
<Skeleton variant="circular" />
<Skeleton variant="rectangular" width={200} height={60} />
</div>
</DemoCard>
{/* 25. Sparkline */}
<DemoCard
id="sparkline"
title="Sparkline"
description="Lightweight SVG line chart for inline trend visualisation."
>
<Sparkline data={SPARKLINE_DATA} width={120} height={30} />
<Sparkline data={SPARKLINE_DATA} color="var(--success)" width={120} height={30} />
<Sparkline data={SPARKLINE_DATA} color="var(--error)" width={120} height={30} />
</DemoCard>
{/* 26. Spinner */}
<DemoCard
id="spinner"
title="Spinner"
description="Animated loading indicator in sm, md, and lg sizes."
>
<Spinner size="sm" />
<Spinner size="md" />
<Spinner size="lg" />
</DemoCard>
{/* 27. StatCard */}
<DemoCard
id="statcard"
title="StatCard"
description="Metric tile with label, value, trend indicator, and inline sparkline."
>
<StatCard
label="Throughput"
value="1,433"
detail="msg/s"
trend="up"
trendValue="+12%"
accent="amber"
sparkline={SPARKLINE_DATA}
/>
<StatCard
label="Error rate"
value="0.4%"
trend="down"
trendValue="-0.1pp"
accent="error"
/>
<StatCard
label="Agents"
value="6"
detail="all healthy"
trend="neutral"
trendValue="±0"
accent="success"
/>
</DemoCard>
{/* 28. StatusDot */}
<DemoCard
id="statusdot"
title="StatusDot"
description="Small coloured dot for all status variants, with optional pulse animation."
>
{(['live', 'stale', 'dead', 'success', 'warning', 'error', 'running'] as const).map(
(v) => (
<div key={v} className={styles.demoGroupRow}>
<StatusDot variant={v} />
<span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{v}</span>
</div>
),
)}
<div className={styles.demoGroupRow}>
<StatusDot variant="live" pulse />
<span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>live + pulse</span>
</div>
</DemoCard>
{/* 29. StatusText */}
<DemoCard
id="statustext"
title="StatusText"
description="Inline coloured text for status values — five semantic variants with optional bold."
>
<div className={styles.demoAreaColumn} style={{ width: '100%' }}>
<div className={styles.demoAreaRow}>
<StatusText variant="success">99.8% uptime</StatusText>
<StatusText variant="warning">SLA at risk</StatusText>
<StatusText variant="error">BREACH</StatusText>
<StatusText variant="running">Processing</StatusText>
<StatusText variant="muted">N/A</StatusText>
</div>
<div className={styles.demoAreaRow}>
<StatusText variant="success" bold>99.8% uptime</StatusText>
<StatusText variant="warning" bold>SLA at risk</StatusText>
<StatusText variant="error" bold>BREACH</StatusText>
<StatusText variant="running" bold>Processing</StatusText>
<StatusText variant="muted" bold>N/A</StatusText>
</div>
</div>
</DemoCard>
{/* 30. Tag */}
<DemoCard
id="tag"
title="Tag"
description="Keyword label with semantic or auto hash colour, and removable variant."
>
<Tag label="primary" color="primary" />
<Tag label="success" color="success" />
<Tag label="warning" color="warning" />
<Tag label="error" color="error" />
<Tag label="auto-hash" color="auto" />
<Tag label="removable" color="primary" onRemove={() => undefined} />
</DemoCard>
{/* 31. Textarea */}
<DemoCard
id="textarea"
title="Textarea"
description="Multi-line text input with vertical resize by default."
>
<Textarea placeholder="Enter a description…" style={{ width: 280 }} />
</DemoCard>
{/* 32. Toggle */}
<DemoCard
id="toggle"
title="Toggle"
description="On/off switch with optional label."
>
<Toggle
label="Enabled"
checked={toggleOn}
onChange={(e) => setToggleOn(e.target.checked)}
/>
<Toggle
label="Disabled off"
checked={toggleOff}
onChange={(e) => setToggleOff(e.target.checked)}
/>
<Toggle label="Locked on" checked disabled />
<Toggle label="Locked off" disabled />
</DemoCard>
{/* 33. Tooltip */}
<DemoCard
id="tooltip"
title="Tooltip"
description="CSS-only hover tooltip in all four positions."
>
<Tooltip content="Top tooltip" position="top">
<Button size="sm" variant="secondary">Top</Button>
</Tooltip>
<Tooltip content="Bottom tooltip" position="bottom">
<Button size="sm" variant="secondary">Bottom</Button>
</Tooltip>
<Tooltip content="Left tooltip" position="left">
<Button size="sm" variant="secondary">Left</Button>
</Tooltip>
<Tooltip content="Right tooltip" position="right">
<Button size="sm" variant="secondary">Right</Button>
</Tooltip>
</DemoCard>
</section>
)
}