Skip to content

LCO DMA Design System

Single source of truth for the visual design language. All tokens are defined as CSS custom properties using Tailwind CSS v4's CSS-native configuration.

Figma source: LCO-DMA Dev Design


Architecture

design-tokens.css  ← Color primitives, semantic tokens, typography, spacing (source of truth)
index.css          ← @theme inline (shadcn bridge) + @utility rules (text-lco-*) + sidebar theme
Components         ← Use Tailwind classes (never hardcoded values)

Tailwind v4: No tailwind.config.js or postcss.config.js — configuration is CSS-native via @theme inline blocks and @utility rules. The Vite plugin (@tailwindcss/vite) handles compilation. See the Tailwind v4 gotcha section below.

Rules: 1. All colors, sizes, and dimensions are CSS variables — never hardcode hex/rgb/px 2. Use semantic Tailwind classes (e.g. text-lco-heading-01) not raw values (text-sm, text-xl) 3. Use semantic color tokens (text-foreground, bg-primary) not raw colors (text-neutral-500, bg-white, bg-blue-900) 4. Use cn() from @/lib/utils for conditional class merging 5. Use shadcn/ui components from @/components/ui/ — don't rebuild primitives

Configuration Files

File Purpose
src/design-tokens.css CSS variables — color primitives, semantic tokens, typography, spacing
src/index.css shadcn bridge (@theme inline), @utility rules (text-lco-*), sidebar theme, dark mode
src/lib/utils.ts cn() utility (clsx + extendTailwindMerge — prevents text-lco-* stripping)
components.json shadcn/ui configuration
vite.config.ts @tailwindcss/vite plugin (replaces tailwind.config.js + postcss)

Font

Family: Geist by Vercel — geometric sans-serif optimized for UI.

--font-sans: 'Geist', ui-sans-serif, system-ui, -apple-system, sans-serif;

Package: geist@1.7.0 — import in main.tsx:

import 'geist/font/sans.css';

Weights used: 400 (normal), 500 (medium), 600 (semibold)


Typography Scale

Semantic tokens map design intent to sizes. Use text-lco-* classes — they include size, line-height, and default weight.

Implementation note: text-lco-* classes are defined as @utility rules in index.css (not auto-generated from @theme). This is because @theme inline in imported CSS files does not generate Tailwind v4 utilities — only the main entry file's @theme block does. When adding new typography tokens, add the CSS variable to design-tokens.css and a matching @utility rule to index.css.

Token Class Size Line-Height Weight Usage
--font-size-lco-display text-lco-display 36px 1.1 500 Hero headings (Home welcome)
--font-size-lco-heading-01 text-lco-heading-01 24px 1.2 600 Page titles
--font-size-lco-heading-02 text-lco-heading-02 20px 1.3 500 Section headings
--font-size-lco-heading-03 text-lco-heading-03 16px 1.4 600 Sub-section headings, card titles
--font-size-lco-body-01 text-lco-body-01 16px 1.5 Primary body text
--font-size-lco-body-02 text-lco-body-02 14px 1.43 Secondary body, table cell text
--font-size-lco-caption text-lco-caption 12px 1.33 Captions, badges, breadcrumbs, metadata
--font-size-lco-overline text-lco-overline 11px 1.45 500 Column headers, overline labels (tracking: 0.025em)

Tailwind v4 Gotcha

@theme inline blocks in imported CSS files (like design-tokens.css) define CSS variables but do NOT auto-generate Tailwind utility classes. Only the main entry file's @theme inline generates utilities. That's why text-lco-* classes are defined as explicit @utility rules in index.css.

When adding new design tokens: - Colors: Add --color-* to design-tokens.css @theme inline — color utilities ARE generated because they're bridged through index.css @theme inline via var() references - Typography: Add --font-size-* to design-tokens.css, then add a matching @utility rule in index.css. Also add the new class name to the extendTailwindMerge font-size list in lib/utils.ts so cn() doesn't strip it. - Spacing/layout: Add to index.css @theme inline directly (e.g., --spacing-header)

When to Override Weight

The text-lco-* classes set a default weight, but you can override with font-normal, font-medium, font-semibold:

<h1 className="text-lco-display font-medium">Welcome back, User</h1>
<p className="text-lco-body-02 font-normal text-muted-foreground">Description text</p>
<th className="text-lco-overline font-medium uppercase text-muted-foreground">Column</th>

Do NOT add leading-* classes alongside text-lco-* — line-height is already baked in.

