Apply stack design system: light theme + Lucide icons

- index.css: match portal token system (light body, dark login chrome)
- NoticeBoard: white cards/shadows, Lucide Pin/PinOff/Trash2/Plus/X icons,
  light input/button styles, category pills with navy active state
- AuthGate: explicit dark bg on login container (body is now light)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-01 18:15:53 +00:00
parent 58b3d82b85
commit 24747919cd
5 changed files with 1957 additions and 63 deletions

1859
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^1.23.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},

View file

@ -75,9 +75,10 @@ export function AuthGate({ children }: Props) {
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'center',
justifyContent: 'center', height: '100dvh', padding: '1.5rem',
background: 'var(--navy-dark)',
}}>
<div style={{
background: 'var(--surface)', borderRadius: 'var(--radius)',
background: 'var(--navy)', borderRadius: 'var(--radius)',
padding: '2rem', width: '100%', maxWidth: '360px',
border: '1px solid var(--surface-2)',
}}>

View file

@ -1,4 +1,5 @@
import { useEffect, useReducer, useState } from 'react'
import { Pin, PinOff, Trash2, Plus, X } from 'lucide-react'
interface Notice {
id: number
@ -14,11 +15,11 @@ interface Notice {
interface User { email: string; name: string; is_admin: boolean }
const CATEGORIES = [
{ value: 'all', label: 'All' },
{ value: 'general', label: 'General' },
{ value: 'kitchen', label: 'Kitchen' },
{ value: 'housekeeping', label: 'Housekeeping' },
{ value: 'management', label: 'Management' },
{ value: 'all', label: 'All' },
{ value: 'general', label: 'General' },
{ value: 'kitchen', label: 'Kitchen' },
{ value: 'housekeeping',label: 'Housekeeping' },
{ value: 'management', label: 'Management' },
]
export function NoticeBoard({ user }: { user: User }) {
@ -46,24 +47,27 @@ export function NoticeBoard({ user }: { user: User }) {
}
return (
<div style={{ maxWidth: '720px', margin: '0 auto', padding: '1rem' }}>
<div style={{ maxWidth: '720px', margin: '0 auto', padding: '1.5rem 1rem' }}>
<header style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: '1.25rem', paddingBottom: '1rem',
borderBottom: '1px solid var(--surface-2)',
marginBottom: '1.25rem',
}}>
<div>
<h1 style={{ fontSize: '1.25rem', color: 'var(--gold)' }}>Noticeboard</h1>
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{user.name}</p>
<h1 style={{ fontSize: '1.15rem', fontWeight: 700, color: 'var(--text-dark)' }}>
Noticeboard
</h1>
<p style={{ fontSize: '0.75rem', color: 'var(--text-mid)', marginTop: '0.1rem' }}>{user.name}</p>
</div>
{user.is_admin && (
<button onClick={() => setShowForm(v => !v)} style={{
background: showForm ? 'var(--surface-2)' : 'var(--gold)',
color: showForm ? 'var(--text)' : 'var(--navy-dark)',
border: 'none', borderRadius: '6px', padding: '0.5rem 1rem',
fontSize: '0.875rem', fontWeight: 600,
background: showForm ? 'var(--card-bg)' : 'var(--gold)',
color: showForm ? 'var(--text-mid)' : 'var(--navy)',
border: showForm ? '1px solid var(--card-border)' : 'none',
borderRadius: '7px', padding: '0.45rem 0.9rem',
fontSize: '0.82rem', fontWeight: 600,
display: 'flex', alignItems: 'center', gap: '0.4rem',
}}>
{showForm ? 'Cancel' : '+ Post'}
{showForm ? <><X size={14} strokeWidth={2} /> Cancel</> : <><Plus size={14} strokeWidth={2} /> Post</>}
</button>
)}
</header>
@ -72,13 +76,15 @@ export function NoticeBoard({ user }: { user: User }) {
<PostForm onPosted={() => { setShowForm(false); forceRefresh() }} />
)}
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
{/* Category filters */}
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '1.25rem', flexWrap: 'wrap' }}>
{CATEGORIES.map(c => (
<button key={c.value} onClick={() => setCategory(c.value)} style={{
background: category === c.value ? 'var(--gold)' : 'var(--surface)',
color: category === c.value ? 'var(--navy-dark)' : 'var(--text-muted)',
border: '1px solid var(--surface-2)', borderRadius: '20px',
padding: '0.3rem 0.875rem', fontSize: '0.8rem', fontWeight: 600,
background: category === c.value ? 'var(--navy)' : 'var(--card-bg)',
color: category === c.value ? 'var(--gold)' : 'var(--text-mid)',
border: `1px solid ${category === c.value ? 'var(--navy)' : 'var(--card-border)'}`,
borderRadius: '20px', padding: '0.3rem 0.875rem',
fontSize: '0.78rem', fontWeight: 500,
}}>
{c.label}
</button>
@ -86,7 +92,7 @@ export function NoticeBoard({ user }: { user: User }) {
</div>
{notices.length === 0 && (
<p style={{ color: 'var(--text-muted)', textAlign: 'center', padding: '3rem 0', fontSize: '0.9rem' }}>
<p style={{ color: 'var(--text-mid)', textAlign: 'center', padding: '3rem 0', fontSize: '0.9rem' }}>
No notices yet.
</p>
)}
@ -94,29 +100,41 @@ export function NoticeBoard({ user }: { user: User }) {
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{notices.map(n => (
<div key={n.id} style={{
background: 'var(--surface)', borderRadius: 'var(--radius)',
background: 'var(--card-bg)',
borderRadius: 'var(--radius)',
padding: '1rem 1.125rem',
border: n.pinned ? '1px solid var(--gold)' : '1px solid var(--surface-2)',
border: `1px solid ${n.pinned ? 'var(--gold)' : 'var(--card-border)'}`,
borderLeft: `3px solid ${n.pinned ? 'var(--gold)' : 'var(--card-border)'}`,
boxShadow: 'var(--shadow-sm)',
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '0.5rem' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.375rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginBottom: '0.4rem' }}>
{n.pinned && (
<span style={{ fontSize: '0.7rem', background: 'var(--gold)', color: 'var(--navy-dark)',
borderRadius: '4px', padding: '0.1rem 0.4rem', fontWeight: 700 }}>
<span style={{
fontSize: '0.65rem', background: 'var(--gold)', color: 'var(--navy)',
borderRadius: '4px', padding: '0.1rem 0.4rem', fontWeight: 700,
letterSpacing: '0.04em',
}}>
PINNED
</span>
)}
<span style={{ fontSize: '0.7rem', background: 'var(--surface-2)', color: 'var(--text-muted)',
borderRadius: '4px', padding: '0.1rem 0.4rem', textTransform: 'uppercase' }}>
<span style={{
fontSize: '0.65rem', background: 'var(--body-bg)', color: 'var(--text-mid)',
borderRadius: '4px', padding: '0.1rem 0.4rem',
textTransform: 'uppercase', letterSpacing: '0.04em',
border: '1px solid var(--card-border)',
}}>
{n.category}
</span>
</div>
<h2 style={{ fontSize: '1rem', marginBottom: '0.4rem' }}>{n.title}</h2>
<p style={{ color: 'var(--text-muted)', fontSize: '0.875rem', lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>
<h2 style={{ fontSize: '0.95rem', fontWeight: 600, color: 'var(--text-dark)', marginBottom: '0.35rem' }}>
{n.title}
</h2>
<p style={{ color: 'var(--text-mid)', fontSize: '0.875rem', lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>
{n.body}
</p>
<p style={{ color: 'var(--text-muted)', fontSize: '0.75rem', marginTop: '0.75rem' }}>
<p style={{ color: 'var(--text-mid)', fontSize: '0.72rem', marginTop: '0.75rem' }}>
{n.author_name} · {new Date(n.created_at).toLocaleDateString('en-GB', {
day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit'
})}
@ -124,12 +142,15 @@ export function NoticeBoard({ user }: { user: User }) {
</p>
</div>
{user.is_admin && (
<div style={{ display: 'flex', gap: '0.4rem', flexShrink: 0 }}>
<div style={{ display: 'flex', gap: '0.35rem', flexShrink: 0 }}>
<button onClick={() => togglePin(n.id)} title={n.pinned ? 'Unpin' : 'Pin'} style={iconBtn}>
{n.pinned ? '📌' : '📍'}
{n.pinned
? <PinOff size={15} strokeWidth={1.75} />
: <Pin size={15} strokeWidth={1.75} />}
</button>
<button onClick={() => deleteNotice(n.id)} title="Delete" style={{ ...iconBtn, color: 'var(--danger)' }}>
<button onClick={() => deleteNotice(n.id)} title="Delete"
style={{ ...iconBtn, color: 'var(--danger)' }}>
<Trash2 size={15} strokeWidth={1.75} />
</button>
</div>
)}
@ -172,11 +193,13 @@ function PostForm({ onPosted }: { onPosted: () => void }) {
return (
<form onSubmit={submit} style={{
background: 'var(--surface)', borderRadius: 'var(--radius)',
background: 'var(--card-bg)', borderRadius: 'var(--radius)',
padding: '1.25rem', marginBottom: '1.25rem',
border: '1px solid var(--gold)', display: 'flex', flexDirection: 'column', gap: '0.75rem',
border: '1px solid var(--gold)',
boxShadow: 'var(--shadow-md)',
display: 'flex', flexDirection: 'column', gap: '0.75rem',
}}>
<h2 style={{ fontSize: '0.9rem', color: 'var(--gold)', marginBottom: '0.25rem' }}>New Notice</h2>
<h2 style={{ fontSize: '0.875rem', fontWeight: 600, color: 'var(--text-dark)' }}>New Notice</h2>
<input value={title} onChange={e => setTitle(e.target.value)}
placeholder="Title" required style={inputStyle} />
<textarea value={body} onChange={e => setBody(e.target.value)}
@ -191,7 +214,8 @@ function PostForm({ onPosted }: { onPosted: () => void }) {
<input type="date" value={expires} onChange={e => setExpires(e.target.value)}
style={{ ...inputStyle, flex: 1 }} title="Expires (optional)" />
</div>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.875rem', cursor: 'pointer' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.875rem',
cursor: 'pointer', color: 'var(--text-dark)' }}>
<input type="checkbox" checked={pinned} onChange={e => setPinned(e.target.checked)} />
Pin to top
</label>
@ -204,18 +228,18 @@ function PostForm({ onPosted }: { onPosted: () => void }) {
}
const inputStyle: React.CSSProperties = {
background: 'var(--navy-dark)', border: '1px solid var(--surface-2)',
borderRadius: '6px', color: 'var(--text)', padding: '0.6rem 0.75rem',
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',
}
const iconBtn: React.CSSProperties = {
background: 'none', border: 'none', fontSize: '1rem',
padding: '0.25rem', borderRadius: '4px', color: 'var(--text-muted)',
lineHeight: 1,
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',
}
const submitBtn: React.CSSProperties = {
background: 'var(--gold)', color: 'var(--navy-dark)', border: 'none',
borderRadius: '6px', padding: '0.625rem', fontSize: '0.9rem', fontWeight: 600,
background: 'var(--gold)', color: 'var(--navy)', border: 'none',
borderRadius: '7px', padding: '0.625rem', fontSize: '0.9rem', fontWeight: 600,
}

View file

@ -1,27 +1,36 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--navy: #1e3a5f;
--navy-dark: #0f1f35;
--gold: #c9a84c;
--gold-light: #e8c96d;
--surface: #1a2d47;
--surface-2: #243d5c;
--text: #e8edf2;
--text-muted: #8ba3bc;
--danger: #e05252;
--radius: 10px;
/* ── Shell chrome (dark) — for standalone login screens ── */
--navy: #1a1a2e;
--navy-dark: #0f0f20;
--gold: #c9a84c;
--gold-light: #e8c96d;
--surface: rgba(255,255,255,0.07);
--surface-2: rgba(255,255,255,0.08);
--text: rgba(255,255,255,0.88);
--text-muted: rgba(255,255,255,0.48);
/* ── Content area (light) ── */
--body-bg: #f4f5f7;
--card-bg: #ffffff;
--card-border: #e4e8ee;
--text-dark: #1e293b;
--text-mid: #64748b;
--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;
--radius: 10px;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
body {
background: var(--navy-dark);
color: var(--text);
background: var(--body-bg);
color: var(--text-dark);
font-family: var(--font);
min-height: 100dvh;
}
button {
cursor: pointer;
font-family: inherit;
}
button { cursor: pointer; font-family: inherit; }