Files
pulse/frontend-modern/DESIGN_SYSTEM.md
T
rcourtman 9e7a5ffdeb Move frontend to Tailwind CSS 4 and jscpd 5 to drop braces
GHSA-vfj7-8cjw-p6xm affects every braces release and has no fix, so the
required frontend audit failed with seven high findings on any pull
request that changes the dependency graph. Tailwind CSS 3 (chokidar,
fast-glob, micromatch) and jscpd 4 (@jscpd/finder, fast-glob) were the
only paths in, both dev-only, and no override can remove braces from
fast-glob or micromatch. Tailwind CSS 4.3.3 through @tailwindcss/vite and
jscpd 5.4.0 leave none; npm audit over the full graph reports zero.

The stylesheet keeps the v3 rendering. It pins the v3 palette (inlined,
so opacity modifiers compile to static colors), font stacks and line
heights, keeps v3's cascade order by placing former component and
unlayered rules around the generated utilities, and restores v3 hover on
touch devices, gray-200 default borders, the 3px blue ring, placeholder
color, button cursor, table-cell padding and the space-* and divide-*
child selectors. The 900/950 shades Pulse overrode with 25% alpha are
opaque now and their callers say /25, because v4 multiplies an opacity
modifier into a translucent color instead of replacing it. Opacity
modifiers on var() colors that v3 never generated are removed.

Utility order changed from lexical to numeric, so a few caller overrides
that silently lost in v3 would start to win. Touch targets keep their
authored 44px phone size (the Machines row stays 32px); cosmetic
overrides v3 never applied are removed, and overrides it did apply use
the important suffix.

Contract-Neutral: Tailwind CSS 4 migration for GHSA-vfj7-8cjw-p6xm: utility class renames and stylesheet compatibility, no subsystem contract change
2026-10-03 15:19:35 +01:00

7.6 KiB

Pulse Modern Design System 🎨

This document outlines the standard UI primitives, tokens, and components that conform to Pulse's single-source-of-truth semantic design spec.