Migration from Raw Tailwind Classes

Old (raw Tailwind) New (semantic) Context
text-2xl font-semibold text-lco-heading-01 Page titles (24px)
text-xl font-semibold or text-lg font-semibold text-lco-heading-02 Section headings (20px)
text-base font-semibold text-lco-heading-03 Card/subsection titles (16px)
text-base text-lco-body-01 Primary body text (16px)
text-sm text-lco-body-02 General body text, table cells (14px)
text-xs text-lco-caption Metadata, timestamps, badges (12px)
text-xs font-medium uppercase text-lco-overline Table column headers (11px)

Color System

Semantic Colors (via CSS Variables)

These are the primary colors used across the app. All defined in index.css, consumed via Tailwind.

Token Tailwind Hex (Light) Usage
--foreground text-foreground #151619 Primary text (lco-gray-900)
--muted-foreground text-muted-foreground #86878B Secondary text (lco-gray-400)
--primary bg-primary, text-primary #0f4380 Brand blue — CTAs, links
--primary-foreground text-primary-foreground #FFFFFF Text on primary bg
--destructive bg-destructive #DA1E28 Delete, danger actions
--border border-border #D7D7D8 Standard borders (lco-gray-200)
--muted bg-muted #F9F9FA Subtle backgrounds (lco-gray-50)
--accent bg-accent #F9F9FA Hover/focus backgrounds (lco-gray-50)
--card bg-card #FFFFFF Card backgrounds
--secondary bg-secondary #F9F9FA Secondary backgrounds (lco-gray-50)

LCO Color Palettes

