2031 lines
93 KiB
React
2031 lines
93 KiB
React
// 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 · ABCDEFGHIJKLMNOPQRSTUVWXYZ · 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 | MAC: AA:BB:CC:DD:EE:FF | 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>
|
||
)
|
||
}
|