# 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 │ │ │ │ │ │ │ │ │ │ ← always first │ │ │ │ │ │ │ │ │ └──────────────────────────────────────┘ │ └─────────────┴────────────────────────────────────────────┘ ``` ### 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 `
` — 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 `` 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
{/* content fills the full content area */}
``` #### 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
{/* every direct child is capped at --content-max-width-sm (640px) and centered */}
``` Default max-width is `--content-max-width-sm` (640px). Override per-page only when necessary: ```jsx
``` 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 (``):** `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 ` ``` --- ### 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 ``. 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 `` inside buttons. --- ### StatusBadge Import: `@/components/ui/StatusBadge` Never use a raw `` with a background color for status. Always ``. 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 ``` **Asset SVGs** (from `/assets/` folders) are displayed via `` tags in the Style Guide, not via ``. 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 | |-------------------------------------|---------------------------------------|-----------------------------| | `` | Action icons, UI chrome | `@/components/ui/Icon` | | `assets/side-menu-icons/*.svg` | Sidebar navigation | `` | | `assets/comms/*.svg` | Communication type indicators | `` | | `assets/customer-status/*.svg` | CRM status icons | `` | | `assets/global-icons/*.svg` | Legacy action icons (prefer ``) | `` | | `assets/other-icons/*.svg` | Misc UI icons | `` | 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 `` - 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 */}
{/* auto-drops into shortest column */}
{/* 3 columns */}
{sections.map(s => )}
``` 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 `