Skip to main content

DESIGN_PRINCIPLES.md β€” h.work

The design decisions behind the platform UI. Read this before adding a screen. Cross-reference: GLOSSARY.md, INFORMATION_ARCHITECTURE.md, VIEWS_BY_ROLE.md, COMPONENT_INVENTORY.md, frontend/src/app/globals.css. Last updated: 2026-05-04.


1. Light/dark mode strategy​

Both modes are first-class. Light is the default.​

  • Light mode (Intercom-inspired): white surfaces, warm gray canvas. The platform's home aesthetic β€” what new users see.
  • Dark mode (Linear-inspired): warm near-black, lighter desaturated claret accent. The aesthetic for power users who live in the queue.

Mechanism​

[data-theme="dark"] on <html> flips the entire token system. No .dark class proliferation. Set by DarkModeProvider, which:

  1. Reads localStorage.hw_theme on mount.
  2. Falls back to prefers-color-scheme: dark if not stored.
  3. Updates the attribute and persists choice on toggle.

Token system (truncated β€” see globals.css)​

:root {                       /* light mode */
--surface-0: #FFFFFF; /* cards / panels / elevated */
--surface-1: #F7F8FA; /* page canvas */
--surface-2: #F0F1F5; /* sidebar */
--surface-3: #E8E9EE; /* hover */
--text-0: #111827;
--text-2: #6B7280;
--border-0: #E5E7EB;
--accent-0: #7E1D1D; /* claret */
}

[data-theme="dark"] {
--surface-0: #1C1C1E; /* elevated */
--surface-1: #0F0F10; /* canvas */
--surface-2: #161618; /* sidebar */
--surface-3: #242426; /* hover */
--text-0: #F5F5F5;
--text-2: #8E8E93;
--border-0: #2C2C2E;
--accent-0: #C94B4B; /* lighter claret β€” readable on dark */
}

Then semantic aliases sit on top: --bg-canvas, --bg-surface, --bg-sidebar, --bg-hover, --text-primary, --text-secondary, --text-muted, --border, --accent, --accent-subtle, --accent-hover, --accent-fg. Components consume the aliases, never the raw scale. This way a future palette change touches one block in globals.css.

Key rule​

  • Hex colors don't appear in components. Every color is a token. Exception: brand SVG icons that need a literal brand color (e.g. Slack purple) β€” but even then, prefer brand tokens defined once.
  • Risk badges and status pills have their own theme-aware token pairs (--risk-critical-bg/text, etc.). They flip automatically. No if (theme === 'dark') checks in components.

2. Color usage rules​

Claret β€” used sparingly​