Palette Key shades Usage
lco-primary 50–900 (mid: #0f4380) Brand blue, CTAs, links
lco-gray 25–900 (warmer custom gray) All UI chrome via semantic tokens — replaces Tailwind neutral
lco-slate 50–900 (mid: #2F4054) Sidebar dark theme
lco-blue-accent 25–900 (mid: #5A8DE0) Charts, info states, secondary blue
lco-yellow-accent 25–900 (mid: #D9AF3A) Charts, warm accents

Note: lco-gray is a warmer custom gray that replaces Tailwind's neutral for semantic tokens. Status palettes (red, green, amber, blue) still use Tailwind built-ins.

Status Colors (Direct Tailwind)

Status Background Text Border
Active bg-green-50 text-green-700 border-green-200
Inactive bg-yellow-50 text-yellow-900 border-yellow-500
Archived bg-neutral-100 text-neutral-600 border-neutral-300
Error bg-red-50 text-red-700 border-red-200
Warning bg-yellow-50 text-yellow-900 border-yellow-500
Info bg-blue-50 text-blue-700 border-blue-200

Color Migration — Use Semantic Tokens

Prefer semantic tokens over raw Tailwind colors. These adapt to dark mode and theme changes:

Old (raw) New (semantic) Why
text-neutral-950 text-foreground Primary text
text-neutral-700, text-neutral-600 text-muted-foreground Secondary text
text-neutral-500 text-muted-foreground Muted text, breadcrumbs
bg-white bg-card or bg-background Card / page backgrounds
bg-neutral-100 bg-muted Page backgrounds, subtle fills
bg-neutral-50 bg-accent Table headers, hover states
border-neutral-200 border-border Standard borders
bg-blue-900, bg-blue-800 bg-primary, hover:bg-primary/90 Brand CTA buttons
text-white (on brand bg) text-primary-foreground Text on primary bg

Layout Dimensions

CSS variables defined in index.css @theme inline:

Token Tailwind Value Usage
--spacing-header h-header 48px Top header bar height
--spacing-sidebar-collapsed w-sidebar-collapsed 56px Sidebar icon-only width
--spacing-sidebar-expanded w-sidebar-expanded 256px Sidebar full width

Spacing Scale

Standard Tailwind spacing. Key values from the Figma:

Tailwind px Figma Variable Usage
p-1 4px spacing/1 Tight gaps
p-2 8px spacing/2 Icon padding, compact spacing
p-3 12px spacing/3 Sidebar item gaps
p-4 16px spacing/4 Standard padding, header px
p-6 24px spacing/6 Primary containers, page padding
p-9 36px spacing/9 Large section spacing
gap-2 8px Compact element spacing
gap-4 16px Form fields, button groups
gap-6 24px Card grids, major sections

Border Radius

Base --radius: 0.625rem (10px). All computed from this value:

Tailwind Value Usage
rounded-sm 6px Buttons, breadcrumb items, inputs
rounded-md 8px Logo wrapper, cards
rounded-lg 10px Cards, sidebar items
rounded-xl 14px Large cards, modals
rounded-full 9999px Avatars, badges, pill tabs

Shadow

Name Value Usage
shadow-xs 0 1px 2px #0000000d Subtle card elevation
shadow-sm Default Tailwind Standard card shadow

Two sidebar themes via CSS class on the sidebar root:

Default (Light)

White background, dark text. No extra class needed.

Slate (Dark)

Apply .sidebar-slate class to the sidebar container:

<Sidebar className="sidebar-slate" collapsible="icon">

This overrides all --sidebar-* variables to the dark navy palette.


Shell Layout

The app uses a consistent shell: Sidebar + Header + Page Content.

┌─────┬────────────────────────────────────┐
│     │  Header (48px) — breadcrumbs + utils│
│ S   ├────────────────────────────────────┤
│ i   │                                    │
│ d   │  Page Header — title, desc, actions │
│ e   │                                    │
│ b   │  Content Area                      │
│ a   │                                    │
│ r   │                                    │
│     │                                    │
└─────┴────────────────────────────────────┘

Page Structure Pattern

<div className="flex flex-col h-full">
  <PageHeader title="Page Title" description="Description" actions={...} />
  <div className="flex-1 bg-muted p-6">
    {/* Content */}
  </div>
</div>

Component Patterns

Buttons

All variants use semantic tokens — never hardcode bg-blue-* or text-white in button styling.

Variant When Example
default Main page action ("Create Project") <Button>Create</Button>
primary Equivalent to default — brand blue CTA <Button variant="primary">Save</Button>
outline Secondary actions ("Export", "Cancel") <Button variant="outline">Export</Button>
outlineSecondary Subtle secondary with shadow <Button variant="outlineSecondary">Manage</Button>
ghost Icon buttons, tertiary actions <Button variant="ghost" size="icon"><X /></Button>
destructive Delete, dangerous actions <Button variant="destructive">Delete</Button>

Button sizes (default = h-8, compact):

Size Height Use
default / sm 32px (h-8) Standard buttons
xs 24px (h-6) Compact UI, inline
md 40px (h-10) Prominent CTAs, forms
lg 48px (h-12) Hero actions
icon / icon-sm 32px Icon buttons
icon-xs 24px Compact icon buttons
icon-md 40px Prominent icon buttons
icon-lg 48px Large icon buttons

Cards

Three Figma card layouts:

Layout Usage Pattern
Default (horizontal) Compact clickable row Icon + title + description + arrow
Vertical Feature cards with link Icon + title + desc + action link
Vertical 2 Universal Tables hub Icon + title + desc + button

Tabs

Two tab styles:

Variant Tailwind Usage
Underline Default shadcn Tabs Page-level navigation (PageHeader tabs)
Pill Rounded bg on active Section filtering (Home "Recent Activity" / "Bookmarks")

Data Tables

Tables use TanStack Table v8 with two toolbar variants:

Variant Controls Usage
Extensive Select view, Filter, Sort, Group by, Hide, overflow, Search Detail pages (Crew Members, etc.)
Basic Filter, Sort, Hide, Search List pages (Projects, Clients)

Bulk Actions Bar: Blue bar (bg-lco-primary-400) at top of table when rows selected. Row Overflow Menu: 3-dot menu on each row (right side).

Table Footers / Totals Rows

Decision Rule (LCO-247 / LCO-256, stable token LCO-256-C3)

  • Has in-table totals row → add a Total Man-Hours sum cell to that footer row (use the canonical hyphenated label Total Man-Hours, matching the totalManhours column header). Sum must recompute on filter/sort (client-side only; no backend changes).
  • No in-table totals row → no-op. Document the decision in the audit table below and in a JSDoc marker on the relevant column-config file (search the codebase for token LCO-256-C3 to find all such markers).

Audit Results — May 2026

Surface Table tech In-table totals row? Verdict
Current Budget items tab VirtualizedDataTable No — rollups live in KPI header cards DEFER — adding a footer requires PM UX decision (LCO-247 out of scope); rollup already in KPI cards
Current Budget "By Package" tab shadcn Table + manual summary card No — manual rollup card sits below the table DEFER — sibling summary card already serves as the rollup; no in-table totals row to attach to
Control Budget items tab VirtualizedDataTable No — rollups live in KPI header cards DEFER — same as Current Budget items tab
Control Budget "By Package" tab shadcn Table + manual summary card No — manual rollup card sits below the table DEFER — same as Current Budget By-Package
MTO Discipline Sheets VirtualizedDataTable No — virtualized; no <TableFooter> plumbing exists DEFER — adding requires VirtualizedDataTable footer support (separate ticket)
Estimation Master VirtualizedDataTable No DEFER — same as MTO discipline sheets
ECR Budget Variance Table (BudgetVarianceTable.tsx) shadcn Table Verify on next PM review — currently no <TableFooter> DEFER — not in LCO-247 cluster scope; flag for future audit if a totals row is added
Labor Distribution Report (LaborDistributionReportPage.tsxLaborDistributionTable.tsx) shadcn Table, prop totalManhours already passed from page No DEFER — report-style page; rollup is conveyed via the totalManhours prop displayed in the report header, not via a table footer
Labor Distribution Chart (LaborDistributionChart.tsx) chart, not a table N/A NOT-A-TABLE — out of scope for table-footer pattern
S-Curve Phase Calculate Card (PhaseCalculateCard.tsx) summary card with inline <strong>Total Man-Hours:</strong> N/A NOT-A-TABLE — already surfaces total man-hours in a labelled summary line
Labor Settings Tab (LaborSettingsTab.tsx) formula-doc helper with inline <strong>Total Man-Hours:</strong> N/A NOT-A-TABLE — documentation helper, not a data table
Discipline Validation Panel (DisciplineValidationPanel.tsx) shadcn Table with <TableFooter> for validation.totalObsHours (OBS hours, not man-hours) Footer exists, but for a non-man-hour metric N/A — surface does not expose man-hours; out of contract scope

When to Revisit

Revisit this decision when PM confirms that any of the above views should display in-table aggregate totals. At that point:

  1. Wire getFooterGroups() consumption into VirtualizedDataTable (currently absent).
  2. Add a footer callback to the totalManhours column def on the target surface.
  3. For non-virtualized shadcn tables (Current/Control Budget "By Package", ECR Budget Variance), sum via table.getFilteredRowModel().rows. For VirtualizedDataTable, source the sum from the page's already-maintained post-filter-sort array (not from the virtualized DOM slice). See behavior contract C-256-3.1.

Badges

Badges are pill-shaped (rounded-full) with 6 variants: default, secondary, destructive, outline, ghost, link.

Status badges use the outline variant with direct Tailwind colors for data states:

<Badge variant="outline" className="border-green-500 text-green-700">Active</Badge>
<Badge variant="outline" className="border-yellow-500 text-yellow-800">Inactive</Badge>
<Badge variant="outline" className="border-neutral-300 text-neutral-600">Archived</Badge>

Form Patterns

  • react-hook-form + zod for validation
  • Schemas in frontend/src/schemas/
  • Use <Form> / <FormField> from @/components/ui/form
  • Layout: space-y-4 for field stacking, grid grid-cols-2 gap-4 for multi-column

Input Styling

All inputs inherit from shadcn — h-10 rounded-md border-input text-sm.

Combobox Variants

Component Height Usage
Combobox h-10 Standard searchable dropdown
CellCombobox h-8 Table cell editing
FilterCombobox h-6 Table header filters
COACombobox Configurable Server-side search with infinite scroll

Loading & Feedback

Toast Notifications

Variant Usage
default Success confirmations
destructive Error messages
warning Warnings (yellow)

Loading States

<Loader2 className="h-4 w-4 animate-spin" />        // Inline
<Loader2 className="h-8 w-8 animate-spin text-primary" /> // Full page

Skeleton Loaders

<Skeleton className="h-4 w-[250px]" />

Icons

Library: Lucide React (lucide-react)

Context Size
Standard UI (buttons, nav) h-4 w-4 (16px)
Small indicators h-3 w-3
Feature icons (cards) h-5 w-5
Large (empty states) h-6 w-6
Loading spinners h-8 w-8

Z-Index Reference

Element z-index
Sidebar z-10
Header z-20
Modals / Dialogs z-50
Floating action bar z-50
Popovers / Tooltips z-50

Quick Reference: Adding New UI

  1. Use semantic text-lco-* classes — not raw text-xl
  2. Use bg-primary / text-primary — not hardcoded #0f4380
  3. Use h-header / w-sidebar-collapsed for layout dimensions
  4. Use cn() for conditional classes — never string concatenation
  5. Import shadcn components from @/components/ui/
  6. Check this doc + Figma before creating new visual patterns