diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md new file mode 100644 index 0000000..4ae4fa6 --- /dev/null +++ b/docs/DESIGN-SYSTEM.md @@ -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' + + +``` + +**Conventions:** +- `size`: 14–16px for inline/nav icons, 20–28px 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.88–0.95rem` | `600` | +| Body text | `0.875rem` | `400` | +| Small / meta | `0.72–0.78rem` | `400` | +| Label / tag | `0.65rem` | `500–700`, uppercase, `letter-spacing: 0.04em` | +| Monospace (hashes, IDs) | `0.72rem`, `font-family: monospace` | `400` | + +--- + +## Card Pattern + +Every content card follows this pattern: + +```tsx +
+``` + +Use `borderLeft: '3px solid '` 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//.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 +/// +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