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

246 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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