# 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