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" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"lucide-react": "^1.23.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },

View file

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

View file

@ -1,4 +1,5 @@
import { useEffect, useReducer, useState } from 'react' import { useEffect, useReducer, useState } from 'react'
import { Pin, PinOff, Trash2, Plus, X } from 'lucide-react'
interface Notice { interface Notice {
id: number id: number
@ -17,7 +18,7 @@ const CATEGORIES = [
{ value: 'all', label: 'All' }, { value: 'all', label: 'All' },
{ value: 'general', label: 'General' }, { value: 'general', label: 'General' },
{ value: 'kitchen', label: 'Kitchen' }, { value: 'kitchen', label: 'Kitchen' },
{ value: 'housekeeping', label: 'Housekeeping' }, { value: 'housekeeping',label: 'Housekeeping' },
{ value: 'management', label: 'Management' }, { value: 'management', label: 'Management' },
] ]
@ -46,24 +47,27 @@ export function NoticeBoard({ user }: { user: User }) {
} }
return ( return (
<div style={{ maxWidth: '720px', margin: '0 auto', padding: '1rem' }}> <div style={{ maxWidth: '720px', margin: '0 auto', padding: '1.5rem 1rem' }}>
<header style={{ <header style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: '1.25rem', paddingBottom: '1rem', marginBottom: '1.25rem',
borderBottom: '1px solid var(--surface-2)',
}}> }}>
<div> <div>
<h1 style={{ fontSize: '1.25rem', color: 'var(--gold)' }}>Noticeboard</h1> <h1 style={{ fontSize: '1.15rem', fontWeight: 700, color: 'var(--text-dark)' }}>
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{user.name}</p> Noticeboard
</h1>
<p style={{ fontSize: '0.75rem', color: 'var(--text-mid)', marginTop: '0.1rem' }}>{user.name}</p>
</div> </div>
{user.is_admin && ( {user.is_admin && (
<button onClick={() => setShowForm(v => !v)} style={{ <button onClick={() => setShowForm(v => !v)} style={{
background: showForm ? 'var(--surface-2)' : 'var(--gold)', background: showForm ? 'var(--card-bg)' : 'var(--gold)',
color: showForm ? 'var(--text)' : 'var(--navy-dark)', color: showForm ? 'var(--text-mid)' : 'var(--navy)',
border: 'none', borderRadius: '6px', padding: '0.5rem 1rem', border: showForm ? '1px solid var(--card-border)' : 'none',
fontSize: '0.875rem', fontWeight: 600, 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> </button>
)} )}
</header> </header>
@ -72,13 +76,15 @@ export function NoticeBoard({ user }: { user: User }) {
<PostForm onPosted={() => { setShowForm(false); forceRefresh() }} /> <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 => ( {CATEGORIES.map(c => (
<button key={c.value} onClick={() => setCategory(c.value)} style={{ <button key={c.value} onClick={() => setCategory(c.value)} style={{
background: category === c.value ? 'var(--gold)' : 'var(--surface)', background: category === c.value ? 'var(--navy)' : 'var(--card-bg)',
color: category === c.value ? 'var(--navy-dark)' : 'var(--text-muted)', color: category === c.value ? 'var(--gold)' : 'var(--text-mid)',
border: '1px solid var(--surface-2)', borderRadius: '20px', border: `1px solid ${category === c.value ? 'var(--navy)' : 'var(--card-border)'}`,
padding: '0.3rem 0.875rem', fontSize: '0.8rem', fontWeight: 600, borderRadius: '20px', padding: '0.3rem 0.875rem',
fontSize: '0.78rem', fontWeight: 500,
}}> }}>
{c.label} {c.label}
</button> </button>
@ -86,7 +92,7 @@ export function NoticeBoard({ user }: { user: User }) {
</div> </div>
{notices.length === 0 && ( {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. No notices yet.
</p> </p>
)} )}
@ -94,29 +100,41 @@ export function NoticeBoard({ user }: { user: User }) {
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{notices.map(n => ( {notices.map(n => (
<div key={n.id} style={{ <div key={n.id} style={{
background: 'var(--surface)', borderRadius: 'var(--radius)', background: 'var(--card-bg)',
borderRadius: 'var(--radius)',
padding: '1rem 1.125rem', 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={{ display: 'flex', alignItems: 'flex-start', gap: '0.5rem' }}>
<div style={{ flex: 1 }}> <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 && ( {n.pinned && (
<span style={{ fontSize: '0.7rem', background: 'var(--gold)', color: 'var(--navy-dark)', <span style={{
borderRadius: '4px', padding: '0.1rem 0.4rem', fontWeight: 700 }}> fontSize: '0.65rem', background: 'var(--gold)', color: 'var(--navy)',
borderRadius: '4px', padding: '0.1rem 0.4rem', fontWeight: 700,
letterSpacing: '0.04em',
}}>
PINNED PINNED
</span> </span>
)} )}
<span style={{ fontSize: '0.7rem', background: 'var(--surface-2)', color: 'var(--text-muted)', <span style={{
borderRadius: '4px', padding: '0.1rem 0.4rem', textTransform: 'uppercase' }}> 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} {n.category}
</span> </span>
</div> </div>
<h2 style={{ fontSize: '1rem', marginBottom: '0.4rem' }}>{n.title}</h2> <h2 style={{ fontSize: '0.95rem', fontWeight: 600, color: 'var(--text-dark)', marginBottom: '0.35rem' }}>
<p style={{ color: 'var(--text-muted)', fontSize: '0.875rem', lineHeight: 1.5, whiteSpace: 'pre-wrap' }}> {n.title}
</h2>
<p style={{ color: 'var(--text-mid)', fontSize: '0.875rem', lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>
{n.body} {n.body}
</p> </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', { {n.author_name} · {new Date(n.created_at).toLocaleDateString('en-GB', {
day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit'
})} })}
@ -124,12 +142,15 @@ export function NoticeBoard({ user }: { user: User }) {
</p> </p>
</div> </div>
{user.is_admin && ( {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}> <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>
<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> </button>
</div> </div>
)} )}
@ -172,11 +193,13 @@ function PostForm({ onPosted }: { onPosted: () => void }) {
return ( return (
<form onSubmit={submit} style={{ <form onSubmit={submit} style={{
background: 'var(--surface)', borderRadius: 'var(--radius)', background: 'var(--card-bg)', borderRadius: 'var(--radius)',
padding: '1.25rem', marginBottom: '1.25rem', 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)} <input value={title} onChange={e => setTitle(e.target.value)}
placeholder="Title" required style={inputStyle} /> placeholder="Title" required style={inputStyle} />
<textarea value={body} onChange={e => setBody(e.target.value)} <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)} <input type="date" value={expires} onChange={e => setExpires(e.target.value)}
style={{ ...inputStyle, flex: 1 }} title="Expires (optional)" /> style={{ ...inputStyle, flex: 1 }} title="Expires (optional)" />
</div> </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)} /> <input type="checkbox" checked={pinned} onChange={e => setPinned(e.target.checked)} />
Pin to top Pin to top
</label> </label>
@ -204,18 +228,18 @@ function PostForm({ onPosted }: { onPosted: () => void }) {
} }
const inputStyle: React.CSSProperties = { const inputStyle: React.CSSProperties = {
background: 'var(--navy-dark)', border: '1px solid var(--surface-2)', background: 'var(--body-bg)', border: '1px solid var(--card-border)',
borderRadius: '6px', color: 'var(--text)', padding: '0.6rem 0.75rem', borderRadius: '7px', color: 'var(--text-dark)', padding: '0.6rem 0.75rem',
fontSize: '0.9rem', width: '100%', outline: 'none', fontSize: '0.9rem', width: '100%', outline: 'none',
} }
const iconBtn: React.CSSProperties = { const iconBtn: React.CSSProperties = {
background: 'none', border: 'none', fontSize: '1rem', background: 'var(--body-bg)', border: '1px solid var(--card-border)',
padding: '0.25rem', borderRadius: '4px', color: 'var(--text-muted)', borderRadius: '6px', padding: '0.3rem', color: 'var(--text-mid)',
lineHeight: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
} }
const submitBtn: React.CSSProperties = { const submitBtn: React.CSSProperties = {
background: 'var(--gold)', color: 'var(--navy-dark)', border: 'none', background: 'var(--gold)', color: 'var(--navy)', border: 'none',
borderRadius: '6px', padding: '0.625rem', fontSize: '0.9rem', fontWeight: 600, 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; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { :root {
--navy: #1e3a5f; /* ── Shell chrome (dark) — for standalone login screens ── */
--navy-dark: #0f1f35; --navy: #1a1a2e;
--navy-dark: #0f0f20;
--gold: #c9a84c; --gold: #c9a84c;
--gold-light: #e8c96d; --gold-light: #e8c96d;
--surface: #1a2d47; --surface: rgba(255,255,255,0.07);
--surface-2: #243d5c; --surface-2: rgba(255,255,255,0.08);
--text: #e8edf2; --text: rgba(255,255,255,0.88);
--text-muted: #8ba3bc; --text-muted: rgba(255,255,255,0.48);
--danger: #e05252;
/* ── 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; --radius: 10px;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
} }
body { body {
background: var(--navy-dark); background: var(--body-bg);
color: var(--text); color: var(--text-dark);
font-family: var(--font); font-family: var(--font);
min-height: 100dvh; min-height: 100dvh;
} }
button { button { cursor: pointer; font-family: inherit; }
cursor: pointer;
font-family: inherit;
}