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:
parent
092c4fda31
commit
15c2064483
1 changed files with 246 additions and 0 deletions
246
docs/DESIGN-SYSTEM.md
Normal file
246
docs/DESIGN-SYSTEM.md
Normal 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`: 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
|
||||
<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
|
||||
Loading…
Add table
Add a link
Reference in a new issue