Files
bellsystems-cp/frontend/src/pages/dev/StyleGuide.jsx
T

2031 lines
93 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// frontend/src/pages/dev/StyleGuide.jsx
// Development reference only — showcases every UI component with all variants and states.
// Not linked from the sidebar. Route: /dev/styleguide (no auth required).
import { useState } from 'react'
import Button from '@/components/ui/Button'
import RowActions from '@/components/ui/RowActions'
import StatusBadge from '@/components/ui/StatusBadge'
import FormField from '@/components/ui/FormField'
import Modal from '@/components/ui/Modal'
import DataTable from '@/components/ui/DataTable'
import Pagination from '@/components/ui/Pagination'
import Spinner from '@/components/ui/Spinner'
import PageHeader from '@/components/ui/PageHeader'
import { ProfilePageHeader, ProfilePageHeaderActions } from '@/components/ui/ProfilePageHeader'
import Card from '@/components/ui/Card'
import Tabs from '@/components/ui/Tabs'
import { ToastProvider, useToast } from '@/components/ui/Toast'
import SearchBar from '@/components/ui/SearchBar'
import Select from '@/components/ui/Select'
import HeaderSearch from '@/components/ui/HeaderSearch'
import Breadcrumbs from '@/components/ui/Breadcrumbs'
import ConfirmDialog from '@/components/ui/ConfirmDialog'
import Icon, { ICON_NAMES } from '@/components/ui/Icon'
import PillButton from '@/components/ui/PillButton'
import SegmentedControl from '@/components/ui/SegmentedControl'
import IconButtonGroup from '@/components/ui/IconButtonGroup'
import DateTimePicker from '@/components/ui/DateTimePicker'
// Asset SVG URLs — grouped by folder (loaded at module init, no runtime fetch)
const _globalIconUrls = import.meta.glob('../../assets/global-icons/*.svg', { query: '?url', import: 'default', eager: true })
const _sideMenuIconUrls = import.meta.glob('../../assets/side-menu-icons/*.svg', { query: '?url', import: 'default', eager: true })
const _commsIconUrls = import.meta.glob('../../assets/comms/*.svg', { query: '?url', import: 'default', eager: true })
const _otherIconUrls = import.meta.glob('../../assets/other-icons/*.svg', { query: '?url', import: 'default', eager: true })
const _customerStatusUrls = import.meta.glob('../../assets/customer-status/*.svg', { query: '?url', import: 'default', eager: true })
// Helper: { '/path/to/foo-bar.svg': url } → [{ name: 'foo-bar', url }]
function urlMapToList(map) {
return Object.entries(map).map(([path, url]) => {
const name = path.split('/').pop().replace(/\.svg$/, '')
return { name, url }
}).sort((a, b) => a.name.localeCompare(b.name))
}
const ASSET_ICON_GROUPS = [
{ title: 'Global Icons', items: urlMapToList(_globalIconUrls) },
{ title: 'Side Menu Icons', items: urlMapToList(_sideMenuIconUrls) },
{ title: 'Comms', items: urlMapToList(_commsIconUrls) },
{ title: 'Other Icons', items: urlMapToList(_otherIconUrls) },
{ title: 'Customer Status', items: urlMapToList(_customerStatusUrls) },
]
// ---------------------------------------------------------------------------
// Layout helpers — local to this page only
// ---------------------------------------------------------------------------
function Section({ title, id, children }) {
return (
<section id={id} style={{ marginBottom: 'var(--space-12)' }}>
<h2
style={{
fontSize: 'var(--font-size-lg)',
fontWeight: 'var(--font-weight-semibold)',
color: 'var(--color-text-primary)',
borderBottom: '1px solid var(--color-border-strong)',
paddingBottom: 'var(--space-3)',
marginBottom: 'var(--space-6)',
letterSpacing: 'var(--tracking-normal)',
}}
>
{title}
</h2>
{children}
</section>
)
}
function Subsection({ title, children }) {
return (
<div style={{ marginBottom: 'var(--space-8)' }}>
<h3
style={{
fontSize: 'var(--font-size-base)',
fontWeight: 'var(--font-weight-semibold)',
color: 'var(--color-text-secondary)',
marginBottom: 'var(--space-4)',
textTransform: 'uppercase',
letterSpacing: 'var(--tracking-wide)',
fontSize: 'var(--font-size-xs)',
}}
>
{title}
</h3>
{children}
</div>
)
}
function DemoRow({ children, wrap = false }) {
return (
<div
style={{
display: 'flex',
flexWrap: wrap ? 'wrap' : 'nowrap',
alignItems: 'center',
gap: 'var(--space-4)',
marginBottom: 'var(--space-4)',
}}
>
{children}
</div>
)
}
function DemoGrid({ cols = 3, children }) {
return (
<div
style={{
display: 'grid',
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gap: 'var(--space-4)',
}}
>
{children}
</div>
)
}
function DemoCell({ label, children, centered = false }) {
return (
<div
style={{
backgroundColor: 'var(--color-bg-surface)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-lg)',
padding: 'var(--space-4)',
display: 'flex',
flexDirection: 'column',
gap: 'var(--space-3)',
alignItems: centered ? 'center' : 'flex-start',
}}
>
{children}
<span
style={{
fontSize: 'var(--font-size-xs)',
color: 'var(--color-text-muted)',
fontFamily: 'var(--font-family-mono)',
marginTop: 'auto',
}}
>
{label}
</span>
</div>
)
}
function Swatch({ token, value }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
<div
style={{
width: '100%',
height: '40px',
backgroundColor: `var(${token})`,
borderRadius: 'var(--radius-md)',
border: '1px solid var(--color-border)',
}}
/>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>
{token}
</span>
</div>
)
}
// ---------------------------------------------------------------------------
// Demo sub-components (need hooks, so they're standalone components)
// ---------------------------------------------------------------------------
function DateTimePickerDemo() {
const [val, setVal] = useState('')
const [val2, setVal2] = useState('2026-03-05T14:30:00.000Z')
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-6)' }}>
<DemoGrid cols={2}>
<DemoCell label="Empty (no value)">
<DateTimePicker
label="Occurred at"
name="demo-dtp-1"
value={val}
onChange={setVal}
hint="Click to open the calendar"
/>
{val && (
<span style={{ fontSize: 'var(--font-size-xs)', fontFamily: 'var(--font-family-mono)', color: 'var(--color-text-muted)', marginTop: 'var(--space-2)' }}>
ISO: {val}
</span>
)}
</DemoCell>
<DemoCell label="Pre-filled value">
<DateTimePicker
label="Scheduled for"
name="demo-dtp-2"
value={val2}
onChange={setVal2}
/>
</DemoCell>
<DemoCell label="With error">
<DateTimePicker
label="Date & time"
name="demo-dtp-3"
value=""
onChange={() => {}}
error="This field is required"
/>
</DemoCell>
<DemoCell label="Disabled">
<DateTimePicker
label="Locked field"
name="demo-dtp-4"
value="2026-01-15T09:00:00.000Z"
onChange={() => {}}
disabled
/>
</DemoCell>
</DemoGrid>
</div>
)
}
function TabsLineDemo() {
const [active, setActive] = useState('overview')
const tabs = [
{ key: 'overview', label: 'Overview' },
{ key: 'financials', label: 'Financials' },
{ key: 'orders', label: 'Orders' },
{ key: 'support', label: 'Support' },
]
return (
<div>
<Tabs tabs={tabs} active={active} onChange={setActive} variant="line" />
<div style={{ padding: 'var(--space-4)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>
Active tab: <strong style={{ color: 'var(--color-text-primary)' }}>{active}</strong>
</div>
</div>
)
}
function TabsPillDemo() {
const [active, setActive] = useState('devices')
const tabs = [
{ key: 'devices', label: 'Devices' },
{ key: 'firmware', label: 'Firmware' },
{ key: 'mqtt', label: 'MQTT' },
]
return <Tabs tabs={tabs} active={active} onChange={setActive} variant="pill" />
}
function TabsIconDemo() {
const [active, setActive] = useState('customers')
const tabs = [
{
key: 'customers', label: 'Customers', count: 142,
icon: <Icon name="customers" size={14} />,
},
{
key: 'orders', label: 'Orders', count: 28,
icon: <Icon name="devices" size={14} />,
},
{
key: 'settings', label: 'Settings',
icon: <Icon name="settings" size={14} />,
},
]
return <Tabs tabs={tabs} active={active} onChange={setActive} variant="line" />
}
function ToastDemo() {
const { toast } = useToast()
return (
<DemoRow wrap>
<Button variant="success" size="md" onClick={() => toast.success('Saved!', 'Your changes have been saved successfully.')}>
success
</Button>
<Button variant="secondary" size="md" onClick={() => toast.info('Info', 'A new firmware version is available.')}>
info
</Button>
<Button variant="secondary" size="md" onClick={() => toast.warning('Warning', 'Disk space is running low on this device.')}>
warning
</Button>
<Button variant="danger" size="md" onClick={() => toast.danger('Error', 'Failed to connect to the device. Check your network.')}>
danger
</Button>
<Button variant="ghost" size="md" onClick={() => toast.success('Title only')}>
title only
</Button>
</DemoRow>
)
}
function HeaderSearchDemo() {
const [value, setValue] = useState('')
return <HeaderSearch value={value} onChange={setValue} placeholder="Search…" />
}
function SearchBarControlledDemo() {
const [value, setValue] = useState('Bell Unit')
return (
<div style={{ maxWidth: '400px', display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
<SearchBar value={value} onChange={setValue} placeholder="Search…" />
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>
Current value: <code style={{ fontFamily: 'var(--font-family-mono)', color: 'var(--color-primary)' }}>"{value}"</code>
</p>
</div>
)
}
function ConfirmDialogDemo() {
const [dangerOpen, setDangerOpen] = useState(false)
const [primaryOpen, setPrimaryOpen] = useState(false)
const [loading, setLoading] = useState(false)
function handleConfirm(setter) {
setLoading(true)
setTimeout(() => { setLoading(false); setter(false) }, 1800)
}
return (
<DemoRow>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
<Button variant="danger" onClick={() => setDangerOpen(true)}>
Danger variant
</Button>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>variant="danger"</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
<Button variant="secondary" onClick={() => setPrimaryOpen(true)}>
Primary variant
</Button>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>variant="primary"</span>
</div>
<ConfirmDialog
open={dangerOpen}
onClose={() => setDangerOpen(false)}
onConfirm={() => handleConfirm(setDangerOpen)}
title="Delete Device"
message="This will permanently delete Bell Unit Alpha and all associated data. This action cannot be undone."
confirmLabel="Delete"
variant="danger"
loading={loading}
/>
<ConfirmDialog
open={primaryOpen}
onClose={() => setPrimaryOpen(false)}
onConfirm={() => handleConfirm(setPrimaryOpen)}
title="Publish Firmware"
message="This will push firmware v2.3.1 to all connected devices in this group. Confirm to proceed."
confirmLabel="Publish"
variant="primary"
loading={loading}
/>
</DemoRow>
)
}
// ---------------------------------------------------------------------------
// Sample data for DataTable demos
// ---------------------------------------------------------------------------
// All columns definition for the interactive column-management demo
const TABLE_ALL_COLUMNS = [
{ key: 'name', label: 'Name', sortable: true, alwaysOn: true },
{ key: 'serial', label: 'Serial', width: '140px' },
{ key: 'location', label: 'Location', sortable: true },
{ key: 'status', label: 'Status', width: '120px', render: (row) => <StatusBadge variant={row.statusVariant}>{row.status}</StatusBadge> },
{ key: 'actions', label: '', width: '110px', align: 'right', render: () => (
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<RowActions actions={[
{ label: 'Edit', icon: <Icon name="edit" size={14} />, color: 'var(--color-info)', onClick: () => {} },
{ label: 'Delete', icon: <Icon name="delete" size={14} />, color: 'var(--color-danger)', onClick: () => {}, divider: true },
]} />
</div>
) },
]
const TABLE_DATA = [
{ id: 1, name: 'Bell Unit Alpha', serial: 'SN-00001', location: 'London', status: 'Online', statusVariant: 'success' },
{ id: 2, name: 'Bell Unit Beta', serial: 'SN-00002', location: 'Berlin', status: 'Pending', statusVariant: 'warning' },
{ id: 3, name: 'Bell Unit Gamma', serial: 'SN-00003', location: 'Athens', status: 'Offline', statusVariant: 'danger' },
{ id: 4, name: 'Bell Unit Delta', serial: 'SN-00004', location: 'New York', status: 'Info', statusVariant: 'info' },
]
// Static column list for non-interactive demos
const TABLE_COLUMNS = TABLE_ALL_COLUMNS
// ---------------------------------------------------------------------------
// Main style guide
// ---------------------------------------------------------------------------
export default function StyleGuide() {
const [modalOpen, setModalOpen] = useState(false)
const [modalSize, setModalSize] = useState('md')
const [paginationPage, setPaginationPage] = useState(3)
const [paginationSize, setPaginationSize] = useState(20)
const [formValues, setFormValues] = useState({ text: '', email: '', select: '', textarea: '' })
// DataTable interactive demo state
const [tableSortKey, setTableSortKey] = useState('')
const [tableSortDir, setTableSortDir] = useState('asc')
const [tableVisibleKeys, setTableVisibleKeys] = useState(
TABLE_ALL_COLUMNS.map((c) => c.key)
)
// Filter bar + table demo state
const [filterSearch, setFilterSearch] = useState('')
const [filterStatus, setFilterStatus] = useState('')
const [filterLocation, setFilterLocation] = useState('')
const filteredTableData = TABLE_DATA.filter((row) => {
const matchSearch = !filterSearch || row.name.toLowerCase().includes(filterSearch.toLowerCase()) || row.serial.toLowerCase().includes(filterSearch.toLowerCase())
const matchStatus = !filterStatus || row.status.toLowerCase() === filterStatus.toLowerCase()
const matchLocation = !filterLocation || row.location.toLowerCase() === filterLocation.toLowerCase()
return matchSearch && matchStatus && matchLocation
})
const sortedTableData = tableSortKey
? [...TABLE_DATA].sort((a, b) => {
const va = (a[tableSortKey] || '').toString().toLowerCase()
const vb = (b[tableSortKey] || '').toString().toLowerCase()
if (va < vb) return tableSortDir === 'asc' ? -1 : 1
if (va > vb) return tableSortDir === 'asc' ? 1 : -1
return 0
})
: TABLE_DATA
const visibleTableColumns = TABLE_ALL_COLUMNS.filter((c) => tableVisibleKeys.includes(c.key))
const handleFormChange = (name) => (e) => setFormValues((v) => ({ ...v, [name]: e.target.value }))
return (
<div
style={{
minHeight: '100vh',
backgroundColor: 'var(--color-bg-base)',
color: 'var(--color-text-primary)',
fontFamily: 'var(--font-family-base)',
}}
>
{/* ── Dev banner ───────────────────────────────────────────────────── */}
<div
style={{
backgroundColor: 'var(--color-warning-bg)',
borderBottom: '1px solid var(--color-warning)',
padding: 'var(--space-2) var(--space-6)',
fontSize: 'var(--font-size-sm)',
color: 'var(--color-warning)',
fontWeight: 'var(--font-weight-medium)',
textAlign: 'center',
}}
>
⚠ Development Style Guide — Do not ship in production navigation
</div>
<div style={{ maxWidth: '1100px', margin: '0 auto', padding: 'var(--space-8) var(--space-6)' }}>
{/* ── Page title ─────────────────────────────────────────────────── */}
<div style={{ marginBottom: 'var(--space-10)' }}>
<h1
style={{
fontSize: 'var(--font-size-xl)',
fontWeight: 'var(--font-weight-bold)',
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-2)',
}}
>
BellSystems v2 — Component Style Guide
</h1>
<p style={{ fontSize: 'var(--font-size-base)', color: 'var(--color-text-muted)' }}>
Every UI component with every variant and state. For development reference only.
</p>
</div>
{/* ── TOC ────────────────────────────────────────────────────────── */}
<nav
aria-label="On-page navigation"
style={{
backgroundColor: 'var(--color-bg-surface)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-lg)',
padding: 'var(--space-4) var(--space-6)',
marginBottom: 'var(--space-10)',
display: 'flex',
flexWrap: 'wrap',
gap: 'var(--space-2) var(--space-6)',
}}
>
{[
['#colors', 'Color Tokens'],
['#typography', 'Typography'],
['#spacing', 'Spacing'],
['#buttons', 'Button'],
['#badges', 'StatusBadge'],
['#formfields', 'FormField'],
['#spinner', 'Spinner'],
['#datatable', 'DataTable'],
['#rowactions', 'RowActions'],
['#pagination', 'Pagination'],
['#pageheader', 'PageHeader'],
['#profileheader', 'ProfilePageHeader'],
['#modal', 'Modal'],
['#card', 'Card'],
['#tabs', 'Tabs'],
['#toast', 'Toast'],
['#searchbar', 'SearchBar'],
['#headersearch', 'HeaderSearch'],
['#breadcrumbs', 'Breadcrumbs'],
['#confirmdialog','ConfirmDialog'],
['#icons', 'Icon'],
].map(([href, label]) => (
<a
key={href}
href={href}
style={{
fontSize: 'var(--font-size-sm)',
color: 'var(--color-text-accent)',
textDecoration: 'none',
fontWeight: 'var(--font-weight-medium)',
}}
>
{label}
</a>
))}
</nav>
{/* ================================================================
1. COLOR TOKENS
================================================================ */}
<Section title="Color Tokens" id="colors">
<Subsection title="Backgrounds">
<DemoGrid cols={4}>
{[
'--color-bg-abyss', '--color-bg-base', '--color-bg-void',
'--color-bg-surface', '--color-bg-elevated', '--color-bg-island', '--color-bg-float',
].map((t) => <Swatch key={t} token={t} />)}
</DemoGrid>
</Subsection>
<Subsection title="Brand / Primary">
<DemoGrid cols={4}>
{[
'--color-primary', '--color-primary-hover', '--color-primary-container',
'--color-primary-subtle', '--color-primary-deep', '--color-primary-inverse',
].map((t) => <Swatch key={t} token={t} />)}
</DemoGrid>
</Subsection>
<Subsection title="Semantic">
<DemoGrid cols={4}>
{[
'--color-success', '--color-success-bg',
'--color-warning', '--color-warning-bg',
'--color-danger', '--color-danger-bg',
'--color-info', '--color-info-bg',
].map((t) => <Swatch key={t} token={t} />)}
</DemoGrid>
</Subsection>
<Subsection title="Text">
<DemoGrid cols={4}>
{['--color-text-primary', '--color-text-secondary', '--color-text-muted', '--color-text-accent'].map(
(t) => (
<div key={t} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
<div
style={{
fontSize: 'var(--font-size-md)',
fontWeight: 'var(--font-weight-semibold)',
color: `var(${t})`,
padding: 'var(--space-3)',
backgroundColor: 'var(--color-bg-surface)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--color-border)',
}}
>
Aa
</div>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>
{t}
</span>
</div>
)
)}
</DemoGrid>
</Subsection>
<Subsection title="Borders">
<DemoGrid cols={3}>
{['--color-border', '--color-border-strong', '--color-border-focus'].map((t) => (
<div key={t} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
<div
style={{
height: '40px',
border: `2px solid var(${t})`,
borderRadius: 'var(--radius-md)',
backgroundColor: 'var(--color-bg-surface)',
}}
/>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>
{t}
</span>
</div>
))}
</DemoGrid>
</Subsection>
</Section>
{/* ================================================================
2. TYPOGRAPHY
================================================================ */}
<Section title="Typography" id="typography">
<Subsection title="Font Sizes">
{[
{ token: '--font-size-2xl', label: '2xl — 56px — Hero KPIs', sample: '1,284' },
{ token: '--font-size-xl', label: 'xl — 24px — Page Titles', sample: 'Device Inventory' },
{ token: '--font-size-lg', label: 'lg — 18px — Section Headings', sample: 'Recent Activity' },
{ token: '--font-size-md', label: 'md — 16px — Card Titles', sample: 'Connection Status' },
{ token: '--font-size-base',label: 'base — 14px — Body / Table rows', sample: 'Bell Unit Alpha SN-00001' },
{ token: '--font-size-sm', label: 'sm — 12px — Captions / Helper text', sample: 'Last seen 5 minutes ago' },
{ token: '--font-size-xs', label: 'xs — 11px — Labels / Chips', sample: 'DEVICE STATUS' },
{ token: '--font-size-2xs', label: '2xs — 10px — Sidebar section headers / Micro labels', sample: 'BELL CLOUD' },
].map(({ token, label, sample }) => (
<div
key={token}
style={{
display: 'flex',
alignItems: 'baseline',
gap: 'var(--space-4)',
padding: 'var(--space-3) 0',
borderBottom: '1px solid var(--color-border)',
}}
>
<span
style={{
fontFamily: 'var(--font-family-mono)',
fontSize: 'var(--font-size-xs)',
color: 'var(--color-text-muted)',
minWidth: '200px',
flexShrink: 0,
}}
>
{label}
</span>
<span style={{ fontSize: `var(${token})`, color: 'var(--color-text-primary)' }}>
{sample}
</span>
</div>
))}
</Subsection>
<Subsection title="Font Weights">
<DemoRow wrap>
{[
{ token: '--font-weight-normal', label: 'normal / 400' },
{ token: '--font-weight-medium', label: 'medium / 500' },
{ token: '--font-weight-semibold', label: 'semibold / 600' },
{ token: '--font-weight-bold', label: 'bold / 700' },
].map(({ token, label }) => (
<div key={token} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
<span style={{ fontWeight: `var(${token})`, fontSize: 'var(--font-size-md)', color: 'var(--color-text-primary)' }}>
Hamburgefons
</span>
<span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
{label}
</span>
</div>
))}
</DemoRow>
</Subsection>
<Subsection title="Letter Spacing">
<DemoRow wrap>
{[
{ token: '--tracking-display', label: '-0.02em — Hero KPI numbers' },
{ token: '--tracking-tight', label: '-0.01em — Barlow Condensed headings' },
{ token: '--tracking-normal', label: '0em — Default body text' },
{ token: '--tracking-snug', label: '0.04em — Sidebar section labels (uppercase)' },
{ token: '--tracking-wide', label: '0.08em — Chip labels, all-caps badges' },
].map(({ token, label }) => (
<div key={token} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)', minWidth: '200px' }}>
<span style={{ letterSpacing: `var(${token})`, textTransform: 'uppercase', fontSize: 'var(--font-size-sm)', fontWeight: 600, color: 'var(--color-text-primary)' }}>
Bell Cloud
</span>
<span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
{token}
</span>
<span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
{label}
</span>
</div>
))}
</DemoRow>
</Subsection>
<Subsection title="Type pairing — Display + Body + Sidebar">
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-4)' }}>
<div>
<div style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-1)' }}>
--font-family-display — Barlow Condensed
</div>
<div style={{ fontFamily: 'var(--font-family-display)', fontSize: '2rem', fontWeight: 600, letterSpacing: 'var(--tracking-tight)', color: 'var(--color-text-primary)', lineHeight: 1.2 }}>
Device Inventory
</div>
<div style={{ fontFamily: 'var(--font-family-display)', fontSize: '1.5rem', fontWeight: 600, letterSpacing: 'var(--tracking-tight)', color: 'var(--color-text-secondary)', lineHeight: 1.2 }}>
BellSystems Control Panel v2
</div>
</div>
<div>
<div style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-1)' }}>
--font-family-base — Onest
</div>
<div style={{ fontFamily: 'var(--font-family-base)', fontSize: 'var(--font-size-base)', color: 'var(--color-text-primary)', lineHeight: 'var(--line-height-base)' }}>
Bell Unit Alpha · SN-00042 · Firmware v2.3.1 · Last seen 5 minutes ago via MQTT
</div>
<div style={{ fontFamily: 'var(--font-family-base)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', marginTop: 'var(--space-1)' }}>
0123456789 &nbsp;· &nbsp;ABCDEFGHIJKLMNOPQRSTUVWXYZ &nbsp;· &nbsp;abcdefghijklmnopqrstuvwxyz
</div>
</div>
<div>
<div style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-1)' }}>
--font-family-sidebar — Inter · Used for sidebar navigation (section labels, nav items)
</div>
<div style={{ fontFamily: 'var(--font-family-sidebar)', fontSize: 'var(--font-size-2xs)', fontWeight: 600, letterSpacing: 'var(--tracking-snug)', textTransform: 'uppercase', color: 'var(--color-sidebar-text-muted)', marginBottom: 'var(--space-1)' }}>
Bell Cloud
</div>
<div style={{ fontFamily: 'var(--font-family-sidebar)', fontSize: 'var(--font-size-sm)', fontWeight: 500, color: 'var(--color-sidebar-text)' }}>
Devices · Melodies · App Users
</div>
<div style={{ fontFamily: 'var(--font-family-sidebar)', fontSize: 'var(--font-size-sm)', fontWeight: 500, color: 'var(--color-sidebar-active-text)', marginTop: 'var(--space-1)' }}>
Dashboard — active/selected state (white)
</div>
</div>
</div>
</Subsection>
<Subsection title="Monospace (IDs, serials, codes)">
<div
style={{
fontFamily: 'var(--font-family-mono)',
fontSize: 'var(--font-size-base)',
color: 'var(--color-text-primary)',
backgroundColor: 'var(--color-bg-abyss)',
padding: 'var(--space-4)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--color-border)',
}}
>
SN-00042 &nbsp;|&nbsp; MAC: AA:BB:CC:DD:EE:FF &nbsp;|&nbsp; fw-v2.3.1-stable
</div>
</Subsection>
</Section>
{/* ================================================================
3. SPACING
================================================================ */}
<Section title="Spacing" id="spacing">
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
{[1, 2, 3, 4, 5, 6, 8, 10, 12, 16].map((n) => (
<div key={n} style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)' }}>
<span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', minWidth: '120px' }}>
--space-{n}
</span>
<div
style={{
height: '20px',
width: `var(--space-${n})`,
backgroundColor: 'var(--color-primary)',
borderRadius: 'var(--radius-sm)',
flexShrink: 0,
}}
/>
<span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
{n * 4}px
</span>
</div>
))}
</div>
</Section>
{/* ================================================================
4. BUTTON
================================================================ */}
<Section title="Button" id="buttons">
<Subsection title="Variants — size md">
<DemoRow wrap>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
<Button variant="success">Success</Button>
<Button variant="info">Info</Button>
<Button variant="warning">Warning</Button>
<Button variant="table-actions">Table Actions</Button>
</DemoRow>
</Subsection>
<Subsection title="table-actions — row-hover context (hover the row below)">
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-3)' }}>
At rest the button is invisible. It becomes visible on row hover or when open. This is the standard last-column button for all tables.
</p>
<div className="table-container">
<table className="table">
<thead className="table-thead">
<tr>
<th>Name</th>
<th>Status</th>
<th style={{ textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody className="table-tbody">
{['Bell Unit Alpha', 'Bell Unit Beta', 'Bell Unit Gamma'].map((name) => (
<tr key={name} className="clickable">
<td>{name}</td>
<td><StatusBadge variant="success">Online</StatusBadge></td>
<td style={{ textAlign: 'right' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button variant="table-actions" size="sm">Actions</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Subsection>
<Subsection title="Sizes — variant primary">
<DemoRow>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--space-1)' }}>
<Button variant="primary" size="sm">Small</Button>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>size="sm"</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--space-1)' }}>
<Button variant="primary" size="md">Medium (default)</Button>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>size="md"</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--space-1)' }}>
<Button variant="primary" size="lg">Large</Button>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>size="lg"</span>
</div>
</DemoRow>
</Subsection>
<Subsection title="States">
<DemoGrid cols={3}>
<DemoCell label='loading={true}' centered>
<Button variant="primary" loading>Saving…</Button>
</DemoCell>
<DemoCell label='disabled={true}' centered>
<Button variant="primary" disabled>Disabled</Button>
</DemoCell>
<DemoCell label='loading + secondary' centered>
<Button variant="secondary" loading>Loading</Button>
</DemoCell>
<DemoCell label='loading + danger' centered>
<Button variant="danger" loading>Deleting…</Button>
</DemoCell>
<DemoCell label='disabled + ghost' centered>
<Button variant="ghost" disabled>Can't click</Button>
</DemoCell>
<DemoCell label='disabled + danger' centered>
<Button variant="danger" disabled>Disabled</Button>
</DemoCell>
</DemoGrid>
</Subsection>
<Subsection title="With Icons (left and right)">
<DemoRow wrap>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
<Button
variant="primary"
icon={
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<line x1="8" y1="2" x2="8" y2="14" /><line x1="2" y1="8" x2="14" y2="8" />
</svg>
}
>
Add Device
</Button>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>icon (left)</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
<Button
variant="secondary"
iconRight={
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M3 8h10M9 4l4 4-4 4" />
</svg>
}
>
Continue
</Button>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>iconRight (right)</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
<Button
variant="ghost"
size="sm"
icon={
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M2 8h12M8 2l6 6-6 6" />
</svg>
}
aria-label="Refresh"
/>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>icon-only</span>
</div>
</DemoRow>
</Subsection>
<Subsection title="All variants × all sizes matrix">
<div style={{ overflowX: 'auto' }}>
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
<thead>
<tr>
<th style={{ padding: 'var(--space-2) var(--space-3)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', textAlign: 'left', fontFamily: 'var(--font-family-mono)', fontWeight: 'var(--font-weight-normal)' }}>variant</th>
{['sm', 'md', 'lg'].map((s) => (
<th key={s} style={{ padding: 'var(--space-2) var(--space-4)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', textAlign: 'center', fontFamily: 'var(--font-family-mono)', fontWeight: 'var(--font-weight-normal)' }}>
size="{s}"
</th>
))}
</tr>
</thead>
<tbody>
{['primary', 'secondary', 'ghost', 'danger', 'success', 'table-actions'].map((v) => (
<tr key={v} style={{ borderTop: '1px solid var(--color-border)' }}>
<td style={{ padding: 'var(--space-3)', fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
{v}
</td>
{['sm', 'md', 'lg'].map((s) => (
<td key={s} style={{ padding: 'var(--space-3) var(--space-4)', textAlign: 'center' }}>
<Button variant={v} size={s}>{v}</Button>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</Subsection>
</Section>
{/* ================================================================
5. STATUS BADGE
================================================================ */}
<Section title="StatusBadge" id="badges">
<Subsection title="All variants — size md (default)">
<DemoRow wrap>
{['success', 'warning', 'danger', 'info', 'primary', 'neutral'].map((v) => (
<div key={v} style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--space-1)' }}>
<StatusBadge variant={v}>{v}</StatusBadge>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>variant="{v}"</span>
</div>
))}
</DemoRow>
</Subsection>
<Subsection title='All variants — size="sm"'>
<DemoRow wrap>
{['success', 'warning', 'danger', 'info', 'primary', 'neutral'].map((v) => (
<div key={v} style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--space-1)' }}>
<StatusBadge variant={v} size="sm">{v}</StatusBadge>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>size="sm"</span>
</div>
))}
</DemoRow>
</Subsection>
<Subsection title='dot={false}'>
<DemoRow wrap>
{['success', 'warning', 'danger', 'info', 'primary', 'neutral'].map((v) => (
<StatusBadge key={v} variant={v} dot={false}>{v}</StatusBadge>
))}
</DemoRow>
</Subsection>
<Subsection title="Preset STATUS_VARIANT mappings">
<DemoGrid cols={4}>
{[
['online', 'success'], ['active', 'success'], ['connected', 'success'],
['sold', 'success'], ['claimed', 'success'], ['provisioned','success'],
['flashed', 'info'], ['manufactured','info'], ['pending', 'warning'],
['offline', 'danger'], ['error', 'danger'], ['inactive', 'danger'],
['open', 'primary'], ['draft', 'neutral'], ['sent', 'info'],
['accepted', 'success'], ['rejected', 'danger'], ['paid', 'success'],
['unpaid', 'warning'], ['overdue', 'danger'], ['cancelled', 'neutral'],
['completed', 'success'], ['processing', 'info'], ['shipped', 'primary'],
['delivered', 'success'], ['returned', 'warning'],
].map(([status, v]) => (
<StatusBadge key={status} variant={v}>{status}</StatusBadge>
))}
</DemoGrid>
</Subsection>
</Section>
{/* ================================================================
5b. PILL BUTTON
================================================================ */}
<Section title="PillButton" id="pill-button">
<Subsection title="All variants — inline with badges">
<DemoRow wrap>
{['neutral', 'primary', 'success', 'warning', 'danger', 'info'].map((v) => (
<div key={v} style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--space-1)' }}>
<PillButton
variant={v}
value={v}
options={[{ label: 'English', value: 'en' }, { label: 'Greek', value: 'el' }, { label: 'French', value: 'fr' }]}
>
{v}
</PillButton>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>variant="{v}"</span>
</div>
))}
</DemoRow>
</Subsection>
<Subsection title="Inline with StatusBadge — matching size">
<DemoRow>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 'var(--space-2)', fontSize: 'var(--font-size-lg)', fontWeight: 600, color: 'var(--color-text-primary)' }}>
Melody Title
<StatusBadge variant="success">Live</StatusBadge>
<PillButton
variant="neutral"
value="en"
options={[{ label: 'English', value: 'en' }, { label: 'Greek', value: 'el' }]}
>
English
</PillButton>
</span>
</DemoRow>
</Subsection>
</Section>
{/* ================================================================
6a. SEGMENTED CONTROL
================================================================ */}
<Section title="SegmentedControl" id="segmented-control">
<Subsection title="Variants — active button style">
<DemoRow wrap>
{['secondary', 'primary', 'ghost'].map((v) => (
<div key={v} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
<SegmentedControl
variant={v}
value="inbound"
onChange={() => {}}
options={[
{ value: 'inbound', label: 'Inbound' },
{ value: 'outbound', label: 'Outbound' },
]}
/>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>variant="{v}"</span>
</div>
))}
</DemoRow>
</Subsection>
<Subsection title="Sizes">
<DemoRow wrap>
{['sm', 'md', 'lg'].map((s) => (
<div key={s} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
<SegmentedControl
size={s}
value="all"
onChange={() => {}}
options={[
{ value: 'all', label: 'All' },
{ value: 'unread', label: 'Unread' },
{ value: 'read', label: 'Read' },
]}
/>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>size="{s}"</span>
</div>
))}
</DemoRow>
</Subsection>
<Subsection title="Three and four options — real-world examples">
<DemoRow wrap>
<SegmentedControl
value="both"
onChange={() => {}}
options={[
{ value: 'sales', label: 'Sales' },
{ value: 'support', label: 'Support' },
{ value: 'both', label: 'All' },
]}
/>
<SegmentedControl
value="all"
onChange={() => {}}
options={[
{ value: 'all', label: 'All' },
{ value: 'unread', label: 'Unread' },
{ value: 'read', label: 'Read' },
{ value: 'important', label: 'Bookmarked' },
]}
/>
</DemoRow>
</Subsection>
<Subsection title="Disabled state">
<SegmentedControl
disabled
value="inbound"
onChange={() => {}}
options={[
{ value: 'inbound', label: 'Inbound' },
{ value: 'outbound', label: 'Outbound' },
]}
/>
</Subsection>
</Section>
{/* ================================================================
6b. ICON BUTTON GROUP
================================================================ */}
<Section title="IconButtonGroup" id="icon-button-group">
<Subsection title="Default (secondary) — refresh + sync example">
<DemoRow>
<IconButtonGroup
buttons={[
{
label: 'Refresh',
icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>,
onClick: () => {},
},
{
label: 'Sync',
icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg>,
onClick: () => {},
},
]}
/>
</DemoRow>
</Subsection>
<Subsection title="Ghost variant">
<DemoRow>
<IconButtonGroup
variant="ghost"
buttons={[
{ label: 'Edit', icon: <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>, onClick: () => {} },
{ label: 'Delete', icon: <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>, onClick: () => {} },
{ label: 'Share', icon: <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>, onClick: () => {} },
]}
/>
</DemoRow>
</Subsection>
<Subsection title="Sizes">
<DemoRow wrap>
{['sm', 'md', 'lg'].map((s) => (
<div key={s} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
<IconButtonGroup
size={s}
buttons={[
{ label: 'Refresh', icon: <svg width={s === 'sm' ? 13 : s === 'md' ? 15 : 17} height={s === 'sm' ? 13 : s === 'md' ? 15 : 17} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>, onClick: () => {} },
{ label: 'Sync', icon: <svg width={s === 'sm' ? 13 : s === 'md' ? 15 : 17} height={s === 'sm' ? 13 : s === 'md' ? 15 : 17} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg>, onClick: () => {} },
]}
/>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>size="{s}"</span>
</div>
))}
</DemoRow>
</Subsection>
<Subsection title="Loading and disabled states">
<DemoRow>
<IconButtonGroup
buttons={[
{ label: 'Refresh', loading: true, icon: <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>, onClick: () => {} },
{ label: 'Sync', disabled: true, icon: <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg>, onClick: () => {} },
]}
/>
</DemoRow>
</Subsection>
</Section>
{/* ================================================================
6. FORM FIELD
================================================================ */}
<Section title="FormField" id="formfields">
<Subsection title="Input types">
<DemoGrid cols={2}>
<FormField label="Text" name="sg-text" type="text" placeholder="Enter text…" value={formValues.text} onChange={handleFormChange('text')} />
<FormField label="Email" name="sg-email" type="email" placeholder="name@example.com" value={formValues.email} onChange={handleFormChange('email')} />
<FormField label="Password" name="sg-password" type="password" placeholder="••••••••" value="" onChange={() => {}} />
<FormField label="Number" name="sg-number" type="number" placeholder="0" value="" onChange={() => {}} />
<FormField label="URL" name="sg-url" type="url" placeholder="https://…" value="" onChange={() => {}} />
<FormField label="Tel" name="sg-tel" type="tel" placeholder="+1 555 000 0000" value="" onChange={() => {}} />
</DemoGrid>
</Subsection>
<Subsection title="Textarea">
<FormField
label="Notes"
name="sg-textarea"
type="textarea"
placeholder="Enter notes…"
rows={4}
value={formValues.textarea}
onChange={handleFormChange('textarea')}
/>
</Subsection>
<Subsection title="Select">
<FormField label="Status" name="sg-select" type="select" value={formValues.select} onChange={handleFormChange('select')}>
<option value="">Choose…</option>
<option value="online">Online</option>
<option value="offline">Offline</option>
<option value="pending">Pending</option>
</FormField>
</Subsection>
<Subsection title="States">
<DemoGrid cols={2}>
<FormField label="Required field" name="sg-req" type="text" required placeholder="Must be filled" value="" onChange={() => {}} />
<FormField label="Disabled" name="sg-dis" type="text" disabled placeholder="Read only" value="Cannot change" onChange={() => {}} />
<FormField
label="With hint"
name="sg-hint"
type="text"
hint="This is helper text shown below the input."
placeholder="Type something…"
value=""
onChange={() => {}}
/>
<FormField
label="Validation error"
name="sg-err"
type="text"
error="This field is required and cannot be blank."
placeholder="Invalid value"
value=""
onChange={() => {}}
/>
<FormField
label="Required + error"
name="sg-req-err"
type="email"
required
error="Please enter a valid email address."
placeholder="bad-value"
value="bad-value"
onChange={() => {}}
/>
<FormField
label="Required + hint"
name="sg-req-hint"
type="text"
required
hint="Will appear in the customer portal."
placeholder="Display name…"
value=""
onChange={() => {}}
/>
</DemoGrid>
</Subsection>
</Section>
{/* ================================================================
7. SPINNER
================================================================ */}
<Section title="Spinner" id="spinner">
<Subsection title="Sizes">
<DemoRow>
{['sm', 'md', 'lg'].map((s) => (
<div key={s} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--space-2)' }}>
<Spinner size={s} />
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontFamily: 'var(--font-family-mono)' }}>size="{s}"</span>
</div>
))}
</DemoRow>
</Subsection>
<Subsection title="Colour — inherits currentColor">
<DemoRow>
{[
['var(--color-primary)', 'primary'],
['var(--color-success)', 'success'],
['var(--color-warning)', 'warning'],
['var(--color-danger)', 'danger'],
['var(--color-text-muted)', 'muted'],
].map(([color, label]) => (
<div key={label} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--space-2)', color }}>
<Spinner size="md" />
<span style={{ fontSize: 'var(--font-size-xs)', fontFamily: 'var(--font-family-mono)', color: 'var(--color-text-muted)' }}>{label}</span>
</div>
))}
</DemoRow>
</Subsection>
</Section>
{/* ================================================================
8. DATA TABLE
================================================================ */}
<Section title="DataTable" id="datatable">
<Subsection title="Interactive — sortable columns, drag-to-reorder, column visibility toggle">
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-3)' }}>
Click a column header to sort. Drag column headers left/right to reorder. Use the grid icon (far right of header) to show/hide columns.
</p>
<DataTable
columns={visibleTableColumns}
data={sortedTableData}
loading={false}
sortKey={tableSortKey}
sortDir={tableSortDir}
onSort={(key, dir) => { setTableSortKey(key); setTableSortDir(dir) }}
allColumns={TABLE_ALL_COLUMNS}
visibleKeys={tableVisibleKeys}
onColumnChange={setTableVisibleKeys}
onRowClick={() => {}}
/>
</Subsection>
<Subsection title="Loading state (skeleton)">
<DataTable columns={TABLE_COLUMNS} data={[]} loading />
</Subsection>
<Subsection title="Empty state">
<DataTable
columns={TABLE_COLUMNS}
data={[]}
loading={false}
emptyMessage="No devices found"
emptyDescription="Try adjusting your search filters or add a new device."
/>
</Subsection>
<Subsection title="Clickable rows + selected row">
<DataTable
columns={TABLE_COLUMNS}
data={TABLE_DATA}
loading={false}
onRowClick={() => {}}
selectedIds={new Set([2])}
/>
</Subsection>
<Subsection title="Custom skeleton row count (skeletonRows=3)">
<DataTable columns={TABLE_COLUMNS} data={[]} loading skeletonRows={3} />
</Subsection>
<Subsection title="Filter bar + table — standard list page pattern">
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-4)' }}>
SearchBar expands to fill remaining space. Filter selects and page-size select are fixed-width with <code>flexShrink: 0</code>. Use <code>flexWrap: 'nowrap'</code> on the row so everything stays on one line.
</p>
<div style={{ display: 'flex', flexWrap: 'nowrap', gap: 'var(--space-3)', alignItems: 'center', marginBottom: 'var(--space-4)', minWidth: 0 }}>
<div style={{ flex: '1 1 0', minWidth: 0 }}>
<SearchBar
value={filterSearch}
onChange={setFilterSearch}
placeholder="Search by name or serial…"
/>
</div>
<Select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
placeholder="All Statuses"
style={{ width: '130px', flexShrink: 0 }}
>
<option value="">All Statuses</option>
<option value="Online">Online</option>
<option value="Offline">Offline</option>
<option value="Warning">Warning</option>
</Select>
<Select
value={filterLocation}
onChange={(e) => setFilterLocation(e.target.value)}
placeholder="All Locations"
style={{ width: '130px', flexShrink: 0 }}
>
<option value="">All Locations</option>
<option value="London">London</option>
<option value="Paris">Paris</option>
<option value="Berlin">Berlin</option>
<option value="Athens">Athens</option>
</Select>
{(filterSearch || filterStatus || filterLocation) && (
<Button
variant="ghost"
size="sm"
style={{ flexShrink: 0 }}
onClick={() => { setFilterSearch(''); setFilterStatus(''); setFilterLocation('') }}
>
Clear
</Button>
)}
</div>
<DataTable
columns={TABLE_COLUMNS}
data={filteredTableData}
loading={false}
emptyMessage="No results"
emptyDescription="Try adjusting your filters."
onRowClick={() => {}}
/>
</Subsection>
</Section>
{/* ================================================================
9. ROW ACTIONS
================================================================ */}
<Section title="RowActions" id="rowactions">
<Subsection title="Default — with Edit and Delete actions">
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-3)' }}>
The locked last column of every table. Click to open the action menu. Closes on any outside click.
</p>
<div style={{ display: 'flex', gap: 'var(--space-4)', alignItems: 'center' }}>
<RowActions actions={[
{ label: 'View', icon: <Icon name="eye" size={14} />, onClick: () => {} },
{ label: 'Edit', icon: <Icon name="edit" size={14} />, color: 'var(--color-info)', onClick: () => {} },
{ label: 'Delete', icon: <Icon name="delete" size={14} />, color: 'var(--color-danger)', onClick: () => {}, divider: true },
]} />
<RowActions actions={[
{ label: 'Edit', icon: <Icon name="edit" size={14} />, color: 'var(--color-info)', onClick: () => {} },
{ label: 'Delete', icon: <Icon name="delete" size={14} />, color: 'var(--color-danger)', onClick: () => {}, divider: true },
]} />
<RowActions actions={[{ label: 'Edit', icon: <Icon name="edit" size={14} />, onClick: () => {} }]} />
<RowActions actions={[]} disabled />
</div>
</Subsection>
</Section>
{/* ================================================================
10. PAGINATION
================================================================ */}
<Section title="Pagination" id="pagination">
<Subsection title="Interactive — 243 total records, page 3 of 13">
<Pagination
page={paginationPage}
pageSize={paginationSize}
total={243}
onPageChange={setPaginationPage}
onSizeChange={setPaginationSize}
/>
</Subsection>
<Subsection title="First page (prev disabled)">
<Pagination page={1} pageSize={20} total={243} onPageChange={() => {}} />
</Subsection>
<Subsection title="Last page (next disabled)">
<Pagination page={13} pageSize={20} total={243} onPageChange={() => {}} />
</Subsection>
<Subsection title="Few pages — no ellipsis (7 pages)">
<Pagination page={4} pageSize={20} total={140} onPageChange={() => {}} />
</Subsection>
<Subsection title="No results">
<Pagination page={1} pageSize={20} total={0} onPageChange={() => {}} />
</Subsection>
<Subsection title="With per-page size selector">
<Pagination
page={paginationPage}
pageSize={paginationSize}
total={243}
onPageChange={setPaginationPage}
onSizeChange={setPaginationSize}
pageSizes={[20, 50, 100]}
/>
</Subsection>
</Section>
{/* ================================================================
10. PAGE HEADER
================================================================ */}
<Section title="PageHeader" id="pageheader">
<Subsection title="Title only">
<div style={{ backgroundColor: 'var(--color-bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--color-border)' }}>
<PageHeader title="Device Inventory" />
</div>
</Subsection>
<Subsection title="Title + subtitle">
<div style={{ backgroundColor: 'var(--color-bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--color-border)' }}>
<PageHeader title="CRM — Customers" subtitle="Manage all customer accounts and contact information." />
</div>
</Subsection>
<Subsection title="Title + subtitle + action buttons">
<div style={{ backgroundColor: 'var(--color-bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--color-border)' }}>
<PageHeader title="Firmware Manager" subtitle="Upload and manage firmware releases.">
<Button variant="secondary" size="md">Export</Button>
<Button variant="primary" size="md">Upload Firmware</Button>
</PageHeader>
</div>
</Subsection>
<Subsection title="With breadcrumbs (detail page)">
<div style={{ backgroundColor: 'var(--color-bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--color-border)' }}>
<PageHeader
title="Bell Unit Alpha"
subtitle="SN-00001 · Last seen 2 minutes ago"
breadcrumbs={[
{ label: 'Devices', href: '/devices' },
{ label: 'Bell Unit Alpha' },
]}
>
<Button variant="ghost" size="md">Edit</Button>
<Button variant="danger" size="md">Delete</Button>
</PageHeader>
</div>
</Subsection>
<Subsection title="With breadcrumbs — 3 levels">
<div style={{ backgroundColor: 'var(--color-bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--color-border)' }}>
<PageHeader
title="Edit Order #ORD-2024-0042"
breadcrumbs={[
{ label: 'CRM', href: '/crm/customers' },
{ label: 'Acme Corp', href: '/crm/customers/1' },
{ label: 'Order #ORD-2024-0042' },
]}
>
<Button variant="primary" size="md">Save Changes</Button>
</PageHeader>
</div>
</Subsection>
</Section>
{/* ================================================================
10b. PROFILE PAGE HEADER
================================================================ */}
<Section title="ProfilePageHeader" id="profileheader">
<Subsection title="Variant 1 — photo + title + subtitle (no actions)">
<div style={{ backgroundColor: 'var(--color-bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--color-border)', padding: 'var(--space-6)' }}>
<ProfilePageHeader
name="Adrian Thorne"
subtitle="Senior Systems Architect"
badge={<StatusBadge variant="success" dot={false}>Active</StatusBadge>}
/>
</div>
</Subsection>
<Subsection title="Variant 1 — with breadcrumbs">
<div style={{ backgroundColor: 'var(--color-bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--color-border)', padding: 'var(--space-6)' }}>
<ProfilePageHeader
name="Maria Santos"
subtitle="Field Technician • ID: USR-4421"
breadcrumbs={[{ label: 'App Users', href: '/users' }, { label: 'Maria Santos' }]}
badge={<StatusBadge variant="warning" dot={false}>Blocked</StatusBadge>}
/>
</div>
</Subsection>
<Subsection title="Variant 1 — with photo + photo click overlay">
<div style={{ backgroundColor: 'var(--color-bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--color-border)', padding: 'var(--space-6)' }}>
<ProfilePageHeader
name="James Okafor"
subtitle="Network Engineer • Hover over avatar to see overlay"
photoUrl="https://i.pravatar.cc/150?u=james"
onPhotoClick={() => {}}
badge={<StatusBadge variant="success" dot={false}>Active</StatusBadge>}
/>
</div>
</Subsection>
<Subsection title="Variant 2 — photo + title + subtitle + action buttons (md size)">
<div style={{ backgroundColor: 'var(--color-bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--color-border)', padding: 'var(--space-6)' }}>
<ProfilePageHeaderActions
name="Adrian Thorne"
subtitle="Senior Systems Architect • ID: USR-8829-QX"
badge={<StatusBadge variant="success" dot={false}>Active</StatusBadge>}
>
<Button variant="primary" size="md">Actions ▾</Button>
</ProfilePageHeaderActions>
</div>
</Subsection>
<Subsection title="Variant 2 — with photo">
<div style={{ backgroundColor: 'var(--color-bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--color-border)', padding: 'var(--space-6)' }}>
<ProfilePageHeaderActions
name="Elena Vasquez"
subtitle="Product Manager"
photoUrl="https://i.pravatar.cc/150?u=elena"
onPhotoClick={() => {}}
badge={<StatusBadge variant="success" dot={false}>Active</StatusBadge>}
>
<Button variant="primary" size="md">Actions ▾</Button>
</ProfilePageHeaderActions>
</div>
</Subsection>
</Section>
{/* ================================================================
11. MODAL
================================================================ */}
<Section title="Modal" id="modal">
<Subsection title="Launch modal — choose size">
<DemoRow wrap>
{['sm', 'md', 'lg', 'xl', 'xxl', 'full'].map((size) => (
<Button
key={size}
variant="secondary"
onClick={() => { setModalSize(size); setModalOpen(true) }}
>
size="{size}"
</Button>
))}
</DemoRow>
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', marginTop: 'var(--space-2)' }}>
All modals support: Escape to close, backdrop click to close, focus trap, and scroll lock.
</p>
</Subsection>
<Modal
open={modalOpen}
onClose={() => setModalOpen(false)}
title={`Modal — size="${modalSize}"`}
size={modalSize}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--space-3)' }}>
<Button variant="ghost" onClick={() => setModalOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={() => setModalOpen(false)}>Confirm</Button>
</div>
}
>
<p style={{ color: 'var(--color-text-secondary)', fontSize: 'var(--font-size-base)', marginBottom: 'var(--space-4)' }}>
This is the modal body. It can contain any content — forms, tables, confirmations.
</p>
<FormField
label="Example field inside modal"
name="sg-modal-field"
type="text"
placeholder="Type something…"
value=""
onChange={() => {}}
hint="Focus is trapped inside the modal while open."
/>
</Modal>
<Subsection title="Sizes reference">
<DemoGrid cols={3}>
<DemoCell label='size="sm" — 480px max'>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>Confirm dialogs, simple alerts</span>
</DemoCell>
<DemoCell label='size="md" — 640px max (default)'>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>Standard forms, single-step flows</span>
</DemoCell>
<DemoCell label='size="lg" — 800px max'>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>Complex forms, data selection</span>
</DemoCell>
<DemoCell label='size="xl" — 60vw × 60vh'>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>Large editors, rich content views</span>
</DemoCell>
<DemoCell label='size="xxl" — 80vw × 80vh'>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>Dashboards, preview panes</span>
</DemoCell>
<DemoCell label='size="full" — near-full viewport'>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>Wizards, multi-step flows</span>
</DemoCell>
</DemoGrid>
</Subsection>
</Section>
{/* ================================================================
12. CARD
================================================================ */}
<Section title="Card" id="card">
<Subsection title="Variants">
<DemoGrid cols={3}>
<Card variant="flat" title="Flat Card" subtitle="Default surface — bg-surface + card glow">
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>Body content goes here.</p>
</Card>
<Card variant="elevated" title="Elevated Card" subtitle="Floats above — bg-elevated + shadow-md">
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>Body content goes here.</p>
</Card>
<Card variant="outlined" title="Outlined Card" subtitle="Transparent bg + strong border">
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>Body content goes here.</p>
</Card>
</DemoGrid>
</Subsection>
<Subsection title="With footer">
<div style={{ maxWidth: '400px' }}>
<Card
variant="flat"
title="Device Alpha"
subtitle="SN-00042 · Last seen 3 min ago"
footer={
<>
<Button variant="ghost" size="sm">View</Button>
<Button variant="primary" size="sm">Configure</Button>
</>
}
>
<StatusBadge variant="success">Online</StatusBadge>
</Card>
</div>
</Subsection>
<Subsection title='padding={false} — flush body (e.g. for tables inside cards)'>
<div style={{ maxWidth: '500px' }}>
<Card variant="flat" title="Recent Orders" padding={false}>
<DataTable
columns={[
{ key: 'id', label: 'Order' },
{ key: 'status', label: 'Status', render: (r) => <StatusBadge variant={r.sv}>{r.status}</StatusBadge> },
]}
data={[
{ id: '#ORD-001', status: 'Paid', sv: 'success' },
{ id: '#ORD-002', status: 'Pending', sv: 'warning' },
{ id: '#ORD-003', status: 'Overdue', sv: 'danger' },
]}
/>
</Card>
</div>
</Subsection>
</Section>
{/* ================================================================
13. TABS
================================================================ */}
<Section title="Tabs" id="tabs">
<Subsection title='variant="line" — sliding underline (default)'>
<TabsLineDemo />
</Subsection>
<Subsection title='variant="pill" — filled pill'>
<TabsPillDemo />
</Subsection>
<Subsection title="Tabs with icons and badge counts">
<TabsIconDemo />
</Subsection>
</Section>
{/* ================================================================
14. TOAST
================================================================ */}
<Section title="Toast" id="toast">
<Subsection title="Fire toasts — all variants">
<ToastProvider>
<ToastDemo />
</ToastProvider>
</Subsection>
</Section>
{/* ================================================================
15. SEARCH BAR
================================================================ */}
<Section title="SearchBar" id="searchbar">
<Subsection title="Default — uncontrolled, 300ms debounce">
<div style={{ maxWidth: '400px' }}>
<SearchBar placeholder="Search devices…" onChange={(v) => void v} />
</div>
</Subsection>
<Subsection title="Controlled — with external value">
<SearchBarControlledDemo />
</Subsection>
<Subsection title="Disabled">
<div style={{ maxWidth: '400px' }}>
<SearchBar placeholder="Search…" disabled />
</div>
</Subsection>
</Section>
{/* ================================================================
16. HEADER SEARCH
================================================================ */}
<Section title="HeaderSearch" id="headersearch">
<Subsection title="Top-bar search — pill-shaped, no border/shadow">
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-3)' }}>
Used exclusively in the fixed top bar. Background: <code style={{ fontFamily: 'var(--font-family-mono)', color: 'var(--color-primary)' }}>--color-bg-elevated</code>.
Fully rounded (<code style={{ fontFamily: 'var(--font-family-mono)', color: 'var(--color-primary)' }}>--radius-full</code>).
No border, no shadow. Active state uses <code style={{ fontFamily: 'var(--font-family-mono)', color: 'var(--color-primary)' }}>--color-bg-island</code>, no outline.
</p>
<div style={{ width: '220px', backgroundColor: 'var(--color-bg-void)', padding: 'var(--space-3)', borderRadius: 'var(--radius-md)' }}>
<HeaderSearchDemo />
</div>
</Subsection>
</Section>
{/* ================================================================
17. BREADCRUMBS
================================================================ */}
<Section title="Breadcrumbs" id="breadcrumbs">
<Subsection title="2 levels">
<Breadcrumbs items={[{ label: 'Devices', href: '/devices' }, { label: 'Bell Unit Alpha' }]} />
</Subsection>
<Subsection title="3 levels">
<Breadcrumbs items={[
{ label: 'CRM', href: '/crm/customers' },
{ label: 'Acme Corp', href: '/crm/customers/1' },
{ label: 'Order #ORD-2024-0042' },
]} />
</Subsection>
<Subsection title="4 levels">
<Breadcrumbs items={[
{ label: 'Settings', href: '/settings' },
{ label: 'Staff', href: '/settings/staff' },
{ label: 'Jane Smith', href: '/settings/staff/42' },
{ label: 'Edit' },
]} />
</Subsection>
</Section>
{/* ================================================================
17. CONFIRM DIALOG
================================================================ */}
<Section title="ConfirmDialog" id="confirmdialog">
<Subsection title="Both variants">
<ConfirmDialogDemo />
</Subsection>
</Section>
{/* ================================================================
18. ICON
================================================================ */}
<Section title="Icon" id="icons">
<Subsection title="Inline SVG icons — all sizes">
<DemoGrid cols={4}>
{ICON_NAMES.map((name) => (
<DemoCell key={name} label={name} centered>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)' }}>
<Icon name={name} size={14} />
<Icon name={name} size={16} />
<Icon name={name} size={20} />
<Icon name={name} size={24} />
</div>
</DemoCell>
))}
</DemoGrid>
</Subsection>
<Subsection title="Colour inheritance — currentColor">
<DemoRow wrap>
{[
['var(--color-primary)', 'primary'],
['var(--color-success)', 'success'],
['var(--color-warning)', 'warning'],
['var(--color-danger)', 'danger'],
['var(--color-info)', 'info'],
['var(--color-text-muted)', 'muted'],
].map(([color, label]) => (
<div key={label} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--space-2)', color }}>
<Icon name="devices" size={20} />
<span style={{ fontSize: 'var(--font-size-xs)', fontFamily: 'var(--font-family-mono)', color: 'var(--color-text-muted)' }}>{label}</span>
</div>
))}
</DemoRow>
</Subsection>
{/* Asset SVGs from /assets/ — displayed with brightness filter for dark bg */}
{ASSET_ICON_GROUPS.map(group => (
<Subsection key={group.title} title={`Asset SVGs — ${group.title}`}>
<DemoGrid cols={5}>
{group.items.map(({ name, url }) => (
<DemoCell key={name} label={name} centered>
<img
src={url}
alt={name}
width={24}
height={24}
style={{
filter: 'brightness(0) invert(1)',
opacity: 0.8,
width: 24,
height: 24,
objectFit: 'contain',
}}
/>
</DemoCell>
))}
</DemoGrid>
</Subsection>
))}
</Section>
{/* ── Masonry Grid ────────────────────────────────────────────────── */}
<Section title="Masonry Grid Layout" id="masonry">
<Subsection title="Overview">
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', marginBottom: 'var(--space-4)', lineHeight: 'var(--line-height-base)' }}>
The default layout for all content pages with multiple variable-height sections.
Sections fill left-to-right across the top, then each new section drops into the shortest column automatically.
Use <code>masonry-grid masonry-grid--N</code> (N = 2, 3, or 4) as a wrapper inside <code>.page-wrapper</code>.
</p>
</Subsection>
<Subsection title="2 columns — masonry-grid--2">
<div className="masonry-grid masonry-grid--2" style={{ width: '100%' }}>
<Card title="Section A" subtitle="Tall section — 3 items" variant="flat">
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
{['Field one', 'Field two', 'Field three'].map(f => (
<div key={f} style={{ height: 36, borderRadius: 'var(--radius-md)', background: 'var(--color-bg-elevated)', display: 'flex', alignItems: 'center', paddingLeft: 'var(--space-3)' }}>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>{f}</span>
</div>
))}
</div>
</Card>
<Card title="Section B" subtitle="Short section — 1 item" variant="flat">
<div style={{ height: 36, borderRadius: 'var(--radius-md)', background: 'var(--color-bg-elevated)', display: 'flex', alignItems: 'center', paddingLeft: 'var(--space-3)' }}>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>Field one</span>
</div>
</Card>
<Card title="Section C" subtitle="Lands under shortest (B)" variant="flat" style={{ borderColor: 'var(--color-primary)', borderWidth: 1 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
{['Field one', 'Field two'].map(f => (
<div key={f} style={{ height: 36, borderRadius: 'var(--radius-md)', background: 'var(--color-primary-subtle)', display: 'flex', alignItems: 'center', paddingLeft: 'var(--space-3)' }}>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-accent)' }}>{f}</span>
</div>
))}
</div>
</Card>
</div>
</Subsection>
<Subsection title="3 columns — masonry-grid--3">
<div className="masonry-grid masonry-grid--3" style={{ width: '100%' }}>
{[
{ label: 'Section A', rows: 3 },
{ label: 'Section B', rows: 1 },
{ label: 'Section C', rows: 4 },
{ label: 'Section D → col 2 (shortest)', rows: 2, accent: true },
{ label: 'Section E → col 1 (next shortest)', rows: 1, accent: true },
].map(({ label, rows, accent }) => (
<Card key={label} title={label} variant={accent ? 'outlined' : 'flat'}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
{Array.from({ length: rows }).map((_, i) => (
<div key={i} style={{
height: 32, borderRadius: 'var(--radius-md)',
background: accent ? 'var(--color-primary-subtle)' : 'var(--color-bg-elevated)',
}} />
))}
</div>
</Card>
))}
</div>
</Subsection>
<Subsection title="Usage">
<pre style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', background: 'var(--color-bg-abyss)', padding: 'var(--space-4)', borderRadius: 'var(--radius-lg)', overflowX: 'auto' }}>{`// Inside .page-wrapper — sections auto-flow into shortest column
<div className="masonry-grid masonry-grid--2">
<Card title="Account Info">…</Card>
<Card title="Profile">…</Card>
<Card title="Security">…</Card> {/* drops into shortest column */}
</div>
// Available: masonry-grid--2 masonry-grid--3 masonry-grid--4
// Responsive: collapses to fewer columns at 1024px, single column at 768px`}</pre>
</Subsection>
</Section>
{/* ── DateTimePicker ─────────────────────────────────────────────── */}
<Section title="DateTimePicker" id="datetimepicker">
<Subsection title="All states">
<DateTimePickerDemo />
</Subsection>
<Subsection title="Usage">
<pre style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', background: 'var(--color-bg-abyss)', padding: 'var(--space-4)', borderRadius: 'var(--radius-lg)', overflowX: 'auto' }}>{`import DateTimePicker from '@/components/ui/DateTimePicker'
// value is an ISO string or ''; onChange receives an ISO string
<DateTimePicker
label="Occurred at"
name="occurred_at"
value={form.occurred_at}
onChange={(iso) => setForm(f => ({ ...f, occurred_at: iso }))}
/>
// With validation error
<DateTimePicker
label="Date & time"
name="occurred_at"
value={form.occurred_at}
onChange={...}
error="This field is required"
required
/>`}</pre>
</Subsection>
</Section>
{/* ── Footer ─────────────────────────────────────────────────────── */}
<div
style={{
borderTop: '1px solid var(--color-border)',
paddingTop: 'var(--space-6)',
marginTop: 'var(--space-8)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: 'var(--space-3)',
}}
>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>
BellSystems Control Panel v2 — Style Guide
</span>
<span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
/dev/styleguide
</span>
</div>
</div>
</div>
)
}