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>
This commit is contained in:
jtricerolph 2026-07-01 18:16:17 +00:00
parent 092c4fda31
commit 15c2064483

246
docs/DESIGN-SYSTEM.md Normal file
View file

@ -0,0 +1,246 @@
# 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.
```css
: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.
```bash
npm install lucide-react
```
```tsx
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:
```tsx
<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
```tsx
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):
```tsx
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):
```tsx
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):
```tsx
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:
```tsx
{import.meta.env.VITE_HOTEL_NAME}
```
Each app's `Dockerfile` must declare it as a build arg with the default:
```dockerfile
ARG VITE_HOTEL_NAME="Number Four at Stow"
ENV VITE_HOTEL_NAME=$VITE_HOTEL_NAME
```
And `docker-compose.yml`:
```yaml
build:
context: .
args:
VITE_HOTEL_NAME: ${VITE_HOTEL_NAME:-Number Four at Stow}
```
Add `src/vite-env.d.ts` so TypeScript knows about it:
```ts
/// <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