628 lines
29 KiB
Markdown
628 lines
29 KiB
Markdown
# DESIGN SYSTEM — BellSystems Control Panel v2
|
||
|
||
> Single source of truth for all UI/UX decisions.
|
||
> Read before writing any page, component, or modal.
|
||
> Never override these rules inline. Change the rule here first, then propagate.
|
||
>
|
||
> Live reference: `/dev/styleguide` — every component, every variant, every state.
|
||
|
||
---
|
||
|
||
## 1. Core Philosophy
|
||
|
||
- **Consistency over creativity.** Every page must feel like it belongs to the same product.
|
||
- **Tokens over hardcoded values.** Never write a raw color, spacing value, or font size. Always `var(--token)`.
|
||
- **Components over repetition.** If you write the same pattern twice, it becomes a shared component.
|
||
- **Page layout is global.** All pages share the same padding and spacing anchors. Content always starts at the same position.
|
||
- **Accessible by default.** ARIA labels, keyboard navigation, visible focus states on all interactive elements.
|
||
|
||
---
|
||
|
||
## 2. Page Layout Anatomy
|
||
|
||
Every authenticated page lives inside `MainLayout`, which provides:
|
||
|
||
```
|
||
┌─────────────┬────────────────────────────────────────────┐
|
||
│ │ HEADER (height: 56px / --header-height) │
|
||
│ │────────────────────────────────────────────┤
|
||
│ │ │
|
||
│ SIDEBAR │ CONTENT AREA │
|
||
│ (224px / │ ┌──────────────────────────────────────┐ │
|
||
│ --sidebar- │ │ .page-wrapper │ │
|
||
│ width) │ │ padding: 48px (--space-12) │ │
|
||
│ │ │ gap: 24px between sections │ │
|
||
│ │ │ │ │
|
||
│ │ │ <PageHeader> ← always first │ │
|
||
│ │ │ <content...> │ │
|
||
│ │ └──────────────────────────────────────┘ │
|
||
└─────────────┴────────────────────────────────────────────┘
|
||
```
|
||
|
||
### The consistency guarantee
|
||
|
||
`.page-wrapper` is defined once in `global.css`. It is the only wrapper used on every page:
|
||
|
||
```css
|
||
.page-wrapper {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: var(--space-12); /* 48px — desktop */
|
||
gap: var(--space-6); /* 24px between direct children */
|
||
min-width: 0;
|
||
}
|
||
/* Mobile: padding drops to --space-8 (32px), gap to --space-4 (16px) */
|
||
```
|
||
|
||
**Rules:**
|
||
- Every page's root element is `<div className="page-wrapper">` — no exceptions
|
||
- Never add extra padding, margin, or wrapper divs that shift content relative to `.page-wrapper`
|
||
- Never override `.page-wrapper` padding per-page
|
||
- The `<PageHeader>` is always the first child inside `.page-wrapper`
|
||
- This ensures that on every page, the title starts at exactly 48px from the top-left corner of the content area
|
||
|
||
### Content width modes
|
||
|
||
Every page falls into one of two modes:
|
||
|
||
#### 1. Full-width (default)
|
||
Content expands to fill the entire available area. Use this for all data-heavy pages: lists, tables, dashboards, detail views.
|
||
|
||
```jsx
|
||
<div className="page-wrapper">
|
||
{/* content fills the full content area */}
|
||
</div>
|
||
```
|
||
|
||
#### 2. Centered (narrow content)
|
||
For pages with a small number of elements that would look lost spanning the full viewport — e.g. settings forms, auth pages, single-entity configuration screens.
|
||
|
||
```jsx
|
||
<div className="page-wrapper page-wrapper--centered">
|
||
{/* every direct child is capped at --content-max-width-sm (640px) and centered */}
|
||
</div>
|
||
```
|
||
|
||
Default max-width is `--content-max-width-sm` (640px). Override per-page only when necessary:
|
||
|
||
```jsx
|
||
<div
|
||
className="page-wrapper page-wrapper--centered"
|
||
style={{ '--page-content-max-width': 'var(--content-max-width-md)' }}
|
||
>
|
||
```
|
||
|
||
Available width tokens:
|
||
|
||
| Token | Value | Use |
|
||
|-----------------------------|--------|-----------------------------------------|
|
||
| `--content-max-width-xs` | 480px | Tiny forms, login, auth |
|
||
| `--content-max-width-sm` | 640px | Small forms, simple settings (default) |
|
||
| `--content-max-width-md` | 800px | Medium forms, detail-light pages |
|
||
| `--content-max-width-lg` | 1024px | Moderate-width constrained pages |
|
||
|
||
**Rules:**
|
||
- Never use `page-wrapper--centered` on a list page, data table page, or any page where content should grow with the viewport
|
||
- Never hardcode a pixel `max-width` in a page file — always use a token
|
||
|
||
---
|
||
|
||
## 3. Color Tokens
|
||
|
||
All colors are CSS custom properties defined in `frontend/src/styles/tokens.css`.
|
||
The system is **dark-first**: `:root` = dark theme. `[data-theme="light"]` overrides exist as a placeholder.
|
||
|
||
### Rule: never write a raw color value in any component or page file. Always `var(--token)`.
|
||
|
||
### Background Surfaces (7-step tonal ladder)
|
||
|
||
| Token | Value | Use |
|
||
|------------------------|--------------|--------------------------------------------------------|
|
||
| `--color-bg-abyss` | `#0a0e14` | Deepest well: code blocks, input backgrounds |
|
||
| `--color-bg-base` | `#10141a` | Page background (viewport fill) |
|
||
| `--color-bg-void` | `#181c22` | Sidebar, header |
|
||
| `--color-bg-surface` | `#1c2026` | Default card / panel background |
|
||
| `--color-bg-elevated` | `#262a31` | Raised cards, hovered rows, dropdowns |
|
||
| `--color-bg-island` | `#31353c` | Active states, selected rows, pressed buttons |
|
||
| `--color-bg-float` | `rgba(53,57,64,0.80)` | Glassmorphism: modals, floating panels |
|
||
|
||
### Brand / Primary (Indigo Glow)
|
||
|
||
| Token | Value | Use |
|
||
|----------------------------|------------------------------|--------------------------------------|
|
||
| `--color-primary` | `#c0c1ff` | CTAs, active nav, key accent |
|
||
| `--color-primary-hover` | `#d2bbff` | Hover, gradient endpoint |
|
||
| `--color-primary-container`| `#8083ff` | Container fills |
|
||
| `--color-primary-subtle` | `rgba(128,131,255,0.12)` | Hover backgrounds, tinted areas |
|
||
| `--gradient-primary` | `linear-gradient(135deg, #c0c1ff, #d2bbff)` | Primary button fill |
|
||
|
||
### Semantic / State Colors
|
||
|
||
| Token | Value | Use |
|
||
|------------------------|---------------------------|------------------------------------------------|
|
||
| `--color-success` | `#4ade80` | Online, active, confirmed |
|
||
| `--color-success-bg` | `rgba(74,222,128,0.12)` | Success badge / button resting background |
|
||
| `--color-warning` | `#fbbf24` | Pending, needs attention |
|
||
| `--color-warning-bg` | `rgba(251,191,36,0.12)` | Warning badge / button resting background |
|
||
| `--color-danger` | `#ff5c5c` | Error text, destructive actions |
|
||
| `--color-danger-bg` | `rgba(255,92,92,0.12)` | Danger badge / button resting background |
|
||
| `--color-info` | `#7bd0ff` | Informational, aqua-sky accent |
|
||
| `--color-info-bg` | `rgba(123,208,255,0.12)` | Info badge background |
|
||
|
||
### Text Colors (4-step hierarchy)
|
||
|
||
| Token | Value | Use |
|
||
|---------------------------|-------------|---------------------------------------------------|
|
||
| `--color-text-primary` | `#dfe2eb` | Body copy, data values, headings |
|
||
| `--color-text-secondary` | `#c7c4d7` | Labels, metadata, inactive nav |
|
||
| `--color-text-muted` | `#908fa0` | Placeholders, disabled, category headers |
|
||
| `--color-text-inverse` | `#10141a` | Text on primary/accent backgrounds (dark on light)|
|
||
| `--color-text-accent` | `#c0c1ff` | Active nav items, links |
|
||
|
||
### Borders
|
||
|
||
| Token | Value | Use |
|
||
|-------------------------|----------------------------|--------------------------------------------|
|
||
| `--color-border` | `rgba(70,69,84,0.20)` | Resting inputs, card outlines |
|
||
| `--color-border-strong` | `rgba(70,69,84,0.45)` | Secondary buttons, stronger dividers |
|
||
| `--color-border-focus` | `rgba(192,193,255,0.40)` | Focus ring halo on inputs |
|
||
|
||
---
|
||
|
||
## 4. Typography
|
||
|
||
Two-font system. Three families total.
|
||
|
||
### Font Families
|
||
|
||
| Token | Font | Role |
|
||
|--------------------------|---------------------|---------------------------------------------------|
|
||
| `--font-family-display` | `Barlow Condensed` | H1, H2, page titles, modal titles |
|
||
| `--font-family-base` | `Onest` | All UI text, body, labels, buttons, table rows |
|
||
| `--font-family-mono` | `JetBrains Mono` | Serial numbers, IDs, code, API keys, terminal |
|
||
|
||
**Why this pairing:**
|
||
- `Barlow Condensed` has an industrial/engineering quality — feels like instrument panel labelling. Makes page titles immediately distinctive.
|
||
- `Onest` is a Ukrainian geometric grotesque with slightly unusual proportions and excellent numerics. Clean at 14px. Not the overused Inter/Space Grotesk.
|
||
- `JetBrains Mono` is the standard for developer-facing data.
|
||
|
||
### Font Sizes
|
||
|
||
| Token | Value | Use |
|
||
|--------------------|------------|----------------------------------------------|
|
||
| `--font-size-xs` | `0.6875rem` (11px) | Labels, sidebar category headers, chips |
|
||
| `--font-size-sm` | `0.75rem` (12px) | Captions, helper text, table headers |
|
||
| `--font-size-base` | `0.875rem` (14px) | Body text, table rows (default) |
|
||
| `--font-size-md` | `1rem` (16px) | Card titles, module headers |
|
||
| `--font-size-lg` | `1.125rem` (18px) | Section subheadings |
|
||
| `--font-size-xl` | `1.5rem` (24px) | Page headings (h1/h2) |
|
||
| `--font-size-2xl` | `3.5rem` (56px) | Hero KPI numbers, dashboard metrics |
|
||
|
||
### Font Weights
|
||
|
||
| Token | Value | Use |
|
||
|---------------------------|-------|----------------------------------------|
|
||
| `--font-weight-normal` | 400 | Body copy |
|
||
| `--font-weight-medium` | 500 | Emphasized body, table values |
|
||
| `--font-weight-semibold` | 600 | Headings, button labels, field labels |
|
||
| `--font-weight-bold` | 700 | Strong emphasis, hero metrics |
|
||
|
||
### Typography Usage Rules
|
||
|
||
- **Page titles (`<PageHeader>`):** `Barlow Condensed`, `1.75rem`, weight 600 (handled by `.v2-page-header-title`)
|
||
- **Modal titles:** `Barlow Condensed`, `1.125rem`, weight 600 (handled by `.v2-modal-title`)
|
||
- **H1, H2 globally:** `Barlow Condensed`, `var(--font-size-xl)`, weight 600 — set in `global.css`
|
||
- **H3–H6:** `Onest` (body font), normal heading weights
|
||
- **Card titles:** `Onest`, `--font-size-base`, weight 600
|
||
- **Table headers:** `Onest`, `--font-size-sm`, weight 600, uppercase, `--tracking-wide`
|
||
- **Body / cell text:** `Onest`, `--font-size-base`, weight 400
|
||
- **Muted / helper text:** `Onest`, `--font-size-sm`, `--color-text-muted`
|
||
- **Serials, IDs, codes:** `JetBrains Mono`, `--font-size-sm`
|
||
|
||
### Letter Spacing
|
||
|
||
| Token | Value | Use |
|
||
|---------------------|-----------|--------------------------------------------|
|
||
| `--tracking-normal` | `0em` | Default |
|
||
| `--tracking-tight` | `-0.01em` | Barlow Condensed headings |
|
||
| `--tracking-wide` | `0.08em` | Uppercase labels, sidebar category headers |
|
||
| `--tracking-display`| `-0.02em` | Hero KPI numbers at 56px |
|
||
|
||
---
|
||
|
||
## 4b. Date, Time & Currency Formatting
|
||
|
||
All dates use **Greek/European style** (day-first). Never use US-style MM/DD/YYYY anywhere in the app.
|
||
|
||
All formatting is centralized in `frontend/src/lib/formatters.js`. Never use raw `toLocaleDateString()`, `Intl.DateTimeFormat`, `toLocaleString()`, or `toISOString().slice()` in pages or modals — always import from `@/lib/formatters`.
|
||
|
||
### Available formatters
|
||
|
||
| Function | Output example | Use for |
|
||
|---------------------|------------------------------------|--------------------------------------|
|
||
| `fmtDate` | `05/03/2026` | Short numeric dates (tables, lists) |
|
||
| `fmtDateMedium` | `5 Mar 2026` | Medium dates (cards, details) |
|
||
| `fmtDateLong` | `5 March 2026` | Long dates (headings, summaries) |
|
||
| `fmtDateFull` | `Wednesday, 5 March 2026` | Dashboard, full context |
|
||
| `fmtDateTime` | `5 March 2026, 2:30 pm` | Date + 12h time |
|
||
| `fmtDateTimeMedium` | `5 Mar 2026, 14:30` | Date + 24h time (compact) |
|
||
| `fmtDateTimeFull` | `Wed, 5 Mar 2026, 2:30 pm` | Emails, comms |
|
||
| `fmtRelative` | `5 minutes ago` | Relative timestamps |
|
||
| `fmtEuro` | `1.250,00 €` | Euro currency (Greek locale) |
|
||
|
||
### Form input helpers
|
||
|
||
| Function | Output example | Use for |
|
||
|---------------------|-------------------------|--------------------------------------------------|
|
||
| `toDatetimeLocal` | `2026-03-05T14:30` | Populating `datetime-local` inputs (local time) |
|
||
| `nowLocal` | `2026-03-05T14:30` | Current time for form defaults |
|
||
| `toDateInput` | `2026-03-05` | Populating `date` inputs |
|
||
|
||
### Critical rule: no `toISOString().slice()` for form inputs
|
||
|
||
`toISOString()` converts to **UTC**, which shifts the time by the user's timezone offset (e.g. 3 hours for Greece). Always use `toDatetimeLocal()` or `nowLocal()` instead.
|
||
|
||
---
|
||
|
||
## 5. Spacing System
|
||
|
||
4px base unit. All spacing must use tokens — no arbitrary pixel values.
|
||
|
||
| Token | Value | Common use |
|
||
|--------------|--------|--------------------------------------------------|
|
||
| `--space-1` | 4px | Tight gaps, icon padding |
|
||
| `--space-2` | 8px | Between label and input, inline gaps |
|
||
| `--space-3` | 12px | Table cell padding, compact button padding |
|
||
| `--space-4` | 16px | Between form fields, mobile page padding |
|
||
| `--space-5` | 20px | Tab item spacing |
|
||
| `--space-6` | 24px | **Page padding**, card padding, section gap |
|
||
| `--space-8` | 32px | Between major sections |
|
||
| `--space-10` | 40px | Large section gap |
|
||
| `--space-12` | 48px | Extra large spacing |
|
||
| `--space-16` | 64px | Maximum spacing, hero sections |
|
||
|
||
---
|
||
|
||
## 6. Border Radius & Shadows
|
||
|
||
### Border Radius
|
||
|
||
| Token | Value | Use |
|
||
|----------------|----------|-------------------------------------------|
|
||
| `--radius-sm` | 4px | Tags, small chips, select option rows |
|
||
| `--radius-md` | 6px | Buttons, inputs, table badges |
|
||
| `--radius-lg` | 8px | Cards, panels, dropdown menus |
|
||
| `--radius-xl` | 12px | Modals, large containers |
|
||
| `--radius-full`| 9999px | Status badge pills, avatars |
|
||
|
||
### Shadows
|
||
|
||
| Token | Value | Use |
|
||
|-------------------------|------------------------------------------|------------------------------------|
|
||
| `--shadow-card` | `inset 0 1px 0 rgba(192,193,255,0.05)` | Card top-edge glass reflection |
|
||
| `--shadow-sm` | `0 2px 8px rgba(10,14,20,0.40)` | Subtle lift |
|
||
| `--shadow-md` | `0 4px 16px rgba(10,14,20,0.50)` | Elevated cards |
|
||
| `--shadow-lg` | `0 8px 24px rgba(13,17,23,0.60)` | Modals, dropdowns |
|
||
| `--shadow-focus` | `0 0 0 3px rgba(192,193,255,0.20)` | Focus ring glow |
|
||
| `--shadow-primary-glow` | `0 4px 16px rgba(192,193,255,0.28)` | Primary button hover halo |
|
||
| `--shadow-danger-glow` | `0 4px 16px rgba(255,92,92,0.40)` | Danger button hover halo |
|
||
| `--shadow-success-glow` | `0 4px 16px rgba(74,222,128,0.35)` | Success button hover halo |
|
||
|
||
---
|
||
|
||
## 7. Component Rules
|
||
|
||
### Button
|
||
|
||
Import: `@/components/ui/Button`
|
||
|
||
**Variants:**
|
||
|
||
| Variant | Resting state | Hover state |
|
||
|------------------|---------------------------------------|----------------------------------------------------------|
|
||
| `primary` | Indigo→violet gradient, dark text | `+brightness(1.06)` + `--shadow-primary-glow` halo |
|
||
| `secondary` | Island bg, ghost border | Elevated bg + focus border + subtle indigo glow |
|
||
| `ghost` | Transparent | Elevated bg + whisper indigo glow |
|
||
| `danger` | Coral tint bg, coral text | **Solid coral fill**, dark text + `--shadow-danger-glow` |
|
||
| `success` | Emerald tint bg, emerald text | **Solid emerald fill**, dark text + `--shadow-success-glow` |
|
||
| `table-actions` | Fully transparent, muted text | Island bg + strong border (identical to `secondary`) — also activates on `tr:hover` |
|
||
|
||
**Sizes:** `sm`, `md` (default), `lg`
|
||
|
||
**Rules:**
|
||
- Never use a raw `<button>` element for a styled action
|
||
- Always pass `loading` prop for async actions (shows spinner, disables interaction)
|
||
- Icon-only buttons must have `aria-label`
|
||
- Active/press state: `filter: brightness(0.94)`, glow removed
|
||
|
||
---
|
||
|
||
### FormField
|
||
|
||
Import: `@/components/ui/FormField`
|
||
|
||
Wraps every form control: label + input/textarea/select + hint + error message.
|
||
Never place a raw `<input>` on a page.
|
||
|
||
**Types:** `text`, `email`, `password`, `number`, `tel`, `url`, `textarea`, `select`
|
||
|
||
**For `type="select"`**: pass `<option>` elements as children. FormField uses the custom `Select` component internally — the native `<select>` is never rendered.
|
||
|
||
```jsx
|
||
<FormField label="Status" name="status" type="select" value={val} onChange={handleChange}>
|
||
<option value="">Choose…</option>
|
||
<option value="active">Active</option>
|
||
<option value="inactive">Inactive</option>
|
||
</FormField>
|
||
```
|
||
|
||
**Input appearance:** "cutout" inset-shadow treatment — the field appears recessed into the surface. Background: `--color-bg-abyss`. Focus: `--color-border-focus` ring.
|
||
|
||
---
|
||
|
||
### Select (standalone)
|
||
|
||
Import: `@/components/ui/Select`
|
||
|
||
Fully custom dropdown replacing native `<select>`. Floating menu via portal, keyboard navigation, checkmark on selected item. Usually consumed via `FormField type="select"`. Use directly when you need a select outside a form label context.
|
||
|
||
---
|
||
|
||
### DataTable
|
||
|
||
Import: `@/components/ui/DataTable`
|
||
|
||
**Always include:** column headers, loading skeleton, empty state, pagination.
|
||
**Rows:** alternate tint via `--color-tint-row` (`rgba(192,193,255,0.015)`). Hover: `--color-bg-island`.
|
||
**Status columns:** always `<StatusBadge>` — never raw text.
|
||
**Row actions:** last column, right-aligned, use portal-based action menu.
|
||
|
||
---
|
||
|
||
### Modal
|
||
|
||
Import: `@/components/ui/Modal`
|
||
|
||
Sizes: `sm` (480px), `md` (640px — default), `lg` (800px), `xl` (60vw/60vh), `xxl` (85vw/85vh), `full` (calc(100vw/100vh − 64px)).
|
||
|
||
**Rules:**
|
||
- Always has: title, close (×) button, footer action buttons
|
||
- Closes on Escape + backdrop click unless `persistent={true}`
|
||
- Destructive prompts use `<ConfirmDialog>` instead
|
||
- Modal JSX never lives inside a page file — always in `frontend/src/modals/[domain]/`
|
||
|
||
---
|
||
|
||
### ConfirmDialog
|
||
|
||
Import: `@/components/ui/ConfirmDialog`
|
||
|
||
Wraps `<Modal size="sm">` with a centred icon + message. Use for any action that is destructive or hard to reverse.
|
||
|
||
Variants: `danger` (coral circle + triangle icon), `primary` (indigo circle + info icon).
|
||
|
||
---
|
||
|
||
### PageHeader
|
||
|
||
Import: `@/components/ui/PageHeader`
|
||
|
||
**Always the first element inside `.page-wrapper`.** Creates the page title block.
|
||
|
||
Props: `title` (required), `subtitle`, `breadcrumbs`, `children` (action buttons slot).
|
||
|
||
The title renders as `<h1>` with class `.v2-page-header-title` — uses `Barlow Condensed` at `1.75rem` / weight 600.
|
||
|
||
```jsx
|
||
<PageHeader title="Device Inventory" subtitle="All registered Bell units">
|
||
<Button variant="primary">Add Device</Button>
|
||
</PageHeader>
|
||
```
|
||
|
||
---
|
||
|
||
### Card
|
||
|
||
Import: `@/components/ui/Card`
|
||
|
||
Variants: `flat` (default), `elevated`, `outlined`.
|
||
|
||
Props: `title`, `subtitle`, `footer`, `padding` (bool, default true), `children`.
|
||
|
||
Card header has a faint indigo gradient ceiling (`linear-gradient` from top).
|
||
|
||
---
|
||
|
||
### Tabs
|
||
|
||
Import: `@/components/ui/Tabs`
|
||
|
||
Variants: `line` (default — underline indicator), `pill` (filled background).
|
||
|
||
Props: `tabs` (array of `{key, label, icon?, count?}`), `active`, `onChange`, `variant`.
|
||
|
||
Line variant uses a sliding indicator measured with `useLayoutEffect`. Pill variant uses filled backgrounds.
|
||
|
||
Spacing: line tabs have `gap: --space-5` between items, pill tabs `gap: --space-4`.
|
||
|
||
---
|
||
|
||
### Toast
|
||
|
||
Import: `@/components/ui/Toast` → `{ ToastProvider, useToast }`
|
||
|
||
Setup: wrap the app (or router) with `<ToastProvider>`. Then in any component:
|
||
|
||
```jsx
|
||
const toast = useToast()
|
||
toast.success('Saved', 'Device updated successfully.')
|
||
toast.danger('Error', 'Failed to connect.')
|
||
toast.warning('Warning', 'Firmware is outdated.')
|
||
toast.info('Info', 'Sync in progress.')
|
||
```
|
||
|
||
Toasts auto-dismiss after 4000ms. Hover pauses the timer. Stack appears in the bottom-right corner.
|
||
|
||
---
|
||
|
||
### SearchBar
|
||
|
||
Import: `@/components/ui/SearchBar`
|
||
|
||
Supports controlled (`value` + `onChange`) or uncontrolled mode.
|
||
Debounced by default (300ms). Clear button appears when text is present.
|
||
Appearance matches the `FormField` cutout treatment.
|
||
|
||
---
|
||
|
||
### Breadcrumbs
|
||
|
||
Import: `@/components/ui/Breadcrumbs`
|
||
|
||
Use on detail pages only (not list pages). Items: array of `{ label, href? }`. Last item has no href — it is the current page.
|
||
|
||
---
|
||
|
||
### Spinner
|
||
|
||
Import: `@/components/ui/Spinner`
|
||
|
||
Props: `size` (`sm`, `md`, `lg`), `color` (defaults to `--color-primary`).
|
||
|
||
Use inside loading states. Buttons show their own spinner via `loading` prop — do not add a separate `<Spinner>` inside buttons.
|
||
|
||
---
|
||
|
||
### StatusBadge
|
||
|
||
Import: `@/components/ui/StatusBadge`
|
||
|
||
Never use a raw `<span>` with a background color for status. Always `<StatusBadge>`.
|
||
|
||
Variants: `success`, `warning`, `danger`, `info`, `neutral`.
|
||
|
||
---
|
||
|
||
### Icon
|
||
|
||
Import: `@/components/ui/Icon`
|
||
|
||
Renders an inline SVG by name. 35 named icons available (see Style Guide `/dev/styleguide` → Icon section for the full list).
|
||
|
||
```jsx
|
||
<Icon name="edit" size={16} />
|
||
<Icon name="delete" size={20} color="var(--color-danger)" />
|
||
```
|
||
|
||
**Asset SVGs** (from `/assets/` folders) are displayed via `<img>` tags in the Style Guide, not via `<Icon>`. These are pre-rendered SVG files used for sidebar icons, comms icons, customer status icons, etc. Use them as image sources, not as Icon component names.
|
||
|
||
---
|
||
|
||
## 8. Icons
|
||
|
||
Three sources:
|
||
|
||
| Source | Use case | How to render |
|
||
|-------------------------------------|---------------------------------------|-----------------------------|
|
||
| `<Icon name="..." />` | Action icons, UI chrome | `@/components/ui/Icon` |
|
||
| `assets/side-menu-icons/*.svg` | Sidebar navigation | `<img src={...} />` |
|
||
| `assets/comms/*.svg` | Communication type indicators | `<img src={...} />` |
|
||
| `assets/customer-status/*.svg` | CRM status icons | `<img src={...} />` |
|
||
| `assets/global-icons/*.svg` | Legacy action icons (prefer `<Icon>`) | `<img src={...} />` |
|
||
| `assets/other-icons/*.svg` | Misc UI icons | `<img src={...} />` |
|
||
|
||
Never add a new icon library (e.g. heroicons, lucide). Use the existing sources.
|
||
|
||
---
|
||
|
||
## 9. Theming Rules
|
||
|
||
- Theme is controlled by `data-theme` attribute on `<html>`
|
||
- Default is dark (`:root` = dark theme)
|
||
- **Never** use Tailwind's `dark:` prefix — theming is handled entirely via CSS tokens
|
||
- `[data-theme="light"]` overrides exist in `tokens.css` as a future placeholder
|
||
|
||
---
|
||
|
||
## 10. Responsive Breakpoints
|
||
|
||
| Token | Value | Behaviour |
|
||
|--------------------|--------|--------------------------------------------------------|
|
||
| `--breakpoint-sm` | 640px | |
|
||
| `--breakpoint-md` | 768px | Sidebar collapses; page padding drops to `--space-4` |
|
||
| `--breakpoint-lg` | 1024px | Full sidebar shown |
|
||
| `--breakpoint-xl` | 1280px | |
|
||
|
||
Mobile (`< 768px`): single column, sidebar hidden (drawer), tables may become card lists.
|
||
|
||
---
|
||
|
||
## 11. Section Layout — Masonry Grid
|
||
|
||
**Default layout for ALL content pages with multiple variable-height sections.**
|
||
|
||
Sections on a content page must flow like physical objects stacked in columns — the next section always drops into the shortest column. This is CSS column masonry.
|
||
|
||
### How it works
|
||
|
||
```
|
||
Column 1 | Column 2 | Column 3
|
||
────────────┼─────────────┼────────────
|
||
Section A | Section B | Section C
|
||
(300px) | (250px) | (350px)
|
||
│ │
|
||
Section E | Section D |
|
||
(200px) | (200px) |
|
||
```
|
||
|
||
Sections fill left-to-right across the top, then each new section drops into whichever column is currently shortest. This is automatic — the browser handles placement via CSS `columns`.
|
||
|
||
### Usage
|
||
|
||
```jsx
|
||
{/* 2 columns */}
|
||
<div className="masonry-grid masonry-grid--2">
|
||
<Card title="Account Info">…</Card>
|
||
<Card title="Profile">…</Card>
|
||
<Card title="Security">…</Card> {/* auto-drops into shortest column */}
|
||
</div>
|
||
|
||
{/* 3 columns */}
|
||
<div className="masonry-grid masonry-grid--3">
|
||
{sections.map(s => <Card key={s.id}>…</Card>)}
|
||
</div>
|
||
```
|
||
|
||
Available variants: `masonry-grid--2`, `masonry-grid--3`, `masonry-grid--4`
|
||
|
||
Responsive behaviour:
|
||
- `--4` collapses to 3 cols at 1024px, 1 col at 768px
|
||
- `--3` collapses to 2 cols at 1024px, 1 col at 768px
|
||
- `--2` collapses to 1 col at 768px
|
||
|
||
### Rules
|
||
|
||
- **Use `.masonry-grid` by default** on all content pages with 2+ variable-height sections
|
||
- **Do NOT** use `display: grid` with `gridTemplateColumns` for variable-height card layouts — this creates uneven whitespace when cards differ in height
|
||
- **Do NOT** use `.masonry-grid` for DataTable pages — tables span full width on their own
|
||
- **Do NOT** use `.masonry-grid` when sections must align horizontally (e.g. two fields that are semantically paired side-by-side within a card) — that's an internal card layout, not page-level masonry
|
||
- The `Card` component already has `break-inside: avoid` so it will never be split across columns
|
||
|
||
---
|
||
|
||
## 12. What Claude Code Must NEVER Do
|
||
|
||
- ❌ Write a hex color, `rgb()`, or `hsl()` value directly in any component or page file
|
||
- ❌ Write a pixel spacing or size value that isn't a `--space-*` token
|
||
- ❌ Use a raw `<button>`, `<input>`, or `<select>` for anything styled — always use the wrapper component
|
||
- ❌ Create a `.module.css` or any per-page CSS file
|
||
- ❌ Use Tailwind's `dark:` prefix — theming is via CSS tokens only
|
||
- ❌ Place modal JSX inside a page file — modals live in `frontend/src/modals/`
|
||
- ❌ Wrap `.page-wrapper` in additional divs that shift content alignment
|
||
- ❌ Override `.page-wrapper`'s padding to make a single page "different"
|
||
- ❌ Skip loading, error, and empty states on any data-fetching component
|
||
- ❌ Import from `_archive/` anywhere except `@/lib/api.js`, `@/hooks/useAuth.js`, and `@/providers/AuthProvider.jsx`
|
||
- ❌ Install a new icon library or introduce new SVG icons outside of `assets/`
|
||
- ❌ Invent new color values not in `tokens.css`
|