feat(web): apply Sea Haven Ops design system re-theme and layout fixes

Re-theme the MUI app from teal (#0B5A73) to the Sea Haven Ops neutral-navy
structure (#111827) with action-blue (#2563EB) as the sole brand accent, driven
through the theme tokens so all screens update consistently. Cards become
border-driven (no shadow); table headers gray-50; status/priority chips aligned
to design-system token values.

Layout fixes:
- Topbar: square bottom corners (was inheriting MuiPaper radius)
- Sidebar: remove duplicate user tag (already shown in topbar)
- Main: drop redundant ml that double-counted the persistent drawer width
- New Proposal form: center the constrained container (mx: auto)
This commit is contained in:
Adam Moussa 2026-06-12 16:06:47 -04:00
parent 2549ce3afc
commit f502f8afc2
10 changed files with 89 additions and 92 deletions

View file

@ -1,9 +1,6 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { Box, Button, Card, CardContent, Toolbar, Typography } from '@mui/material';
import PeopleIcon from '@mui/icons-material/People';
import { selectSidebarOpen } from './app/slices/uiSlice';
import type { RootState } from './app/store';
import ProtectedRoute, { RoleGuard } from './components/ProtectedRoute';
import Topbar from './components/Topbar';
import Sidebar from './components/Sidebar';
@ -15,11 +12,8 @@ import ProposalDetailPage from './pages/proposals/detail/ProposalDetailPage';
import ProposalListPage from './pages/proposals/list/ProposalListPage';
import AdminDashboard from './pages/admin/dashboard/AdminDashboard';
import AdminWorkspace from './pages/admin/workspace/AdminWorkspace';
import { DRAWER_WIDTH } from './constants';
export default function App() {
const sidebarOpen = useSelector((state: RootState) => selectSidebarOpen(state));
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
@ -36,9 +30,8 @@ export default function App() {
component="main"
sx={{
flexGrow: 1,
minWidth: 0,
p: 1.25,
ml: sidebarOpen ? `${DRAWER_WIDTH}px` : 0,
transition: 'margin-left 250ms ease',
minHeight: '100vh',
}}
>

View file

@ -179,12 +179,6 @@ export default function Sidebar() {
{navItems.map((item) => renderNavItem(item))}
</List>
</Box>
{user && !isCollapsed && (
<Box sx={{ p: 2, mt: 'auto', borderTop: '1px solid #DDE3E8' }}>
<Box sx={{ fontSize: '13px', fontWeight: 500, color: '#1F2933' }}>{user.displayName}</Box>
<Box sx={{ fontSize: '11px', color: '#8A949E' }}>{user.role}</Box>
</Box>
)}
</Drawer>
);
}

View file

@ -35,7 +35,7 @@ export default function Topbar() {
sx={{
background: '#FEF3C7',
color: '#92400E',
border: '1px solid #FCD34D',
border: '1px solid #FDE68A',
height: 22,
borderRadius: '999px',
fontSize: '11px',
@ -55,7 +55,7 @@ export default function Topbar() {
{user && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: '#0E7490', fontSize: 12, fontWeight: 600 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: '#2563EB', fontSize: 12, fontWeight: 600 }}>
{initials}
</Avatar>
<Box sx={{ display: 'flex', flexDirection: 'column', lineHeight: 1.2 }}>

View file

@ -28,13 +28,14 @@ export const STATUS_LABELS: Record<string, string> = {
Revised: 'Revised',
};
// Sea Haven Ops status chips — flat fill + thin border + bold label (tokens/base.css).
export const STATUS_CHIP_STYLES: Record<string, { bgcolor: string; color: string; border: string }> = {
Draft: { bgcolor: '#F1F5F9', color: '#334155', border: '1px solid #CBD5E1' },
Draft: { bgcolor: '#F3F4F6', color: '#374151', border: '1px solid #D1D5DB' },
InReview: { bgcolor: '#E0F2FE', color: '#075985', border: '1px solid #7DD3FC' },
Approved: { bgcolor: '#DCFCE7', color: '#166534', border: '1px solid #86EFAC' },
Sent: { bgcolor: '#DBEAFE', color: '#1E40AF', border: '1px solid #93C5FD' },
Sent: { bgcolor: '#DBEAFE', color: '#1E40AF', border: '1px solid #BFDBFE' },
Revised: { bgcolor: '#F3E8FF', color: '#6B21A8', border: '1px solid #C084FC' },
Rejected: { bgcolor: '#FEE2E2', color: '#991B1B', border: '1px solid #FCA5A5' },
Rejected: { bgcolor: '#FEE2E2', color: '#991B1B', border: '1px solid #FECACA' },
};
export const DRAWER_WIDTH = 216;
@ -42,13 +43,13 @@ export const DRAWER_WIDTH = 216;
export const PRIORITY_LABELS: Record<string, string> = {
Standard: 'Standard',
Urgent: 'Urgent',
Emergency: 'Emergency Dispatch',
Emergency: 'Emergency',
};
export const PRIORITY_CHIP_STYLES: Record<string, { bgcolor: string; color: string; border: string }> = {
Standard: { bgcolor: '#E2E8F0', color: '#334155', border: '1px solid #CBD5E1' },
Urgent: { bgcolor: '#FFEDD5', color: '#9A3412', border: '1px solid #FDBA74' },
Emergency: { bgcolor: '#FEE2E2', color: '#991B1B', border: '1px solid #F87171' },
Standard: { bgcolor: '#F3F4F6', color: '#374151', border: '1px solid #D1D5DB' },
Urgent: { bgcolor: '#FFEDD5', color: '#9A3412', border: '1px solid #FED7AA' },
Emergency: { bgcolor: '#FEE2E2', color: '#991B1B', border: '1px solid #FECACA' },
};
export const PRIORITY_ROW_SX: Record<string, object> = {

View file

@ -50,13 +50,13 @@ const TAB_STATUSES = ['', 'InReview', 'Approved', 'Sent', 'Revised'] as const;
function StatCard({ icon, label, value, color, subtitle }: { icon: React.ReactNode; label: string; value: string; color: string; subtitle?: string }) {
return (
<Card sx={{ borderRadius: '8px', transition: 'transform 0.2s, box-shadow 0.2s', '&:hover': { transform: 'translateY(-2px)', boxShadow: '0 4px 8px rgba(0,0,0,0.15)' } }}>
<Card sx={{ borderRadius: '8px', transition: 'border-color 120ms ease', '&:hover': { borderColor: '#9CA3AF' } }}>
<CardContent sx={{ textAlign: 'center', py: 2.5 }}>
<Box sx={{ color, display: 'flex', justifyContent: 'center', mb: 1 }}>{icon}</Box>
<Typography sx={{ fontSize: '12px', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, mb: 1 }}>
{label}
</Typography>
<Typography sx={{ fontSize: '36px', fontWeight: 800, color: '#073B4C', letterSpacing: '-0.03em' }}>
<Typography sx={{ fontSize: '30px', fontWeight: 700, color: '#111827', letterSpacing: '-0.02em' }}>
{value}
</Typography>
{subtitle && (

View file

@ -310,7 +310,7 @@ export default function AdminWorkspace() {
component="button"
underline="hover"
onClick={() => guardedNavigate('/admin')}
sx={{ fontSize: 14, color: '#0B5A73', cursor: 'pointer' }}
sx={{ fontSize: 14, color: '#2563EB', cursor: 'pointer' }}
>
Admin Queue
</Link>
@ -452,7 +452,7 @@ export default function AdminWorkspace() {
</Grid>
{/* Action Bar */}
<Card sx={{ mt: 2, position: 'sticky', bottom: 0, zIndex: 10, bgcolor: 'rgba(255,255,255,0.96)', backdropFilter: 'blur(8px)', borderTop: '1px solid #D6E0EA', boxShadow: '0 -4px 12px rgba(15,23,42,0.06)', border: 'none' }}>
<Card sx={{ mt: 2, position: 'sticky', bottom: 0, zIndex: 10, bgcolor: 'rgba(255,255,255,0.96)', backdropFilter: 'blur(8px)', borderTop: '1px solid #E5E7EB', boxShadow: '0 -4px 12px rgba(17,24,39,0.06)', border: 'none' }}>
<CardContent sx={{ py: 1.5, display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
{dirty && (
<Alert severity="info" sx={{ py: 0, flexGrow: 1 }}>Unsaved changes</Alert>

View file

@ -67,7 +67,7 @@ export default function LoginPage() {
alignItems: 'center',
justifyContent: 'flex-start',
pt: '7%',
background: 'linear-gradient(135deg, #073B4C 0%, #172554 100%)',
background: 'linear-gradient(135deg, #111827 0%, #172554 100%)',
}}
>
<Box sx={{ width: '100%', maxWidth: 420, mx: 2 }}>
@ -79,22 +79,22 @@ export default function LoginPage() {
alignItems: 'center',
justifyContent: 'center',
bgcolor: '#fff',
borderBottom: '1px solid #D6E0EA',
borderBottom: '1px solid #E5E7EB',
px: 3,
}}
>
<Typography sx={{ fontSize: '24px', fontWeight: 600, color: '#0B4F6C' }}>
<Typography sx={{ fontSize: '24px', fontWeight: 700, color: '#111827', letterSpacing: '-0.02em' }}>
Sea Haven Industries
</Typography>
</Box>
<Box
sx={{
bgcolor: '#07384D',
bgcolor: '#111827',
height: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderBottom: '1px solid #D6E0EA',
borderBottom: '1px solid #E5E7EB',
}}
>
<Typography sx={{ fontSize: '14px', fontWeight: 700, color: '#fff' }}>
@ -120,7 +120,7 @@ export default function LoginPage() {
startIcon={<AdminPanelSettingsIcon />}
onClick={() => handleDevLogin('SysAdmin')}
disabled={loading}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#0B5A73', '&:hover': { bgcolor: '#084A5F' } }}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#2563EB', '&:hover': { bgcolor: '#1D4ED8' } }}
>
<Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>SysAdmin</Box>
@ -134,7 +134,7 @@ export default function LoginPage() {
startIcon={<EngineeringIcon />}
onClick={() => handleDevLogin('Admin')}
disabled={loading}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#0B5A73', '&:hover': { bgcolor: '#084A5F' } }}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#2563EB', '&:hover': { bgcolor: '#1D4ED8' } }}
>
<Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>Admin</Box>
@ -148,7 +148,7 @@ export default function LoginPage() {
startIcon={<PersonIcon />}
onClick={() => handleDevLogin('Dispatcher')}
disabled={loading}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#0B5A73', '&:hover': { bgcolor: '#084A5F' } }}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#2563EB', '&:hover': { bgcolor: '#1D4ED8' } }}
>
<Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>Dispatcher</Box>
@ -172,7 +172,7 @@ export default function LoginPage() {
fullWidth
startIcon={<GoogleIcon />}
href={buildLoginUrl()}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#0B5A73', '&:hover': { bgcolor: '#084A5F' } }}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#2563EB', '&:hover': { bgcolor: '#1D4ED8' } }}
>
Sign in with Google
</Button>

View file

@ -32,13 +32,13 @@ import { useDocumentTitle } from '../../hooks/useDocumentTitle';
function KpiCard({ icon, label, value, subtitle }: { icon: React.ReactNode; label: string; value: string; subtitle?: string }) {
return (
<Card sx={{ borderRadius: '8px', transition: 'transform 0.2s, box-shadow 0.2s', '&:hover': { transform: 'translateY(-2px)', boxShadow: '0 4px 8px rgba(0,0,0,0.15)' } }}>
<Card sx={{ borderRadius: '8px', transition: 'border-color 120ms ease', '&:hover': { borderColor: '#9CA3AF' } }}>
<CardContent sx={{ textAlign: 'center', py: 2.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 1, color: '#0B5A73' }}>{icon}</Box>
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 1, color: '#2563EB' }}>{icon}</Box>
<Typography sx={{ fontSize: '12px', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, mb: 1 }}>
{label}
</Typography>
<Typography sx={{ fontSize: '36px', fontWeight: 800, color: '#073B4C', letterSpacing: '-0.03em' }}>
<Typography sx={{ fontSize: '30px', fontWeight: 700, color: '#111827', letterSpacing: '-0.02em' }}>
{value}
</Typography>
{subtitle && (

View file

@ -176,7 +176,7 @@ export default function ProposalFormPage() {
}, [form.workOrderNumber, form.customerAddress, form.serviceCategory, form.scopeOfWork, customCategory]);
return (
<Box sx={{ maxWidth: 1200 }}>
<Box sx={{ maxWidth: 1200, mx: 'auto' }}>
<Typography variant="h5" sx={{ mb: 3 }}>
New Proposal Request
</Typography>

View file

@ -1,53 +1,56 @@
import { createTheme } from '@mui/material/styles';
// Sea Haven Ops design system — flat, dense, border-driven enterprise UI.
// Neutral navy/slate/gray structure; action-blue #2563EB is the only brand accent.
// Tokens mirror the Sea Haven Ops design bundle (tokens/colors.css, spacing.css, typography.css).
export const theme = createTheme({
palette: {
primary: {
main: '#0B5A73',
light: '#0E7490',
dark: '#073B4C',
main: '#2563EB', // action blue — primary buttons, links, selected
light: '#3B82F6',
dark: '#1D4ED8', // contained-button hover/pressed
contrastText: '#FFFFFF',
},
secondary: {
main: '#475569',
light: '#64748B',
dark: '#334155',
main: '#4B5563', // slate-600
light: '#6B7280',
dark: '#374151',
contrastText: '#FFFFFF',
},
success: {
main: '#2E7D32',
main: '#16A34A',
light: '#DCFCE7',
dark: '#1B5E20',
dark: '#166534',
contrastText: '#FFFFFF',
},
warning: {
main: '#F97316',
main: '#D97706', // orange — Urgent
light: '#FFEDD5',
dark: '#C2410C',
dark: '#9A3412',
contrastText: '#FFFFFF',
},
error: {
main: '#DC2626',
main: '#DC2626', // red — Emergency / overdue / destructive
light: '#FEE2E2',
dark: '#991B1B',
contrastText: '#FFFFFF',
},
info: {
main: '#0284C7',
light: '#E0F2FE',
dark: '#075985',
main: '#2563EB',
light: '#DBEAFE',
dark: '#1E40AF',
contrastText: '#FFFFFF',
},
background: {
default: '#F4F7FA',
default: '#F3F4F6', // gray-100 page background
paper: '#FFFFFF',
},
text: {
primary: '#1E293B',
secondary: '#475569',
disabled: '#94A3B8',
primary: '#111827', // navy-900
secondary: '#4B5563', // slate-600
disabled: '#9CA3AF', // gray-400
},
divider: '#E2E8F0',
divider: '#E5E7EB', // gray-200
},
typography: {
@ -60,12 +63,12 @@ export const theme = createTheme({
letterSpacing: '-0.02em',
},
h5: {
fontWeight: 700,
fontWeight: 600,
fontSize: '18px',
lineHeight: 1.3,
},
h6: {
fontWeight: 700,
fontWeight: 600,
fontSize: '16px',
lineHeight: 1.4,
},
@ -85,11 +88,11 @@ export const theme = createTheme({
caption: {
fontSize: '12px',
lineHeight: 1.35,
color: '#64748B',
color: '#6B7280',
},
button: {
fontSize: '14px',
fontWeight: 700,
fontWeight: 600,
textTransform: 'none' as const,
},
overline: {
@ -98,12 +101,12 @@ export const theme = createTheme({
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase' as const,
color: '#94A3B8',
color: '#6B7280',
},
},
shape: {
borderRadius: 8,
borderRadius: 6,
},
components: {
@ -111,7 +114,7 @@ export const theme = createTheme({
styleOverrides: {
body: {
fontFeatureSettings: '"tnum"',
backgroundColor: '#F4F7FA',
backgroundColor: '#F3F4F6',
},
},
},
@ -122,8 +125,9 @@ export const theme = createTheme({
},
styleOverrides: {
root: {
backgroundColor: '#073B4C',
backgroundColor: '#111827', // navy-900
boxShadow: 'none',
borderRadius: 0,
borderBottom: '1px solid rgba(255,255,255,0.08)',
},
},
@ -133,7 +137,7 @@ export const theme = createTheme({
styleOverrides: {
paper: {
backgroundColor: '#FFFFFF',
borderRight: '1px solid #DDE3E8',
borderRight: '1px solid #E5E7EB', // border-subtle
width: 216,
},
},
@ -144,9 +148,10 @@ export const theme = createTheme({
elevation: 0,
},
styleOverrides: {
// Cards are border-driven — no shadow (overlays only).
root: {
border: '1px solid #D6E0EA',
boxShadow: '0 1px 2px rgba(15, 23, 42, 0.06)',
border: '1px solid #E5E7EB',
boxShadow: 'none',
borderRadius: 8,
},
},
@ -182,10 +187,11 @@ export const theme = createTheme({
},
},
outlined: {
borderColor: '#BFD0DF',
borderColor: '#D1D5DB', // border-default
color: '#111827',
'&:hover': {
borderColor: '#0B5A73',
backgroundColor: '#E6F3F7',
borderColor: '#9CA3AF', // border-strong
backgroundColor: '#F9FAFB', // gray-50
},
},
},
@ -210,13 +216,13 @@ export const theme = createTheme({
borderRadius: 6,
minHeight: 38,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: '#BFD0DF',
borderColor: '#D1D5DB',
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: '#0E7490',
borderColor: '#9CA3AF',
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: '#0B5A73',
borderColor: '#2563EB',
borderWidth: 2,
},
},
@ -231,9 +237,9 @@ export const theme = createTheme({
styleOverrides: {
root: {
fontSize: 13,
color: '#64748B',
color: '#6B7280',
'&.Mui-focused': {
color: '#0B5A73',
color: '#2563EB',
},
},
},
@ -243,13 +249,13 @@ export const theme = createTheme({
styleOverrides: {
root: {
'& .MuiTableCell-head': {
color: '#334155',
color: '#374151', // slate-700
fontWeight: 700,
fontSize: 13,
paddingTop: 12,
paddingBottom: 12,
borderBottom: '1px solid #DDE3E8',
backgroundColor: '#F8FAFC',
borderBottom: '1px solid #E5E7EB',
backgroundColor: '#F9FAFB', // gray-50
whiteSpace: 'nowrap',
},
},
@ -259,10 +265,10 @@ export const theme = createTheme({
MuiTableCell: {
styleOverrides: {
root: {
color: '#1E293B',
color: '#111827',
fontSize: 14,
padding: '12px 14px',
borderBottom: '1px solid #E2E8F0',
borderBottom: '1px solid #E5E7EB',
},
},
},
@ -271,7 +277,7 @@ export const theme = createTheme({
styleOverrides: {
root: {
'&:hover': {
backgroundColor: '#F8FAFC',
backgroundColor: '#F9FAFB',
},
},
},
@ -307,17 +313,20 @@ export const theme = createTheme({
marginLeft: 8,
marginRight: 8,
marginBottom: 4,
color: '#374151',
color: '#4B5563',
'&.Mui-selected': {
backgroundColor: '#E6F2F8',
borderLeft: '3px solid #0B80B5',
color: '#1F2933',
backgroundColor: '#EFF6FF', // blue-50
borderLeft: '3px solid #2563EB',
color: '#111827',
'& .MuiListItemIcon-root': {
color: '#2563EB',
},
'&:hover': {
backgroundColor: '#DCECF5',
backgroundColor: '#DBEAFE', // blue-100
},
},
'&:hover': {
backgroundColor: '#F1F5F9',
backgroundColor: '#F3F4F6', // gray-100
},
},
},
@ -326,7 +335,7 @@ export const theme = createTheme({
MuiListItemIcon: {
styleOverrides: {
root: {
color: '#8A949E',
color: '#9CA3AF', // gray-400 — inactive nav icon
minWidth: 36,
},
},
@ -336,7 +345,7 @@ export const theme = createTheme({
styleOverrides: {
tooltip: {
fontSize: 12,
backgroundColor: '#1F2933',
backgroundColor: '#1F2937', // navy-800
},
},
},