portal/docs/DESIGN-SYSTEM.md
jtricerolph 15c2064483 Add design system documentation
Covers CSS variables, colour usage, typography, card/input/button
patterns, icon conventions, VITE_HOTEL_NAME setup, and new app checklist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 18:16:17 +00:00

7.6 KiB
Raw Blame History

HNF Stack — Design System

Visual Language

The stack uses a split chrome / content approach:

  • Sidebar / shell chrome — dark navy (#1a1a2e). This is the brand anchor. It stays dark regardless of the content area.
  • Content area — light grey body (#f4f5f7), white cards with subtle shadows. Readable for data-heavy pages and matches the aesthetic of the Kitchen Flash app that preceded this stack.
  • Standalone login screens — full-screen dark (#0f0f20 body, #1a1a2e card). Intentional contrast; feels like signing into something, not just another content page.

The gold accent (#c9a84c) is used sparingly: active nav indicator, primary action buttons, pinned/flagged items, update badges. It reads as the hotel brand colour on both dark and light surfaces.


CSS Variables

Every app in the stack must include this :root block verbatim in its index.css. Do not invent new colour values — add a variable here if the palette genuinely needs extending.

:root {
  /* ── Shell chrome (dark) ── */
  --navy:        #1a1a2e;   /* sidebar background, dark login card */
  --navy-dark:   #0f0f20;   /* login page background */
  --gold:        #c9a84c;   /* accent: active nav, primary buttons, badges */
  --gold-light:  #e8c96d;   /* gold hover/highlight variant */
  --surface:     rgba(255,255,255,0.07);  /* active nav item background */
  --surface-2:   rgba(255,255,255,0.08);  /* sidebar dividers / borders */
  --text:        rgba(255,255,255,0.88);  /* sidebar / dark-bg text */
  --text-muted:  rgba(255,255,255,0.48);  /* sidebar muted text */

  /* ── Content area (light) ── */
  --body-bg:     #f4f5f7;   /* page background */
  --card-bg:     #ffffff;   /* card / panel background */
  --card-border: #e4e8ee;   /* card border, input border */
  --text-dark:   #1e293b;   /* primary text on light backgrounds */
  --text-mid:    #64748b;   /* muted / secondary text on light backgrounds */
  --shadow-sm:   0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.04);
  --shadow-md:   0 4px 12px rgba(0,0,0,0.08);

  /* ── Shared ── */
  --danger:      #dc2626;   /* error states, destructive actions */
  --radius:      10px;      /* standard border radius */
  --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

body {
  background: var(--body-bg);
  color: var(--text-dark);
  font-family: var(--font);
}

Colour Usage Guide

Situation Variable
Page / content background --body-bg
Card / panel background --card-bg
Card / input border --card-border
Primary text (light bg) --text-dark
Secondary / muted text (light bg) --text-mid
Primary text (dark bg / sidebar) --text
Muted text (dark bg / sidebar) --text-muted
Primary action button background: var(--gold); color: var(--navy)
Destructive action var(--danger)
Success state #16a34a (green-600)
Warning / running state #d97706 (amber-600)
Status badge — success background: #dcfce7; color: #16a34a
Status badge — failed background: #fee2e2; color: var(--danger)
Status badge — running background: #fef9c3; color: #ca8a04
Terminal / code output background: #1e293b; color: #94a3b8

Icons

Use Lucide React (lucide-react npm package) for all icons. Do not use emoji as functional UI elements.

npm install lucide-react
import { ChefHat, ClipboardList } from 'lucide-react'

<ChefHat size={20} strokeWidth={1.75} color={app.theme_color} />

Conventions:

  • size: 1416px for inline/nav icons, 2028px for tile/feature icons
  • strokeWidth: always 1.75 (the default is 2, which reads slightly heavy)
  • Nav icons inherit color from the parent element (let CSS handle active state)
  • App tile icons use the app's theme_color from the DB

App icon registry (Lucide name → app slug):

App Lucide name
Noticeboard ClipboardList
Kitchen Flash ChefHat
Cash Up Banknote
Housekeeping BedDouble
Forecasting TrendingUp
Rate Scraper Tag

Stored in apps.icon column in the auth DB. The portal renders them via AppIcon component (portal/src/components/AppIcon.tsx).


Typography

No custom typeface — system font stack only. This avoids a font load and looks native on every device.

Use Size Weight
Section heading 0.95rem 600
Card title 0.880.95rem 600
Body text 0.875rem 400
Small / meta 0.720.78rem 400
Label / tag 0.65rem 500700, uppercase, letter-spacing: 0.04em
Monospace (hashes, IDs) 0.72rem, font-family: monospace 400

Card Pattern

Every content card follows this pattern:

<div style={{
  background: 'var(--card-bg)',
  border: '1px solid var(--card-border)',
  borderRadius: 'var(--radius)',
  boxShadow: 'var(--shadow-sm)',
  padding: '1rem 1.125rem',
}}>

Use borderLeft: '3px solid <colour>' to add a status accent stripe on the left edge. Use var(--shadow-md) on hover.


Input Pattern

const inputStyle: React.CSSProperties = {
  background: 'var(--body-bg)',
  border: '1px solid var(--card-border)',
  borderRadius: '7px',
  color: 'var(--text-dark)',
  padding: '0.6rem 0.75rem',
  fontSize: '0.9rem',
  width: '100%',
  outline: 'none',
}

On focus, add border-color: var(--gold) via CSS or inline onFocus.


Button Patterns

Primary action (e.g. submit, open, update):

style={{
  background: 'var(--gold)', color: 'var(--navy)',
  border: 'none', borderRadius: '7px',
  padding: '0.45rem 1rem', fontSize: '0.82rem', fontWeight: 600,
}}

Secondary / ghost (e.g. refresh, cancel):

style={{
  background: 'var(--card-bg)', color: 'var(--text-mid)',
  border: '1px solid var(--card-border)', borderRadius: '7px',
  padding: '0.35rem 0.9rem', fontSize: '0.8rem',
}}

Icon button (e.g. pin, delete):

style={{
  background: 'var(--body-bg)', border: '1px solid var(--card-border)',
  borderRadius: '6px', padding: '0.3rem',
  color: 'var(--text-mid)',
  display: 'flex', alignItems: 'center', justifyContent: 'center',
}}

Hotel Name

The hotel name is injected at build time via a Vite env var:

VITE_HOTEL_NAME=Number Four at Stow

Set in /opt/<app>/.env on the server before rebuilding. Reference it in any component with:

{import.meta.env.VITE_HOTEL_NAME}

Each app's Dockerfile must declare it as a build arg with the default:

ARG VITE_HOTEL_NAME="Number Four at Stow"
ENV VITE_HOTEL_NAME=$VITE_HOTEL_NAME

And docker-compose.yml:

build:
  context: .
  args:
    VITE_HOTEL_NAME: ${VITE_HOTEL_NAME:-Number Four at Stow}

Add src/vite-env.d.ts so TypeScript knows about it:

/// <reference types="vite/client" />
interface ImportMetaEnv { readonly VITE_HOTEL_NAME: string }
interface ImportMeta { readonly env: ImportMetaEnv }

App Checklist (new app)

  • Copy the full :root CSS block above into src/index.css
  • Install lucide-react, pick an icon, add it to apps.icon in auth DB
  • Add VITE_HOTEL_NAME build arg to Dockerfile and docker-compose.yml
  • Add src/vite-env.d.ts
  • Set base: '/your-path/' in vite.config.ts
  • All API calls use the base path prefix (e.g. /your-path/api/...)
  • Standalone login screen: explicit background: var(--navy-dark) on outer container
  • Content area: --body-bg / --card-bg / --card-border / --text-dark / --text-mid
  • No hardcoded colour hex values in component files — variables only