The accent (#7E1D1D light / #C94B4B dark) is the brand color. It is reserved for:

  1. Primary CTAs β€” "Send", "Send Code", "+ New Thread", "Approve", "Send invite".
  2. Active navigation β€” left border on sidebar items, bottom border on top-nav tabs, active text color.
  3. Specialist persona avatars β€” the workspace identity (slug avatar, Specialist persona, user avatars where the user belongs to that workspace).
  4. Selected-state affordances β€” selected ticket row, selected thread, focus ring (3px --accent-subtle halo).
  5. Branding tile β€” the H logo square.

It is not used for:

  • Generic icons (use --text-muted).
  • Body text (use --text-primary).
  • Generic borders (use --border).
  • Errors (use --danger).
  • Hover affordances on neutral elements (use --bg-hover).

A useful rule: if you can replace the claret with a neutral and the meaning is preserved, you should. Reserve the color for moments where it carries semantic weight.

Status semantics​

TokenUse
--success (green)Connected channels, resolved tickets, "Online now", high SLA
--warning (amber)Trial banner, medium-risk SLA, pending agentic step
--danger (red)Critical risk badge, broken channel, destructive action affirmation
--info (blue)"AI" handling badge, "You" assignee pill
--teal-0"Assigned" pill (other expert), low-risk badge

Risk levels are not red-amber-yellow-green:

  • critical β†’ danger (red)
  • high β†’ warning (orange/amber)
  • medium β†’ warning (lighter)
  • low β†’ teal (intentionally not green; green is reserved for "good" states like online/connected, and a green "low risk" badge would mistakenly read as positive when it's actually about uncertainty)

Light vs dark variation​

The accent shifts (#7E1D1D β†’ #C94B4B) for legibility on dark surfaces. Status colors also shift slightly:

StatusLightDark
success#16A34A#22C55E
warning#D97706#F59E0B
danger#DC2626#EF4444

Designers don't think about this β€” tokens handle it.


3. Typography​

Font stack β€” do not change​

Manrope, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif

Mono: 'JetBrains Mono', ui-monospace, monospace (used for code, IDs, audit log entries).

Display serif (page headers, editorial moments β€” --font-display / --font-heading): 'Source Serif 4', ui-serif, Georgia, serif. Used in <PageHeader>, dashboard hero, login page.

Sizes (px)​

SizeUse
10Section labels (uppercase 0.08em tracking 700 weight), tiny status badges
11Metadata (timestamps, captions), button labels, footer links
12Secondary body, badges, sidebar nav, inputs in dense panels
13Default body, primary table rows, primary nav
14Message bubbles (chat), card titles, primary inputs in forms
16–18Section headings (h2/h3)
20–24Page titles (h1), stat values

Default body is 13px. This is intentionally compact β€” power users (experts especially) value information density. Chat messages bump to 14px because conversation tone benefits from a more comfortable reading size.

Weights​

  • 400 β€” body text
  • 500 β€” sidebar items, semi-emphasized rows
  • 600 β€” headings, labels, primary nav active state
  • 700 β€” uppercase section labels, big numbers (24px stat values), badges

Line height​

  • Body: 1.5
  • Bubbles: 1.55 (slightly more breathing room)
  • Headings: 1.2–1.3

Tabular numerals​

Use font-variant-numeric: tabular-nums for any column of numbers (queue counts, timestamps, SLA percentages). Prevents column wobble.


4. Component patterns​

Cards​

  • --bg-surface background, --border 1px border, 12px border-radius (Linear/Intercom convention).
  • 16–20px internal padding for compact cards, 24–28px for hero cards.
  • Shadows: avoid by default. Use only for floating elements (modals, popovers).

Tables​

  • 13px body, --text-primary.
  • Header row: --bg-surface + 11/600 uppercase --text-muted labels + --border bottom.
  • Row hover: --bg-hover.
  • Row selected: --accent-subtle + --accent left bar (3px).
  • Sticky header on scroll.

Inputs​

  • --bg-surface background, --border 1px border, 8px border-radius (slightly tighter than cards).
  • Focus: --accent border + 3px --accent-subtle halo (box-shadow).
  • 14px font for primary inputs, 13px for dense.
  • 10/12 vertical padding Γ— 12/14 horizontal.

Buttons​

  • See Button in COMPONENT_INVENTORY.md for variants.
  • Heights: sm 28 / md 36 / lg 44. Touch targets β‰₯44px for any tap-able primary action.
  • Primary always uses --accent background; never use --success or --info as a button background.

Badges​

  • 11px / 600 weight / uppercase / 0.04em tracking / 4px radius.
  • Each variant pairs --{semantic}-subtle bg with --{semantic}`` text. Never raw hex.

Bubbles (chat)​

  • 14px body / 1.55 line-height / 9px vertical 13px horizontal padding / 18px radius with directional 4px tail.
  • Outgoing (user/customer): --accent bg + white text. Tail in bottom-right (18px 18px 4px 18px).
  • Incoming (Specialist/Expert): --bg-surface bg + 1px border + --text-primary. Tail in top-left (4px 18px 18px 18px).
  • Internal note: dashed amber border + πŸ”’ icon, never visible to clients (gated by EXPERT_ROLES).

Active nav​

Two patterns, depending on orientation:

LayoutActive state
Vertical sidebar (AM, Superadmin, Portal admin, Portal chat threads)3px --accent left border + --accent text + --accent-subtle bg
Horizontal top tabs (Workspace, status tabs in queue, Org tabs in queue)2px --accent bottom border + --accent text

These are the only two active-nav patterns. Don't invent a third.

Empty states​

  • Centered, --text-muted color, soft tone.
  • Eyebrow label (10/700 uppercase muted) + h2 (20/600 primary) + caption (13/muted) + primary CTA.
  • For the chat empty state: a single Specialist card with green online dot β€” the entire visual weight is on the next action, not on the absence of content.

Loading states​

  • Skeleton placeholders matching real content shape β€” never spinners on content surfaces.
  • Spinner only on buttons during async actions.

Modals​

  • Centered, --bg-surface, 12px radius, 1px border, 24–32px padding.
  • Backdrop: rgba(0,0,0,0.5) light / rgba(0,0,0,0.7) dark.
  • Close icon top-right (16px, muted, hover lifts).
  • Primary action right, secondary left.

5. Voice and copy guidelines​

These come from GLOSSARY.md and from product values. They apply to UI copy, error messages, empty states, and email templates.

The platform name​

  • Always lowercase: h.work β€” including at the start of sentences ("h.work helps you…").
  • The dot is part of the name. Do not write "human work" or "Human Work".

Words to use​

  • "humans" β€” when referring to the people on the team. Not "users", not "members" (unless specifically referring to org membership).
  • "workspace" β€” user-facing term for an org. Use this in UI, URLs (acme.h.work), emails. "Org" is internal/code-only.
  • "Specialist" (capitalized as a role/persona name like "Bob is your Specialist") β€” the named client-facing identity.
  • "Expert" β€” internal humans behind the Specialist. Never shown to clients.
  • "AI Agent" / "agent" β€” automated component. Internal-only language. Clients see "AI" badge inline; they never see the word "agent" in copy.
  • "Send" β€” the verb for outbound messages, not "submit".
  • "Resolve" β€” the verb for closing a ticket, not "close".
  • "Defer" β€” for snoozing (24h default), not "snooze".

Words to avoid​

  • ❌ "User" β€” replace with "human", "team member", or the specific role.
  • ❌ "Submit" β€” replace with "Send" or the specific action verb ("Save", "Invite", "Resolve").
  • ❌ "Ticket" in client-facing copy where conversation works ("thread" is fine in chat sidebar, "ticket" is okay for trackable work items).
  • ❌ Capitalized "User", "Customer" in body copy. Exception: official entity names (Specialist, Expert).
  • ❌ Marketing speak: "amazing", "powerful", "revolutionary". This is a B2B SaaS β€” the audience knows what's amazing.

Tone​

  • Direct, calm, helpful. Treat the reader as a capable colleague.
  • Short sentences. Three short ones beat one compound sentence.
  • No emoji in product UI. Reserve emoji for status chips that already have semantic meaning (βœ“ for confirmation, πŸ”’ for internal note). Never decorative.
  • Errors describe what went wrong + what to do next. "We couldn't reach the agent service. Please try again in a moment." β€” not "Error: 500 Internal Server Error".
  • Copy is sentence case. Never Title Case for body text or buttons. Exceptions: page titles, official names.

Inclusive defaults​

  • Singular "they" by default. No gendered pronouns in templates.
  • No assumptions about the reader's role or business size.

Examples​

❌ Don'tβœ… Do
"Submit Form""Save changes"
"Welcome User!""Welcome to your workspace"
"Click here to message your AI Agent""Message Bob to get started"
"An Error Occurred""We couldn't load your threads. Try refreshing."
"Successfully Sent!""Sent" (in a toast β€” the toast color carries the semantics)

6. What NOT to do​

These are anti-patterns we have explicitly rejected. If a design proposal contains any of these, push back.

❌ Red on black​

The previous design used #7E1D1D claret on #0A0A0B near-black backgrounds. This reads as harsh, amateur, and visually fatiguing. Dark mode now uses warm #0F0F10 with the lighter #C94B4B accent β€” calibrated for contrast and comfort.

❌ Inline hex colors in components​

Every color goes through a token. If you need a new color, define it in globals.css first and then reference the alias. A grep for #[0-9a-f]{6} in TSX should turn up zero matches in component code (only in globals.css).

❌ All-caps headlines or labels in body copy​

Reserve uppercase for section eyebrows (10px, 700 weight, 0.08em tracking) and badges/pills. Page titles, button labels, and body text are sentence case.

βœ… Editorial serif used deliberately, not gratuitously​

The product uses Manrope for body + UI text (--font-sans) and Source Serif 4 as an editorial display serif (--font-display / --font-heading β€” used in page headers, dashboards, login). JetBrains Mono is the only mono font. The serif is deliberate β€” page headers and editorial moments use it. Don't add a third sans family (no Inter β€” superseded 2026-Q2 β€” and no Geist Mono either, despite older comments). The previous all-sans claim was wrong; both font swaps (#1010 Inter + #1104 Test Tiempos) were reverted in #1234 / #1223, restoring Manrope + Source Serif 4 as the canonical stack.

❌ Multiple primary buttons in one view​

One primary CTA per screen (or per section). If there are competing actions, the secondary action gets the secondary variant. The eye should know where to land.

❌ Modal-trapped destructive actions​

Destructive actions (delete workspace, archive org, remove member) should:

  1. Use the danger variant explicitly.
  2. Require typed confirmation for irreversible actions ("type ACME to confirm").
  3. Never auto-focus the destructive button.

❌ Confidence scores in client UI​

Clients never see numeric confidence (e.g. "82%"). They see handling β€” "AI" or "Human review" badge. Confidence is operator data; revealing it dilutes the Specialist persona model and creates a false expectation of opaqueness control.

❌ Exposing the team​

Clients never see:

  • The word "Expert" in attribution (only "Specialist name" or the specialist's persona).
  • The internal note panel.
  • The AI agent's internal name or instance ID. They see "AI" if relevant to the handling.
  • Multiple senders in a single thread (other than themselves and the Specialist).

If a client asks "who answered this?" the answer is always "Bob" (the Specialist).

❌ Yellow/amber for Debug Mode panels​

Make development affordances (debug mode, demo login panels) subtle β€” --surface-3 background, single-pixel separators, low chroma. They shouldn't shout for attention or be confused with warnings.

❌ Random border-radius​

Stick to the scale: 4px (badges), 8px (inputs, small buttons), 12px (cards), 18px (chat bubbles), 50% (avatars). No border-radius: 7px ad-hoc.

❌ Mixed icon styles​

We use line-style icons (Lucide-like, 1.5–2px stroke, currentColor) throughout. No mixing line and filled. Exception: the H logo tile is filled by design.

❌ Toasts with raw Error objects​

toast.error(err instanceof Error ? err : "Friendly fallback") β€” never let raw stack traces or "Internal Server Error" reach users. The error is logged separately.


7. Accessibility baselines​

  • All interactive elements have β‰₯44Γ—44px touch targets (use .tap-target utility).
  • Text contrast β‰₯4.5:1 for body, β‰₯3:1 for large text. Verify both light and dark modes.
  • Focus ring visible on every focusable element β€” --accent 2px outline + 2px offset (set globally on :focus-visible).
  • Form labels are programmatic β€” <label> for inputs, aria-label for icon-only buttons.
  • Color is never the only signal β€” risk badges have text labels; status pills do too.
  • Animations respect prefers-reduced-motion (skip slide/pulse if requested).

8. Performance baselines​

  • Default body 13px is faster to scan and lets us pack more meaningful content above the fold.
  • Skeletons over spinners β€” perceived speed.
  • Polling (queue, chat) at 10–15s intervals; WebSocket pushes when available.
  • No layout shift after data load β€” reserve space (placeholder cards, fixed-height rows).

9. When in doubt​

  1. Reach for tokens, not values. If you're typing a hex, stop and check whether a token already exists or should be added.
  2. Prefer existing components over new ones. If you need a card, use the same pattern other cards use. Variation should be intentional.
  3. Match the role's mental model. A client wants conversation; an expert wants a queue; an AM wants a dashboard. Don't import the wrong metaphor.
  4. Cut, then cut again. The more you remove, the better B2B SaaS gets. If a label, divider, or icon doesn't earn its place, delete it.

Owner: Design + Platform team