-
Notifications
You must be signed in to change notification settings - Fork 0
Style Guide
This page is the canonical reference for the Cornerstone design system. It documents every design token, component pattern, and visual convention in use. All new UI work must follow these specifications.
Managed by: ux-designer agent
Token source: client/src/styles/tokens.css
- Design Token Architecture
- Color Palette (Layer 1)
- Semantic Tokens (Layer 2)
- Shadow Scale
- Typography
- Spacing
- Border Radius
- Transitions
- Z-Index Scale
- Component Patterns
- Dark Mode Implementation Guide
- Brand Identity
Cornerstone uses a 3-layer token system defined in client/src/styles/tokens.css.
Layer 1 — Palette Raw named color values (hex) — source of truth
↓
Layer 2 — Semantic Purpose-driven aliases using var() references to Layer 1
↓
Layer 3 — Dark mode Scoped overrides on [data-theme="dark"] — replaces Layer 2 values only
Rules:
-
Always reference Layer 2 semantic tokens in component CSS Modules (
var(--color-bg-primary)) - Never use raw hex values in
.module.cssfiles — zero exceptions - Only use Layer 1 palette tokens directly when no semantic alias covers the use case
- Layer 3 requires no changes to component code — theming is automatic via CSS cascade
Verification command — must return zero results:
grep -rn '#[0-9a-fA-F]' client/src --include="*.module.css"These are raw color values. Reference these only through Layer 2 semantic tokens in component CSS.
| Token | Hex | Usage |
|---|---|---|
--color-white |
#ffffff |
Pure white |
--color-black |
#000000 |
Pure black |
--color-gray-50 |
#f9fafb |
Near-white backgrounds |
--color-gray-100 |
#f3f4f6 |
Subtle backgrounds, tertiary surfaces |
--color-gray-200 |
#e5e7eb |
Default borders |
--color-gray-300 |
#d1d5db |
Strong borders, dividers |
--color-gray-400 |
#9ca3af |
Placeholder text, disabled text |
--color-gray-500 |
#6b7280 |
Muted text |
--color-gray-600 |
#4b5563 |
Subtle text |
--color-gray-700 |
#374151 |
Secondary text, sidebar hover |
--color-gray-800 |
#1f2937 |
Sidebar background (light mode), inverse bg |
--color-gray-900 |
#111827 |
Primary text, darkest surface |
| Token | Hex | Usage |
|---|---|---|
--color-blue-100 |
#dbeafe |
Primary background tint |
--color-blue-200 |
#bfdbfe |
Primary background tint (hover) |
--color-blue-400 |
#60a5fa |
Sidebar focus ring |
--color-blue-500 |
#3b82f6 |
Primary action, focus border, favicon |
--color-blue-600 |
#2563eb |
Primary hover |
--color-blue-700 |
#1d4ed8 |
Primary active |
--color-blue-800 |
#1e40af |
Badge text on light bg |
| Token | Hex | Usage |
|---|---|---|
--color-red-50 |
#fef2f2 |
Danger background (very light) |
--color-red-100 |
#fee2e2 |
Danger background, blocked badge bg |
--color-red-200 |
#fecaca |
Danger border |
--color-red-400 |
#ef4444 |
Danger input border |
--color-red-500 |
#dc2626 |
Danger action |
--color-red-600 |
#b91c1c |
Danger hover |
--color-red-700 |
#991b1b |
Danger active, badge text |
Note: these values map to a mix of Tailwind's green and emerald scales.
| Token | Hex | Usage |
|---|---|---|
--color-green-50 |
#f0fdf4 |
Success background (very light) |
--color-green-100 |
#d1fae5 |
Emerald-100; completed badge bg, success badge bg |
--color-green-150 |
#dcfce7 |
Green-100 (non-standard step); user active badge bg |
--color-green-200 |
#bbf7d0 |
Success border |
--color-green-500 |
#10b981 |
Success action |
--color-green-600 |
#059669 |
Success hover |
--color-green-700 |
#166534 |
Success text on light bg, user active badge text |
--color-green-900 |
#065f46 |
Completed badge text |
Reference these tokens in all component CSS. They automatically adapt to dark mode via Layer 3 overrides.
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-bg-primary |
#ffffff (white) |
#1a1a2e |
Main page background |
--color-bg-secondary |
#f9fafb (gray-50) |
#16213e |
Sidebar of cards, secondary panels |
--color-bg-tertiary |
#f3f4f6 (gray-100) |
#1e293b |
Code blocks, inset regions |
--color-bg-inverse |
#1f2937 (gray-800) |
#f3f4f6 (gray-100) |
Tooltips, dark callouts |
--color-bg-hover |
#f9fafb (gray-50) |
#1e293b |
Row/item hover state |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-text-primary |
#111827 (gray-900) |
#f1f5f9 |
Headings, prominent labels |
--color-text-secondary |
#374151 (gray-700) |
#cbd5e1 |
Sub-headings, labels |
--color-text-body |
#374151 (gray-700) |
#94a3b8 |
Body copy, descriptions |
--color-text-muted |
#6b7280 (gray-500) |
#64748b |
Helper text, metadata |
--color-text-subtle |
#4b5563 (gray-600) |
#94a3b8 |
Less-prominent secondary copy |
--color-text-inverse |
#ffffff (white) |
#111827 (gray-900) |
Text on dark/inverse surfaces |
--color-text-placeholder |
#9ca3af (gray-400) |
#475569 |
Form input placeholders |
--color-text-disabled |
#9ca3af (gray-400) |
#475569 |
Disabled form elements |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-border |
#e5e7eb (gray-200) |
#334155 |
Default borders, dividers |
--color-border-strong |
#d1d5db (gray-300) |
#475569 |
Emphasized borders, table lines |
--color-border-focus |
#3b82f6 (blue-500) |
#60a5fa (blue-400) |
Focused input border color |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-primary |
#3b82f6 |
#60a5fa |
Button background, links |
--color-primary-hover |
#2563eb |
#3b82f6 |
Button hover state |
--color-primary-active |
#1d4ed8 |
#2563eb |
Button active/pressed |
--color-primary-text |
#ffffff |
#111827 |
Text on primary button |
--color-primary-bg |
#dbeafe |
rgba(59,130,246,0.15) |
Subtle primary tint (tags, etc.) |
--color-primary-bg-hover |
#bfdbfe |
rgba(59,130,246,0.25) |
Subtle primary tint hover |
--color-primary-badge-text |
#1e40af |
#93c5fd |
Text inside primary-tinted badges |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-danger |
#dc2626 |
#f87171 |
Destructive button, error icon |
--color-danger-hover |
#b91c1c |
#ef4444 |
Destructive button hover |
--color-danger-active |
#991b1b |
#dc2626 |
Destructive button pressed |
--color-danger-text |
#ffffff |
#111827 |
Text on danger button |
--color-danger-bg |
#fef2f2 |
rgba(239,68,68,0.1) |
Error banner background |
--color-danger-bg-strong |
#fee2e2 |
rgba(239,68,68,0.2) |
Error badge background |
--color-danger-border |
#fecaca |
rgba(239,68,68,0.3) |
Error banner border |
--color-danger-text-on-light |
#991b1b |
#fca5a5 |
Error text on light bg |
--color-danger-input-border |
#ef4444 |
#ef4444 |
Invalid input border |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-success |
#10b981 |
#34d399 |
Success icon, checkmark |
--color-success-hover |
#059669 |
#10b981 |
Success button hover |
--color-success-bg |
#f0fdf4 |
rgba(16,185,129,0.1) |
Success banner background |
--color-success-border |
#bbf7d0 |
rgba(16,185,129,0.3) |
Success banner border |
--color-success-text-on-light |
#166534 |
#6ee7b7 |
Success text on light bg |
--color-success-badge-bg |
#d1fae5 |
rgba(16,185,129,0.15) |
Success badge background |
--color-success-badge-bg-alt |
#dcfce7 |
rgba(16,185,129,0.2) |
Alternate success badge background |
--color-success-badge-text |
#065f46 |
#a7f3d0 |
Success badge text |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-sidebar-bg |
#1f2937 (gray-800) |
#0f172a |
Sidebar panel background |
--color-sidebar-text |
#f9fafb (gray-50) |
#e2e8f0 |
Sidebar text and icons |
--color-sidebar-hover |
#374151 (gray-700) |
#1e293b |
Nav item hover background |
--color-sidebar-active |
#3b82f6 (blue-500) |
#3b82f6 |
Active nav item background |
--color-sidebar-focus-ring |
#60a5fa (blue-400) |
#60a5fa |
Keyboard focus ring in sidebar |
--color-sidebar-separator |
#374151 (gray-700) |
#334155 |
Horizontal rule in sidebar |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-focus-ring |
rgba(59,130,246,0.3) |
rgba(96,165,250,0.4) |
Standard blue focus ring color |
--color-focus-ring-subtle |
rgba(59,130,246,0.1) |
rgba(96,165,250,0.15) |
Subtle focus ring |
--color-focus-ring-danger |
rgba(220,38,38,0.1) |
rgba(239,68,68,0.15) |
Focus ring on danger element |
--color-focus-ring-primary-alt |
rgba(37,99,235,0.1) |
rgba(96,165,250,0.15) |
Focus ring on alternate primary |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-overlay |
rgba(0,0,0,0.5) |
rgba(0,0,0,0.7) |
Modal backdrop |
--color-overlay-light |
rgba(0,0,0,0.1) |
rgba(0,0,0,0.3) |
Hover overlay on images |
--color-overlay-medium |
rgba(0,0,0,0.15) |
rgba(0,0,0,0.4) |
Medium-weight overlay |
--color-overlay-danger |
rgba(153,27,27,0.1) |
rgba(239,68,68,0.15) |
Danger action overlay |
--color-overlay-delete |
rgba(220,38,38,0.1) |
rgba(239,68,68,0.15) |
Delete confirmation overlay |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-status-not-started-bg |
#e5e7eb (gray-200) |
#334155 |
Not Started badge background |
--color-status-not-started-text |
#374151 (gray-700) |
#94a3b8 |
Not Started badge text |
--color-status-in-progress-bg |
#dbeafe (blue-100) |
rgba(59,130,246,0.2) |
In Progress badge background |
--color-status-in-progress-text |
#1e40af (blue-800) |
#93c5fd |
In Progress badge text |
--color-status-completed-bg |
#d1fae5 (green-100) |
rgba(16,185,129,0.15) |
Completed badge background |
--color-status-completed-text |
#065f46 (green-900) |
#a7f3d0 |
Completed badge text |
--color-status-blocked-bg |
#fee2e2 (red-100) |
rgba(239,68,68,0.15) |
Blocked badge background |
--color-status-blocked-text |
#991b1b (red-700) |
#fca5a5 |
Blocked badge text |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-role-admin-bg |
#dbeafe (blue-100) |
rgba(59,130,246,0.2) |
Admin role badge background |
--color-role-admin-text |
#1e40af (blue-800) |
#93c5fd |
Admin role badge text |
--color-role-member-bg |
#f3f4f6 (gray-100) |
#334155 |
Member role badge background |
--color-role-member-text |
#374151 (gray-700) |
#94a3b8 |
Member role badge text |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-user-active-bg |
#dcfce7 (green-150) |
rgba(16,185,129,0.15) |
Active user badge background |
--color-user-active-text |
#166534 (green-700) |
#6ee7b7 |
Active user badge text |
--color-user-inactive-bg |
#fee2e2 (red-100) |
rgba(239,68,68,0.15) |
Inactive user badge background |
--color-user-inactive-text |
#991b1b (red-700) |
#fca5a5 |
Inactive user badge text |
Shadows automatically deepen in dark mode via Layer 3 overrides.
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--shadow-sm |
0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06) |
0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2) |
Cards, small surfaces |
--shadow-md |
0 4px 6px rgba(0,0,0,0.1) |
0 4px 6px rgba(0,0,0,0.3) |
Raised elements |
--shadow-lg |
0 10px 15px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05) |
0 10px 15px rgba(0,0,0,0.3), 0 4px 6px rgba(0,0,0,0.2) |
Dropdowns, popovers |
--shadow-xl |
0 20px 25px rgba(0,0,0,0.15) |
0 20px 25px rgba(0,0,0,0.4) |
Modals |
--shadow-xl-strong |
0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04) |
0 20px 25px -5px rgba(0,0,0,0.3), 0 10px 10px -5px rgba(0,0,0,0.2) |
Large modals |
--shadow-2xl |
0 20px 25px rgba(0,0,0,0.2) |
0 20px 25px rgba(0,0,0,0.5) |
Full-page overlays |
--shadow-inset-deep |
0 10px 25px rgba(0,0,0,0.2) |
0 10px 25px rgba(0,0,0,0.4) |
Sidebar depth, inset panels |
--shadow-kbd |
0 1px 0 rgba(0,0,0,0.05) |
0 1px 0 rgba(255,255,255,0.1) |
Keyboard shortcut keys |
--shadow-focus |
0 0 0 3px rgba(59,130,246,0.3) |
0 0 0 3px rgba(96,165,250,0.4) |
Standard focus ring |
--shadow-focus-subtle |
0 0 0 3px rgba(59,130,246,0.1) |
0 0 0 3px rgba(96,165,250,0.15) |
Subtle focus ring |
--shadow-focus-primary-alt |
0 0 0 3px rgba(37,99,235,0.1) |
0 0 0 3px rgba(96,165,250,0.15) |
Alternate primary focus ring |
--shadow-focus-danger |
0 0 0 3px rgba(220,38,38,0.1) |
0 0 0 3px rgba(239,68,68,0.15) |
Focus ring on danger element |
The application uses the system-ui font stack — no web font loading, no layout shift:
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Helvetica Neue',
sans-serif;This is set globally in client/src/styles/index.css and should not be overridden per-component.
| Token | Value | Pixels | Usage |
|---|---|---|---|
--font-size-2xs |
0.8125rem |
13px | Between xs and sm — fine print, dense metadata |
--font-size-xs |
0.75rem |
12px | Micro labels, timestamps |
--font-size-sm |
0.875rem |
14px | Form labels, secondary text, badges |
--font-size-base |
1rem |
16px | Body text, inputs |
--font-size-lg |
1.125rem |
18px | Slightly emphasized text |
--font-size-xl |
1.25rem |
20px | Card titles, section headings |
--font-size-2xl |
1.5rem |
24px | Page sub-headings |
--font-size-3xl |
1.875rem |
30px | Major page headings |
--font-size-4xl |
2rem |
32px | Hero headings, large dashboard numbers |
| Token | Value | Usage |
|---|---|---|
--font-weight-normal |
400 |
Body text |
--font-weight-medium |
500 |
Labels, metadata with slight emphasis |
--font-weight-semibold |
600 |
Section headings, button text |
--font-weight-bold |
700 |
Page titles, badges, strong emphasis |
All spacing tokens follow a 4px base grid. Use these tokens for padding, margin, and gap.
| Token | Value | Pixels | Common Usage |
|---|---|---|---|
--spacing-0 |
0 |
0 | Reset |
--spacing-px |
1px |
1px | Hairline borders, fine adjustments |
--spacing-0-5 |
0.125rem |
2px | Micro gaps |
--spacing-1 |
0.25rem |
4px | Icon padding, tight inline spacing |
--spacing-1-5 |
0.375rem |
6px | Dense list items |
--spacing-2 |
0.5rem |
8px | Default inner padding small elements |
--spacing-2-5 |
0.625rem |
10px | Medium-small gap |
--spacing-3 |
0.75rem |
12px | Badge padding, compact list items |
--spacing-4 |
1rem |
16px | Standard inner padding (cards, inputs) |
--spacing-5 |
1.25rem |
20px | Section spacing |
--spacing-6 |
1.5rem |
24px | Card gap, form field spacing |
--spacing-7 |
1.75rem |
28px | Wide gap |
--spacing-8 |
2rem |
32px | Major section padding |
--spacing-10 |
2.5rem |
40px | Large section separation |
--spacing-12 |
3rem |
48px | Page-level vertical rhythm |
--spacing-16 |
4rem |
64px | Hero-level spacing |
| Token | Value | Pixels | Usage |
|---|---|---|---|
--radius-sm |
0.25rem |
4px | Small elements: badges, keyboard keys |
--radius-md |
0.375rem |
6px | Inputs, buttons, dropdowns, popovers |
--radius-lg |
0.5rem |
8px | Cards, modals, dialog sections |
--radius-circle |
50% |
-- | Circular elements (avatar, icon button) |
--radius-full |
9999px |
-- | Pills: tags, status badges |
| Token | Value | Usage |
|---|---|---|
--transition-fast |
0.1s ease |
Opacity fade (sidebar close button) |
--transition-normal |
0.15s ease |
Most interactive elements (buttons, inputs) |
--transition-medium |
0.2s ease |
Hover effects on larger elements |
--transition-slow |
0.3s ease |
Sidebar slide, modals |
Pre-built multi-property transitions for consistency across components:
| Token | Value | Usage |
|---|---|---|
--transition-input |
border-color 0.15s ease, box-shadow 0.15s ease |
Form input focus transitions |
--transition-button |
background-color 0.15s ease, box-shadow 0.15s ease |
Button background + shadow |
--transition-button-border |
background-color 0.15s ease, border-color 0.15s ease |
Outline button hover |
| Token | Value | Usage |
|---|---|---|
--z-dropdown |
10 |
Dropdown menus, date pickers |
--z-overlay |
50 |
Backdrop overlays (below sidebar) |
--z-sidebar |
100 |
Mobile sidebar panel |
--z-modal |
1000 |
Modal dialogs (above everything) |
Always reference Layer 2 semantic tokens with var():
/* CORRECT */
.card {
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: var(--spacing-6);
color: var(--color-text-body);
}
/* WRONG — hardcoded values break dark mode */
.card {
background: #ffffff;
border: 1px solid #e5e7eb;
}- Always prefer semantic tokens — they adapt automatically to dark mode
- Use palette tokens directly only when creating a new component that genuinely needs a specific hue not yet covered by a semantic alias (e.g., a new badge type)
- If you find yourself using palette tokens repeatedly for the same purpose, add a semantic alias to
tokens.cssat all 3 layers
Use the --color-status-* token family for work item status indicators. Apply --radius-full for pill shape:
.badge {
display: inline-flex;
align-items: center;
padding: var(--spacing-1) var(--spacing-3);
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
}
.notStarted {
background: var(--color-status-not-started-bg);
color: var(--color-status-not-started-text);
}
.inProgress {
background: var(--color-status-in-progress-bg);
color: var(--color-status-in-progress-text);
}
.completed {
background: var(--color-status-completed-bg);
color: var(--color-status-completed-text);
}
.blocked {
background: var(--color-status-blocked-bg);
color: var(--color-status-blocked-text);
}Standard input with focus ring and error state:
.input {
width: 100%;
padding: var(--spacing-2) var(--spacing-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: var(--font-size-base);
transition: var(--transition-input);
}
.input::placeholder {
color: var(--color-text-placeholder);
}
.input:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: var(--shadow-focus);
}
/* Error state */
.inputError {
border-color: var(--color-danger-input-border);
}
.inputError:focus {
box-shadow: var(--shadow-focus-danger);
}Three button variants — primary, danger, and outline:
/* Primary button */
.buttonPrimary {
background: var(--color-primary);
color: var(--color-primary-text);
border: none;
border-radius: var(--radius-md);
padding: var(--spacing-2) var(--spacing-4);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: var(--transition-button);
}
.buttonPrimary:hover {
background: var(--color-primary-hover);
}
.buttonPrimary:active {
background: var(--color-primary-active);
}
.buttonPrimary:focus-visible {
outline: none;
box-shadow: var(--shadow-focus);
}
/* Danger button */
.buttonDanger {
background: var(--color-danger);
color: var(--color-danger-text);
/* ... same shape/font/cursor tokens ... */
}
.buttonDanger:hover {
background: var(--color-danger-hover);
}
.buttonDanger:active {
background: var(--color-danger-active);
}
.buttonDanger:focus-visible {
box-shadow: var(--shadow-focus-danger);
}
/* Outline / secondary button */
.buttonOutline {
background: transparent;
color: var(--color-text-primary);
border: 1px solid var(--color-border-strong);
/* ... same shape/font/cursor tokens ... */
transition: var(--transition-button-border);
}
.buttonOutline:hover {
background: var(--color-bg-hover);
border-color: var(--color-border-focus);
}
.buttonOutline:focus-visible {
box-shadow: var(--shadow-focus-subtle);
}All interactive elements on mobile and tablet must meet the 44px minimum touch target:
@media (max-width: 1024px) {
.button {
min-height: 44px;
}
}React Router does not add a literal active class when using CSS Modules. Use the function form:
<NavLink
to="/work-items"
className={({ isActive }) => `${styles.navLink} ${isActive ? styles.active : ''}`}
>
Work Items
</NavLink>tokens.css :root {
--color-bg-primary: #ffffff; /* Layer 1 + 2: palette alias */
}
tokens.css [data-theme="dark"] {
--color-bg-primary: #1a1a2e; /* Layer 3: dark override */
}
MyComponent.module.css {
.card { background: var(--color-bg-primary); } /* no change needed */
}
When document.documentElement.dataset.theme = "dark", the browser's CSS cascade automatically picks up the [data-theme="dark"] overrides. No JavaScript or class toggling in components.
Location: client/src/contexts/ThemeContext.tsx
import { useTheme } from '../../contexts/ThemeContext.js';
function MyComponent() {
const { theme, resolvedTheme, setTheme } = useTheme();
// theme: 'light' | 'dark' | 'system' (user preference)
// resolvedTheme: 'light' | 'dark' (actual applied theme)
}Types exported:
ThemePreference = 'light' | 'dark' | 'system'ResolvedTheme = 'light' | 'dark'-
ThemeContextValue— shape of the context value
Provider setup (already in App.tsx):
<BrowserRouter>
<ThemeProvider>
<AuthProvider>{/* app content */}</AuthProvider>
</ThemeProvider>
</BrowserRouter>Location: client/src/components/ThemeToggle/ThemeToggle.tsx
A single button that cycles through Light → Dark → System preferences. It is placed inside the sidebar footer and is the only UI affordance for changing the theme.
Cycle order: light → dark → system → light → ...
Button label shows the current theme; aria-label announces the next theme (what clicking will do):
aria-label="Switch to Dark mode" (when current is Light)
aria-label="Switch to System mode" (when current is Dark)
aria-label="Switch to Light mode" (when current is System)
The theme preference is stored under the key 'theme' in localStorage. Valid values: 'light', 'dark', 'system'. Defaults to 'system' if not set or invalid.
When theme === 'system', ThemeContext reads window.matchMedia('(prefers-color-scheme: dark)') and subscribes to change events. The resolved theme updates automatically when the OS switches between light and dark mode.
When adding a new semantic token that needs a dark mode override:
-
Layer 2 — Add the token in
:rootwith a light-mode value referencing a palette token::root { --color-my-new-token: var(--color-blue-100); }
-
Layer 3 — Add the override in
[data-theme="dark"]:[data-theme='dark'] { --color-my-new-token: rgba(59, 130, 246, 0.15); }
-
Use in components as normal:
.element { background: var(--color-my-new-token); }
jsdom (used by Jest) does not implement window.matchMedia. The test setup at client/src/test/setupTests.ts provides a polyfill. When writing components that call useTheme(), tests must either:
- Wrap in
<ThemeProvider>(integration-style), or - Mock the
ThemeContextmodule withjest.unstable_mockModule()
Component: client/src/components/Logo/Logo.tsx
The Cornerstone logo is a keystone / arch motif — the central wedge stone of an arch, flanked by two column blocks on a shared base. It represents the product's role as the foundational element of a home-building project.
Design principles:
- Drawn as a single compound path using the even-odd fill rule (
fillRule="evenodd" clipRule="evenodd") so the arch opening is transparent (no background colour required) - Uses
fill="currentColor"— no hardcoded hex values in SVG attributes - Control the colour via CSS on the wrapping element:
color: var(--color-sidebar-focus-ring) - Accessible:
role="img"andaria-label="Cornerstone"for screen readers
Usage:
import { Logo } from '../../components/Logo/Logo.js';
// In sidebar (inherits white color from sidebar text context)
<Logo size={32} className={styles.logo} />;Sizes: The logo renders cleanly from 16px (very small) to 200px+. Default size prop is 32.
Color context:
- Sidebar: wrap in an element with
color: var(--color-sidebar-text)— logo becomes white - Light page: wrap in an element with
color: var(--color-text-primary)— logo becomes dark - No hardcoded color — always inherits from context
File: client/public/favicon.svg
The favicon is a standalone SVG that does NOT use CSS custom properties (browser tab icons don't process CSS variables). It uses explicit #3b82f6 (blue-500) fill on the keystone path.
The CopyWebpackPlugin in client/webpack.config.cjs copies client/public/ into the dist/ output directory so the favicon is served correctly in both development and production.
The brand primary color is blue-500 (#3b82f6). This appears in:
- The favicon fill
- Primary action buttons
- Active nav item in the sidebar
- Focus borders on form inputs
- The
--color-primarysemantic token (light mode)
Last updated: EPIC-12 Story 12.5 — Style Guide Documentation
Managed by the ux-designer agent