Goal: All new pages, panels, toggles, buttons, and text should map exactly to these shared specifications instead of relying on manually typing raw Tailwind color configs (like bg-gray-800 or #1a1c23).

Core Principles

  1. Never hardcode hex values or use static gray / white / slate labels for structural layout colors.
  2. Use the Semantic Tokens. These are declared as Tailwind @theme colors in index.css and resolve through its CSS variables to flawlessly support light/dark transitions without needing a literal dark: prefix in the layout classes.

Enforcement

  • npm run lint is a hard gate for design-system regressions in src/components and src/pages.
  • npm run lint:theme is a hard gate for theme governance across the whole frontend (src/** + index.html).
  • npm run lint:headers audits header composition and page-level header usage policy.
  • Routed surfaces must use shared header primitives (PageHeader, SectionHeader, SettingsPanel, OperationsPanel) instead of raw <h1> markup.
  • Visible form labels must either use for with a matching control ID or wrap a native labelable control; use a heading, paragraph, or span for non-control captions.
  • Shared form controls must keep a stable accessible name as their value changes; a placeholder is input guidance, not a label.
  • Compact pointer targets must remain at least 24 by 24 CSS pixels, including icon and copy actions, per WCAG 2.2 Target Size (Minimum).
  • Product motion must honor the operating-system prefers-reduced-motion setting. The global stylesheet completes animations and transitions immediately under that preference; do not override that safeguard or make motion the only indication of state.
  • Only explicit non-visual route wrappers are exempt from header primitive requirements.
  • Theme ownership policy:
    • Only src/utils/theme.ts and index.html may read/write theme keys (pulseThemePreference, darkMode, pulse_dark_mode) or toggle the root dark class.
    • Full-screen shells (min-h-screen) must use semantic backgrounds (bg-base, bg-surface, bg-surface-alt) instead of hardcoded palettes.
  • CI must fail on:
    • orphaned utility prefixes (for example dark:hover: or group-hover: with no class after them)
    • dark:bg-white / dark:hover:bg-white
    • low-contrast class combinations like bg-base + text-white
    • theme owner violations or non-semantic full-screen backgrounds reported by lint:theme
  • If a UI requires exceptional styling, prefer adding a semantic token or shared component rather than bypassing the rule.

🏗 Tokens

Use standard Tailwind prefixes with these dynamic keys:

Token Utility Example Represents When to Use
base bg-base Application background The absolute bottom layer of the page, or inactive tracker backgrounds.
surface bg-surface Elevated structural elements Cards, panels, dialog boxes, standard button backgrounds, table headers.
surface-hover hover:bg-surface-hover Interactive state When a table row, button, or list item is hovered or active.
surface-alt bg-surface-alt Slightly elevated surface Selected states, inline badges, secondary panels, form group backgrounds.
border-subtle border-border-subtle Faint divider Very subtle dividers that don't need sharp contrast.
border border-border Standard structure borders Outline for panels, buttons, dialog boxes, and inputs.
base-content text-base-content Primary readable text The main text color instead of absolute black/white.
muted text-muted De-emphasized text Secondary tags, descriptions, disabled labels, or table column headers.

Example of what NOT to do:

// ❌ BAD
<div class="bg-white dark:bg-[#1a1c23] border-gray-200 dark:border-gray-800 text-gray-900 border" />

Example of the spec:

// ✅ GOOD
<div class="bg-surface border-border text-base-content border" />

🧩 CSS Component Classes

Reusable component classes defined in index.css using @layer base. Use these instead of repeating the same token combinations:

Class What it provides Use for
.card bg-surface border border-border shadow-sm rounded-md Content panels, dialog boxes
.form-input Full input styling (border, bg, text, focus ring, disabled state) Text inputs, textareas
.form-select Same as .form-input + dropdown arrow padding Select dropdowns
.table-row border-b border-border hover:bg-surface-hover Table body rows
// ❌ BAD — repeating tokens manually
<input class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-base-content focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500" />

// ✅ GOOD — use the component class
<input class="form-input" />

🧩 Standard Components

Before building a native toggle or stylized button from scratch, heavily favor the standardized layout elements contained in /src/components/shared/.

1. Card (/src/components/shared/Card.tsx)

The absolute core container for content across Pulse. Automatically maps to bg-surface and standard borders/shadows/padding.

import { Card } from '@/components/shared/Card';

<Card hoverable border padding="md">
  Main metric panel content!
</Card>

2. Button (/src/components/shared/Button.tsx)

A unified standard button built on semantic tokens instead of inline styling. Variants: primary | secondary (default) | danger | ghost | outline

import { Button } from '@/components/shared/Button';

<Button variant="primary" size="md"> Save Settings </Button>
<Button variant="secondary" isLoading> Saving... </Button>

3. Toggle (/src/components/shared/Toggle.tsx)

Dynamic switch mapped to base, surface, and blue.

import { Toggle } from '@/components/shared/Toggle';

<Toggle checked={true} label="Enable Discovery" description="Scans network elements"/>

4. FilterButtonGroup (/src/components/shared/FilterButtonGroup.tsx)

Segmented radio-style navigation controls. Uses muted for inactive buttons, and maps the active card to a highlighted surface.

import { FilterButtonGroup } from '@/components/shared/FilterButtonGroup';

<FilterButtonGroup 
  value={currentView()} 
  onChange={setView} 
  options={[
    { value: 'pve', label: 'Virtual Environment' },
    { value: 'pbs', label: 'Backup Server' }
  ]} 
/>

5. Table primitives (/src/components/shared/Table.tsx)

Wraps the raw HTML table structure with strict structural tags dynamically handling their hover states, borders, font weights, and tracking properties.

import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/shared/Table';

<Table>
  <TableHeader>
    <TableRow>
      <TableHead>Status</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>Online</TableCell>
    </TableRow>
  </TableBody>
</Table>

6. Form utilities (/src/components/shared/Form.ts)

Pre-composed class strings for form elements. Import individual exports instead of composing tokens manually.

import { formControl, formSelect, formLabel, formField, formCheckbox } from '@/components/shared/Form';

<div class={formField}>
  <label class={formLabel}>API Key</label>
  <input class={formControl} type="text" />
</div>