Skip to content

Design System ​

Last Updated: 2026-08-19


Color Architecture (3 Layers) ​

Layer A — Neutral UI (mode-dependent, theme-independent) ​

Defined in src/lib/palettes.ts as NEUTRAL_LIGHT / NEUTRAL_DARK. Applied via CSS custom properties. Never hardcode hex — always use CSS vars.

CSS VariableLightDarkUsage
--bg#F5F7FA#090E16Page background
--surface#FFFFFF#111A27Card / panel bg
--border#E2E8F0#243143All borders
--text-primary#182230#F1F5F9Headings, values
--text-secondary#667085#A7B2C3Labels, captions
--text-muted#98A2B3#718096Hints, disabled

Layer B — Theme Identity (palette-driven) ​

10 palettes × light/dark variants. Set via applyPalette(key, mode). Default palette: aurora.

CSS VariablePurpose
--primaryButtons, active states, links
--primary-hoverButton hover
--primary-softSoft badge backgrounds
--accentSection borders, active pills, highlights
--palette-gradient-from/mid/toSidebar gradient, login page left panel
--palette-accentSidebar selected indicator, chart accents

Available palettes: aurora, hermina, ocean, slate, cobalt, sunset, rose, forest, midnight, sapphire

ts
import { applyPalette } from '@/lib/palettes';
applyPalette('aurora', 'light'); // sets all CSS vars

Layer C — Data Visualization ​

12-color palette, theme-reordered. Charts use --chart-1 through --chart-12, never --primary.

ts
import { getChartColor } from '@/lib/colors';
const color = getChartColor(index); // cycles 0-11

Status Colors ​

Fixed semantic colors — consistent across all palettes and modes.

TokenLightDarkWhen to use
--success#16A34A#4ADE80>= 95% of target
--info#2563EB#60A5FA80-95% of target
--warning#D97706#FBBF24< 80% of target
--danger#DC2626#F87171Critical only (BOR < 60%, safety events)
ts
import { getStatusColor, getStatusLabel } from '@/lib/colors';
const color = getStatusColor(achievementPct); // 0-100

Rule: Never use --danger for "below target" — use --warning. Red is reserved for emergencies.


Typography ​

Hero KPI values:     34px, weight 700, tracking -0.02em, tabular-nums
Supporting KPIs:     22px, weight 700, tracking -0.01em
Section titles:      13-14px, weight 600
Helper / caption:    11-12px, color var(--text-muted)

Apply className="tabular-nums" on all numeric KPI displays (defined in global.css).


4-Zone Dashboard Layout ​

Every dashboard page follows this structure:

Zone 1 — HERO           1-3 large cards, primary question answered at a glance
Zone 2 — SUPPORTING     3-5 compact KPI tiles explaining hero metrics
Zone 3 — PRIMARY CHART  One main decision chart + optional side chart (same height)
Zone 4 — ACTION ZONE    Filtered lists, branch alerts, actionable items

Grid ​

tsx
// Hero (3 cols)
<Row gutter={[12, 12]}>
  <Col xs={24} sm={8}><HeroCard /></Col>
</Row>

// Charts (flexible, same row = same height)
<Row gutter={[12, 12]}>
  <Col xs={24} lg={14}><Card><BigChart height={280} /></Card></Col>
  <Col xs={24} lg={10}><Card><SideChart height={280} /></Card></Col>
</Row>

Chart Heights ​

Use caseHeight
Default side-by-side280px
Clinical / funnel300px
Large detail320px
Full-width bar360px

Chart Rules ​

  1. Max 12 items — never render all 54 branches
  2. Reference lines for targets via targetValue prop
  3. Status coloring via getStatusColor(pct) — never ad-hoc hex
  4. Dark mode tooltips — always use CSS vars:
    tsx
    <div style={{
      background: 'var(--tooltip-bg)',
      color: 'var(--tooltip-text)',
      border: '1px solid var(--tooltip-border)',
      borderRadius: 8, padding: '8px 12px',
    }}>

Section Headers ​

tsx
<div style={{
  borderLeft: '3px solid var(--primary)',
  paddingLeft: 10,
  fontWeight: 600,
  color: 'var(--text-primary)',
}}>
  Section Title
</div>

Status Badge ​

tsx
<span style={{
  background: statusColor + '18',  // 10% opacity hex suffix
  color: statusColor,
  borderRadius: 20,
  padding: '2px 10px',
  fontSize: 12,
  fontWeight: 600,
}}>
  {achievement.toFixed(0)}% of target
</span>

Delta Indicators ​

tsx
<span style={{ fontSize: 12, color: deltaUp === higherIsBetter ? 'var(--success)' : 'var(--warning)' }}>
  {deltaUp ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
  {' '}{Math.abs(delta).toFixed(1)}% vs last month
</span>

Login Page ​

Two-panel layout:

  • Left panel — full-height gradient (--palette-gradient-from/mid/to), Hermina logo, "Hermina 360" title, "BUSINESS INTELLIGENCE" subtitle, radial glow highlights using --primary and --palette-accent
  • Right panel — 480px wide, --surface bg, border-radius: 24px 0 0 24px, vertically centered form, Google SSO button

The gradient follows the active palette automatically — no hardcoded hex on the login page.


Dark Mode ​

Triggered via data-theme="dark" on <html>. Applied by useThemeStore.toggle().

CSS-only defaults defined in src/styles/global.css under [data-theme="dark"] block (active before JS fires). Runtime values overridden by applyPalette().

Components use CSS vars throughout — no conditional mode === 'dark' checks needed in component styles.


Palette System ​

Stored in useAppearanceStore (hermina360-appearance localStorage key). Default: aurora.

ts
// Aurora palette (default)
gradientStart: '#4338CA'
gradientMid:   '#7C3AED'
gradientEnd:   '#06B6D4'
primary:       '#7C3AED'
accent:        '#06B6D4'

Change palette:

tsx
const { paletteKey, setPalette } = useAppearanceStore();
setPalette('ocean'); // persists to localStorage

Accessibility ​

  • WCAG AA: 4.5:1 contrast minimum
  • Colorblind-safe: Protanopia, Deuteranopia, Tritanopia tested
  • Touch targets >= 44px
  • Test: Chrome DevTools -> Rendering -> "Emulate vision deficiencies"