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>
7.6 KiB
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 (
#0f0f20body,#1a1a2ecard). 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: 14–16px for inline/nav icons, 20–28px for tile/feature iconsstrokeWidth: always1.75(the default is 2, which reads slightly heavy)- Nav icons inherit
colorfrom the parent element (let CSS handle active state) - App tile icons use the app's
theme_colorfrom 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:
<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
:rootCSS block above intosrc/index.css - Install
lucide-react, pick an icon, add it toapps.iconin auth DB - Add
VITE_HOTEL_NAMEbuild arg toDockerfileanddocker-compose.yml - Add
src/vite-env.d.ts - Set
base: '/your-path/'invite.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