feat(web): adopt SHOC design system and shell layout (ADR 0003) (#221)

Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:

- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
  DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
  shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
  adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
  tuple, component overrides; MUI v9 slot renames expressed as class
  selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
  scrolling main); sidebar 244px/76px collapse with brand header row,
  grouped nav, SHOC active treatment (white card + 3px accent bar);
  topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
  sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
  remaining hardcoded hexes replaced with tokens; lucide-react for
  shell/nav icons per SHOC convention

Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
This commit is contained in:
Adam Moussa 2026-07-13 19:06:51 -04:00 • committed by GitHub
parent a4b09eb0ad
commit 8ce2a5cd8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1134 additions and 673 deletions

View file

@ -4,9 +4,6 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Proposal System - Sea Haven Industries</title> <title>Proposal System - Sea Haven Industries</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

40
web/package-lock.json generated
View file

@ -10,11 +10,15 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@fontsource/dm-sans": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/montserrat": "^5.2.8",
"@mui/icons-material": "^9.2.0", "@mui/icons-material": "^9.2.0",
"@mui/material": "^9.1.1", "@mui/material": "^9.1.1",
"@reduxjs/toolkit": "^2.11.2", "@reduxjs/toolkit": "^2.11.2",
"@tanstack/react-query": "^5.101.2", "@tanstack/react-query": "^5.101.2",
"axios": "^1.18.1", "axios": "^1.18.1",
"lucide-react": "^1.24.0",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
@ -582,6 +586,33 @@
} }
} }
}, },
"node_modules/@fontsource/dm-sans": {
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/@fontsource/dm-sans/-/dm-sans-5.2.8.tgz",
"integrity": "sha512-tlovG42m9ESG28WiHpLq3F5umAlm64rv0RkqTbYowRn70e9OlRr5a3yTJhrhrY+k5lftR/OFJjPzOLQzk8EfCA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/jetbrains-mono": {
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz",
"integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/montserrat": {
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/@fontsource/montserrat/-/montserrat-5.2.8.tgz",
"integrity": "sha512-xTjLxSbSfCycDB0pwmNsfNvdfWPaDaRQ2LC6yt/ZI7SdvXG52zHnzNYC/09mzuAuWNJyShkteutfCoDgym56hQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@ -2943,6 +2974,15 @@
"node": "20 || >=22" "node": "20 || >=22"
} }
}, },
"node_modules/lucide-react": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz",
"integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/lz-string": { "node_modules/lz-string": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",

View file

@ -13,11 +13,15 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@fontsource/dm-sans": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/montserrat": "^5.2.8",
"@mui/icons-material": "^9.2.0", "@mui/icons-material": "^9.2.0",
"@mui/material": "^9.1.1", "@mui/material": "^9.1.1",
"@reduxjs/toolkit": "^2.11.2", "@reduxjs/toolkit": "^2.11.2",
"@tanstack/react-query": "^5.101.2", "@tanstack/react-query": "^5.101.2",
"axios": "^1.18.1", "axios": "^1.18.1",
"lucide-react": "^1.24.0",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",

View file

@ -1,5 +1,5 @@
import { Routes, Route, Navigate } from 'react-router-dom'; import { Routes, Route, Navigate } from 'react-router-dom';
import { Box, Button, Card, CardContent, Toolbar, Typography } from '@mui/material'; import { Box, Button, Card, CardContent, Typography } from '@mui/material';
import PeopleIcon from '@mui/icons-material/People'; import PeopleIcon from '@mui/icons-material/People';
import ProtectedRoute, { RoleGuard } from './components/ProtectedRoute'; import ProtectedRoute, { RoleGuard } from './components/ProtectedRoute';
import Topbar from './components/Topbar'; import Topbar from './components/Topbar';
@ -25,20 +25,21 @@ export default function App() {
path="/*" path="/*"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Box sx={{ display: 'flex' }}> {/* SHOC shell composition (ADR 0003): sidebar column + [topbar / scrolling main] */}
<Topbar /> <Box sx={{ display: 'flex', height: '100vh', width: '100%', overflow: 'hidden', backgroundColor: 'var(--background)' }}>
<Sidebar /> <Sidebar />
<Box <Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0, height: '100vh', overflow: 'hidden' }}>
component="main" <Topbar />
sx={{ <Box
flexGrow: 1, component="main"
minWidth: 0, sx={{
p: 1.25, flex: 1,
minHeight: '100vh', overflow: 'auto',
}} p: 1.25,
> backgroundColor: 'var(--color-bg-page)',
<Toolbar sx={{ minHeight: 64 }} /> }}
<Routes> >
<Routes>
<Route path="/" element={<Dashboard />} /> <Route path="/" element={<Dashboard />} />
<Route path="/proposals" element={<ProposalListPage />} /> <Route path="/proposals" element={<ProposalListPage />} />
<Route path="/proposals/new" element={<ProposalFormPage />} /> <Route path="/proposals/new" element={<ProposalFormPage />} />
@ -55,7 +56,7 @@ export default function App() {
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 6 }}> <Box sx={{ display: 'flex', justifyContent: 'center', pt: 6 }}>
<Card sx={{ maxWidth: 520, p: 4, textAlign: 'center' }}> <Card sx={{ maxWidth: 520, p: 4, textAlign: 'center' }}>
<CardContent> <CardContent>
<PeopleIcon sx={{ fontSize: 48, color: '#94A3B8', mb: 2 }} /> <PeopleIcon sx={{ fontSize: 48, color: 'var(--color-text-muted)', mb: 2 }} />
<Typography variant="h5" sx={{ mb: 1 }}>User Management</Typography> <Typography variant="h5" sx={{ mb: 1 }}>User Management</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
Users and roles are currently managed in AWS Cognito. Contact the system administrator to update access. Users and roles are currently managed in AWS Cognito. Contact the system administrator to update access.
@ -75,8 +76,9 @@ export default function App() {
</RoleGuard> </RoleGuard>
} /> } />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
</Box>
</Box> </Box>
</Box> </Box>
</ProtectedRoute> </ProtectedRoute>

View file

@ -1,48 +1,45 @@
// App shell sidebar — SHOC design system port (ADR 0003, mirrors app-sidebar.tsx).
// Expanded 244px / collapsed 76px; brand header row; grouped nav with SHOC
// active treatment (white card bg + shadow + 3px left accent bar).
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { Drawer, Box, Tooltip, useMediaQuery, useTheme } from '@mui/material';
import { import {
Drawer, LayoutDashboard,
List, FileText,
ListItemButton, CirclePlus,
ListItemIcon, ClipboardList,
ListItemText, FolderOpen,
Toolbar, Building2,
Box, BookOpen,
Typography, Users,
Tooltip, type LucideIcon,
useMediaQuery, } from 'lucide-react';
useTheme,
} from '@mui/material';
import DashboardIcon from '@mui/icons-material/Dashboard';
import DescriptionIcon from '@mui/icons-material/Description';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import AssignmentIcon from '@mui/icons-material/Assignment';
import PeopleIcon from '@mui/icons-material/People';
import BusinessIcon from '@mui/icons-material/Business';
import MenuBookIcon from '@mui/icons-material/MenuBook';
import { selectSidebarOpen, setSidebarOpen } from '../app/slices/uiSlice'; import { selectSidebarOpen, setSidebarOpen } from '../app/slices/uiSlice';
import { selectUser } from '../app/slices/authSlice'; import { selectUser } from '../app/slices/authSlice';
import type { RootState } from '../app/store'; import type { RootState } from '../app/store';
import { DRAWER_WIDTH } from '../constants'; import { DRAWER_WIDTH } from '../constants';
import BrandLockup, { BrandMark } from './brand/BrandLockup';
const COLLAPSED_WIDTH = 72; const COLLAPSED_WIDTH = 76; // --spacing-sidebar-collapsed
const dispatcherNav = [ type NavEntry = { label: string; path: string; icon: LucideIcon };
{ label: 'Dashboard', path: '/', icon: <DashboardIcon fontSize="small" /> },
{ label: 'My Proposals', path: '/proposals', icon: <DescriptionIcon fontSize="small" /> }, const dispatcherNav: NavEntry[] = [
{ label: 'New Proposal', path: '/proposals/new', icon: <AddCircleIcon fontSize="small" /> }, { label: 'Dashboard', path: '/', icon: LayoutDashboard },
{ label: 'My Proposals', path: '/proposals', icon: FileText },
{ label: 'New Proposal', path: '/proposals/new', icon: CirclePlus },
]; ];
const adminNav = [ const adminNav: NavEntry[] = [
{ label: 'Admin Queue', path: '/admin', icon: <AdminPanelSettingsIcon fontSize="small" /> }, { label: 'Admin Queue', path: '/admin', icon: ClipboardList },
{ label: 'All Proposals', path: '/admin/proposals', icon: <AssignmentIcon fontSize="small" /> }, { label: 'All Proposals', path: '/admin/proposals', icon: FolderOpen },
{ label: 'Customers', path: '/admin/customers', icon: <BusinessIcon fontSize="small" /> }, { label: 'Customers', path: '/admin/customers', icon: Building2 },
{ label: 'Pricing Library', path: '/admin/pricing-library', icon: <MenuBookIcon fontSize="small" /> }, { label: 'Pricing Library', path: '/admin/pricing-library', icon: BookOpen },
]; ];
const sysadminNav = [ const sysadminNav: NavEntry[] = [
{ label: 'Users', path: '/admin/users', icon: <PeopleIcon fontSize="small" /> }, { label: 'Users', path: '/admin/users', icon: Users },
]; ];
const ALL_NAV_PATHS = new Set( const ALL_NAV_PATHS = new Set(
@ -56,6 +53,88 @@ function isNavActive(pathname: string, itemPath: string): boolean {
return pathname.startsWith(itemPath + '/'); return pathname.startsWith(itemPath + '/');
} }
function NavItem({
item,
active,
collapsed,
onClick,
}: {
item: NavEntry;
active: boolean;
collapsed: boolean;
onClick: () => void;
}) {
const Icon = item.icon;
const row = (
<Box
component="button"
onClick={onClick}
sx={{
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: collapsed ? 'center' : 'flex-start',
gap: '12px',
width: '100%',
height: 40,
mb: '4px',
px: collapsed ? 0 : '12px',
border: 'none',
cursor: 'pointer',
borderRadius: 'var(--radius-md)',
fontFamily: 'var(--font-sans)',
fontSize: 'var(--text-base)',
fontWeight: active ? 600 : 500,
textAlign: 'left',
color: active ? 'var(--primary)' : 'var(--foreground)',
backgroundColor: active ? 'var(--color-sidebar-item-active-bg)' : 'transparent',
boxShadow: active ? 'var(--color-sidebar-item-active-shadow)' : 'none',
transition: 'all var(--duration-fast) var(--ease-default)',
'&:hover': active
? {}
: {
color: 'var(--primary-hover)',
backgroundColor: 'var(--sidebar-accent)',
},
'& svg': {
color: active ? 'var(--primary)' : 'var(--muted-foreground)',
flexShrink: 0,
},
'&:hover svg': active ? {} : { color: 'var(--primary-hover)' },
}}
>
{active && (
<Box
sx={{
position: 'absolute',
left: 0,
top: '8px',
bottom: '8px',
width: '3px',
borderRadius: '0 3px 3px 0',
backgroundColor: 'var(--primary)',
}}
/>
)}
<Icon size={19} />
{!collapsed && (
<Box component="span" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{item.label}
</Box>
)}
</Box>
);
if (collapsed) {
return (
<Tooltip key={item.path} title={item.label} placement="right" arrow>
{row}
</Tooltip>
);
}
return row;
}
export default function Sidebar() { export default function Sidebar() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -65,14 +144,14 @@ export default function Sidebar() {
const user = useSelector((state: RootState) => selectUser(state)); const user = useSelector((state: RootState) => selectUser(state));
const isMobile = useMediaQuery(theme.breakpoints.down('md')); // <900px const isMobile = useMediaQuery(theme.breakpoints.down('md')); // <900px
const isCollapsed = useMediaQuery(theme.breakpoints.between('md', 'lg')); // 900–1200px
const isAdmin = user?.role === 'Admin' || user?.role === 'SysAdmin'; const isAdmin = user?.role === 'Admin' || user?.role === 'SysAdmin';
const isSysAdmin = user?.role === 'SysAdmin'; const isSysAdmin = user?.role === 'SysAdmin';
const navItems = dispatcherNav;
const drawerVariant = isMobile ? 'temporary' : 'persistent'; // Desktop: hamburger toggles expanded (244) ↔ collapsed (76).
const effectiveWidth = isCollapsed ? COLLAPSED_WIDTH : DRAWER_WIDTH; // Mobile: temporary drawer, hamburger opens/closes it.
const collapsed = !isMobile && !open;
const width = collapsed ? COLLAPSED_WIDTH : DRAWER_WIDTH;
const handleNavClick = (path: string) => { const handleNavClick = (path: string) => {
navigate(path); navigate(path);
@ -81,107 +160,69 @@ export default function Sidebar() {
} }
}; };
const handleDrawerClose = () => { const groupLabel = (label: string) => (
dispatch(setSidebarOpen(false)); <Box className="text-nav-group" sx={{ px: '12px', pt: '10px', pb: '6px' }}>
}; {label}
</Box>
);
const renderNavItem = (item: { label: string; path: string; icon: React.ReactNode }) => { const groupDivider = (
const button = ( <Box sx={{ height: '1px', mx: '14px', my: '10px', backgroundColor: 'var(--color-sidebar-border)' }} />
<ListItemButton );
key={item.path}
selected={isNavActive(location.pathname, item.path)}
onClick={() => handleNavClick(item.path)}
sx={{
py: 1,
justifyContent: isCollapsed ? 'center' : 'flex-start',
minHeight: 40,
}}
>
<ListItemIcon
sx={{
minWidth: isCollapsed ? 0 : 40,
justifyContent: 'center',
}}
>
{item.icon}
</ListItemIcon>
{!isCollapsed && (
<ListItemText primary={item.label} slotProps={{ primary: { sx: { fontSize: '13px' } } }} />
)}
</ListItemButton>
);
if (isCollapsed) { const renderGroup = (label: string, items: NavEntry[], first: boolean) => (
return ( <>
<Tooltip key={item.path} title={item.label} placement="right" arrow> {collapsed ? (first ? null : groupDivider) : groupLabel(label)}
{button} {items.map((item) => (
</Tooltip> <NavItem
); key={item.path}
} item={item}
active={isNavActive(location.pathname, item.path)}
return button; collapsed={collapsed}
}; onClick={() => handleNavClick(item.path)}
/>
))}
</>
);
return ( return (
<Drawer <Drawer
variant={drawerVariant} variant={isMobile ? 'temporary' : 'permanent'}
open={open} open={isMobile ? open : true}
onClose={handleDrawerClose} onClose={() => dispatch(setSidebarOpen(false))}
sx={{ sx={{
width: open ? effectiveWidth : 0, width: isMobile ? 0 : width,
flexShrink: 0, flexShrink: 0,
transition: 'width 250ms ease',
'& .MuiDrawer-paper': { '& .MuiDrawer-paper': {
width: effectiveWidth, width: isMobile ? DRAWER_WIDTH : width,
boxSizing: 'border-box', boxSizing: 'border-box',
backgroundColor: 'var(--color-sidebar-bg)',
borderRight: '1px solid var(--color-sidebar-border)',
borderRadius: 0,
overflowX: 'hidden', overflowX: 'hidden',
transition: 'width 200ms var(--ease-default, ease)',
}, },
}} }}
> >
<Toolbar sx={{ minHeight: 64 }} /> {/* Brand header row — 64px, white, aligns with the topbar */}
<Box sx={{ overflow: 'auto', flexGrow: 1, pt: 1 }}> <Box
{isAdmin && ( sx={{
<> height: 'var(--spacing-topbar)',
{!isCollapsed && ( minHeight: 'var(--spacing-topbar)',
<Typography display: 'flex',
sx={{ alignItems: 'center',
fontSize: '11px', justifyContent: 'center',
fontWeight: 700, backgroundColor: 'var(--card)',
letterSpacing: '0.08em', borderBottom: '1px solid var(--color-sidebar-border)',
textTransform: 'uppercase', flexShrink: 0,
color: '#94A3B8', }}
px: 2.5, >
mb: 0.5, {collapsed ? <BrandMark /> : <BrandLockup />}
}} </Box>
>
Admin <Box sx={{ overflowY: 'auto', overflowX: 'hidden', flexGrow: 1, py: 1, px: '8px' }}>
</Typography> {isAdmin && renderGroup('Admin', [...adminNav, ...(isSysAdmin ? sysadminNav : [])], true)}
)} {renderGroup('Proposals', dispatcherNav, !isAdmin)}
<List disablePadding>
{adminNav.map((item) => renderNavItem(item))}
{isSysAdmin && sysadminNav.map((item) => renderNavItem(item))}
</List>
</>
)}
{!isCollapsed && (
<Typography
sx={{
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: '#94A3B8',
px: 2.5,
mt: isAdmin ? 2 : 0,
mb: 0.5,
}}
>
Proposals
</Typography>
)}
<List disablePadding>
{navItems.map((item) => renderNavItem(item))}
</List>
</Box> </Box>
</Drawer> </Drawer>
); );

View file

@ -1,7 +1,7 @@
// App shell top bar — SHOC design system port (ADR 0003, mirrors app-topbar.tsx).
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { AppBar, Toolbar, IconButton, Typography, Box, Avatar } from '@mui/material'; import { AppBar, Toolbar, IconButton, Box, Avatar, Typography } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu'; import { Menu as MenuIcon, LogOut } from 'lucide-react';
import LogoutIcon from '@mui/icons-material/Logout';
import { toggleSidebar } from '../app/slices/uiSlice'; import { toggleSidebar } from '../app/slices/uiSlice';
import { useAuth } from '../hooks/useAuth'; import { useAuth } from '../hooks/useAuth';
@ -11,70 +11,103 @@ export default function Topbar() {
const initials = user?.displayName const initials = user?.displayName
? user.displayName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) ? user.displayName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
: ''; : 'U';
const isDevMode = !import.meta.env.VITE_COGNITO_DOMAIN; const isDevMode = !import.meta.env.VITE_COGNITO_DOMAIN;
return ( return (
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1, height: 64 }}> <AppBar
<Toolbar sx={{ height: 64, minHeight: 64 }}> position="sticky"
elevation={0}
square
sx={{
height: 'var(--spacing-topbar)',
borderRadius: 0,
background: 'var(--color-header-gradient)',
boxShadow: 'var(--color-header-shadow)',
}}
>
<Toolbar disableGutters sx={{ height: 'var(--spacing-topbar)', minHeight: 'var(--spacing-topbar)', gap: '18px', pl: '16px', pr: '22px' }}>
<IconButton <IconButton
color="inherit"
edge="start"
onClick={() => dispatch(toggleSidebar())} onClick={() => dispatch(toggleSidebar())}
sx={{ mr: 2 }}
aria-label="Toggle sidebar menu" aria-label="Toggle sidebar menu"
sx={{
width: 38,
height: 38,
borderRadius: 'var(--radius-md)',
backgroundColor: 'var(--color-header-surface)',
'&:hover': { backgroundColor: 'var(--color-header-surface-hover)' },
}}
> >
<MenuIcon /> <MenuIcon size={18} style={{ color: 'var(--color-header-icon)' }} />
</IconButton> </IconButton>
<Typography variant="h6" noWrap sx={{ flexGrow: 1, fontWeight: 600 }}>
Proposal System {isDevMode && (
{isDevMode && ( <Box
<Box component="span"
component="span" sx={{
backgroundColor: 'var(--color-header-surface)',
color: 'var(--color-header-text-muted)',
border: '1px solid var(--color-header-surface-hover)',
height: 22,
borderRadius: '999px',
fontSize: 'var(--text-xs)',
fontWeight: 700,
px: '10px',
display: 'inline-flex',
alignItems: 'center',
letterSpacing: '0.08em',
}}
>
DEV
</Box>
)}
<Box sx={{ flex: 1 }} />
{user && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '10px',
borderRadius: '9999px',
p: '5px 10px 5px 6px',
'&:hover': { backgroundColor: 'var(--color-header-chip-hover)' },
}}
>
<Avatar
sx={{ sx={{
background: '#FEF3C7', width: 34,
color: '#92400E', height: 34,
border: '1px solid #FDE68A', background: 'linear-gradient(135deg, var(--accent), var(--primary))',
height: 22, boxShadow: '0 0 0 2px var(--color-header-avatar-ring)',
borderRadius: '999px', fontFamily: 'var(--font-display)',
fontSize: '11px', fontSize: 12,
fontWeight: 700, fontWeight: 700,
pl: '8px', color: '#fff',
pr: '8px',
display: 'inline-flex',
alignItems: 'center',
ml: '12px',
verticalAlign: 'middle',
}} }}
> >
DEV {initials}
</Avatar>
<Box sx={{ display: 'flex', flexDirection: 'column', lineHeight: 1.2 }}>
<Typography sx={{ fontSize: '13px', fontWeight: 600, color: 'var(--color-header-text)' }}>
{user.displayName}
</Typography>
<Typography sx={{ fontSize: 'var(--text-xs)', color: 'var(--color-header-text-muted)' }}>
{user.role}
</Typography>
</Box> </Box>
)}
</Typography>
{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: '#2563EB', fontSize: 12, fontWeight: 600 }}>
{initials}
</Avatar>
<Box sx={{ display: 'flex', flexDirection: 'column', lineHeight: 1.2 }}>
<Typography sx={{ fontSize: '13px', fontWeight: 600, color: '#fff' }}>
{user.displayName}
</Typography>
<Typography sx={{ fontSize: '11px', color: 'rgba(255,255,255,0.55)' }}>
{user.role}
</Typography>
</Box>
</Box>
<Box sx={{ width: '1px', height: 24, bgcolor: 'rgba(255,255,255,0.12)', mx: 0.5 }} />
<IconButton <IconButton
onClick={logout} onClick={logout}
size="small" size="small"
aria-label="Log out" aria-label="Log out"
sx={{ color: 'rgba(255,255,255,0.55)', '&:hover': { color: '#ff6b6b', bgcolor: 'rgba(255,100,100,0.1)' } }} sx={{
color: 'var(--color-header-text-muted)',
'&:hover': { color: 'var(--color-header-text)', backgroundColor: 'var(--color-header-surface-hover)' },
}}
> >
<LogoutIcon fontSize="small" /> <LogOut size={16} />
</IconButton> </IconButton>
</Box> </Box>
)} )}

View file

@ -0,0 +1,68 @@
// Sea Haven brand lockup — ported from shoc-frontend-new dev (brand-lockup.tsx),
// Tailwind classes re-expressed as sx (this app is MUI-only per ADR 0003).
// Wordmark subtitle localized to this app: "PROPOSAL SYSTEM".
import { Box } from '@mui/material';
import SeahavenMark from './SeahavenMark';
type BrandLockupSize = 'default' | 'compact';
export function BrandMark({ size = 'default' }: { size?: BrandLockupSize }) {
const compact = size === 'compact';
return (
<Box
sx={{
display: 'flex',
flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
width: compact ? 26 : 30,
height: compact ? 26 : 30,
borderRadius: compact ? '7px' : '8px',
background: 'linear-gradient(to bottom right, var(--accent), var(--primary))',
boxShadow: 'var(--shadow-brand-mark)',
}}
>
<SeahavenMark size={compact ? 16 : 18} />
</Box>
);
}
export function BrandWordmark({ size = 'default' }: { size?: BrandLockupSize }) {
const compact = size === 'compact';
return (
<Box sx={{ lineHeight: 1 }}>
<Box
sx={{
fontFamily: 'var(--font-display)',
fontWeight: 800,
letterSpacing: '0.04em',
color: 'var(--foreground)',
fontSize: compact ? 'var(--text-md)' : '16px',
}}
>
SEAHAVEN
</Box>
<Box
sx={{
mt: '3px',
fontFamily: 'var(--font-mono)',
fontWeight: 600,
letterSpacing: '0.16em',
color: 'var(--muted-foreground)',
fontSize: compact ? '7.5px' : '8px',
}}
>
PROPOSAL SYSTEM
</Box>
</Box>
);
}
export default function BrandLockup({ size = 'default' }: { size?: BrandLockupSize }) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1.25 }}>
<BrandMark size={size} />
<BrandWordmark size={size} />
</Box>
);
}

View file

@ -0,0 +1,21 @@
// Sea Haven brand mark — ported from shoc-frontend-new dev (seahaven-mark.tsx).
export default function SeahavenMark({ size = 18 }: { size?: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={size}
height={size}
fill="none"
stroke="#ffffff"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M2 12q2.5 2 5 0t5 0 5 0 5 0" />
<path d="M2 19q2.5 2 5 0t5 0 5 0 5 0" />
<path d="M2 5q2.5 2 5 0t5 0 5 0 5 0" />
</svg>
);
}

View file

@ -38,7 +38,7 @@ export const STATUS_CHIP_STYLES: Record<string, { bgcolor: string; color: string
Rejected: { bgcolor: '#FEE2E2', color: '#991B1B', border: '1px solid #FECACA' }, Rejected: { bgcolor: '#FEE2E2', color: '#991B1B', border: '1px solid #FECACA' },
}; };
export const DRAWER_WIDTH = 216; export const DRAWER_WIDTH = 244; // --spacing-sidebar (SHOC shell, ADR 0003)
export const PRIORITY_LABELS: Record<string, string> = { export const PRIORITY_LABELS: Record<string, string> = {
Standard: 'Standard', Standard: 'Standard',

View file

@ -1,5 +1,4 @@
@import url("https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;0,700;1,400&display=swap"); /* App-level base styles. Fonts/tokens/typography live in src/styles/globals.css. */
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
@ -9,16 +8,12 @@ html, body, #root {
} }
body { body {
margin: 0; font-size: var(--text-base);
font-family: "Nunito", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: var(--leading-normal);
font-size: 13px;
background: #f4f6f7;
color: #333;
line-height: 1.5;
} }
a { a {
color: #007bff; color: var(--primary);
text-decoration: none; text-decoration: none;
} }

View file

@ -0,0 +1,32 @@
/**
* CSS custom-property bridge — mirrors shoc-frontend-new dev src/lib/theme/css-vars.ts.
* Reads live tokens from :root at theme-creation time; falls back to the static
* map when document is unavailable (tests, pre-paint).
*/
export function getCssVar(name: string, fallback = ''): string {
if (typeof document === 'undefined') return fallback;
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return value || fallback;
}
export const CSS_VAR_FALLBACKS = {
primary: '#1c75bc',
primaryHover: '#155a92',
primaryForeground: '#ffffff',
destructive: '#dc2626',
success: '#00a79d',
warning: '#d97706',
background: '#ffffff',
foreground: '#262262',
border: '#dfe3ea',
muted: '#e6e7e8',
mutedForeground: '#58595b',
inputBackground: '#f9fafb',
ring: '#1c75bc',
bgPage: '#f9fafb',
radius: '8',
headerStart: '#1b1f52',
headerMid: '#1c4f8f',
headerEnd: '#1c75bc',
headerText: '#ffffff',
} as const;

View file

@ -0,0 +1,295 @@
import { createTheme } from '@mui/material/styles';
import type { Shadows } from '@mui/material/styles';
import { getCssVar, CSS_VAR_FALLBACKS } from './css-vars';
/**
* MUI adapter over the CSS-variable token file (src/styles/theme.css).
* Mirrors shoc-frontend-new dev src/lib/theme/mui-theme.ts (ADR 0003).
* Token changes are values-only edits in theme.css — never here.
*/
declare module '@mui/material/styles' {
interface Palette {
header: Palette['primary'];
}
interface PaletteOptions {
header?: PaletteOptions['primary'];
}
}
const v = (name: string, fallback: string) => getCssVar(name, fallback);
const primary = v('--primary', CSS_VAR_FALLBACKS.primary);
const primaryHover = v('--primary-hover', CSS_VAR_FALLBACKS.primaryHover);
const primaryForeground = v('--primary-foreground', CSS_VAR_FALLBACKS.primaryForeground);
const destructive = v('--destructive', CSS_VAR_FALLBACKS.destructive);
const success = v('--success', CSS_VAR_FALLBACKS.success);
const warning = v('--warning', CSS_VAR_FALLBACKS.warning);
const background = v('--background', CSS_VAR_FALLBACKS.background);
const foreground = v('--foreground', CSS_VAR_FALLBACKS.foreground);
const border = v('--border', CSS_VAR_FALLBACKS.border);
const muted = v('--muted', CSS_VAR_FALLBACKS.muted);
const mutedForeground = v('--muted-foreground', CSS_VAR_FALLBACKS.mutedForeground);
const inputBackground = v('--input-background', CSS_VAR_FALLBACKS.inputBackground);
const ring = v('--ring', CSS_VAR_FALLBACKS.ring);
const bgPage = v('--color-bg-page', CSS_VAR_FALLBACKS.bgPage);
const radius = parseInt(v('--radius', `${CSS_VAR_FALLBACKS.radius}px`), 10);
const headerEnd = v('--color-header-bg-end', CSS_VAR_FALLBACKS.headerEnd);
const headerText = v('--color-header-text', CSS_VAR_FALLBACKS.headerText);
const shadowSm = v('--shadow-sm', '0 1px 3px rgba(22, 30, 56, 0.06), 0 1px 2px rgba(22, 30, 56, 0.04)');
const shadowMd = v('--shadow-md', '0 4px 6px -1px rgba(22, 30, 56, 0.08), 0 2px 4px -2px rgba(22, 30, 56, 0.06)');
const shadowLg = v('--shadow-lg', '0 10px 15px -3px rgba(22, 30, 56, 0.1), 0 4px 6px -4px rgba(22, 30, 56, 0.08)');
const fontSans = '"DM Sans", ui-sans-serif, system-ui, sans-serif';
const fontDisplay = '"Montserrat", ui-sans-serif, system-ui, sans-serif';
// MUI's 25-slot elevation tuple, fully overridden: 0=none, 1–2=sm, 3–5=md, 6+=lg.
const shadows = [
'none',
shadowSm,
shadowSm,
shadowMd,
shadowMd,
shadowMd,
...Array<string>(19).fill(shadowLg),
] as Shadows;
export const muiTheme = createTheme({
palette: {
mode: 'light',
primary: {
main: primary,
dark: primaryHover,
contrastText: primaryForeground,
},
// Repurposed as the muted/neutral swatch (SHOC convention), not a brand color.
secondary: {
main: muted,
contrastText: foreground,
},
error: {
main: destructive,
contrastText: '#ffffff',
},
success: {
main: success,
contrastText: '#ffffff',
},
warning: {
main: warning,
contrastText: '#ffffff',
},
background: {
default: bgPage,
paper: background,
},
text: {
primary: foreground,
secondary: mutedForeground,
},
divider: border,
header: {
main: headerEnd,
contrastText: headerText,
},
},
typography: {
fontFamily: fontSans,
fontSize: 14,
fontWeightRegular: 400,
fontWeightMedium: 500,
fontWeightBold: 600,
h1: { fontFamily: fontDisplay, fontWeight: 800, fontSize: '1.75rem', lineHeight: 1.25 },
h2: { fontFamily: fontDisplay, fontWeight: 700, fontSize: '1.375rem', lineHeight: 1.25 },
h3: { fontFamily: fontDisplay, fontWeight: 600, fontSize: '1.125rem', lineHeight: 1.25 },
h4: { fontFamily: fontDisplay, fontWeight: 600, fontSize: '1rem', lineHeight: 1.25 },
h5: { fontFamily: fontDisplay, fontWeight: 600, fontSize: '0.875rem', lineHeight: 1.25 },
h6: { fontFamily: fontDisplay, fontWeight: 600, fontSize: '0.8125rem', lineHeight: 1.25 },
subtitle1: { fontWeight: 600, fontSize: '1rem' },
subtitle2: { fontWeight: 500, fontSize: '0.875rem', color: mutedForeground },
body1: { fontSize: '0.875rem', lineHeight: 1.5 },
body2: { fontSize: '0.8125rem', lineHeight: 1.5, color: mutedForeground },
caption: { fontSize: '0.75rem', color: mutedForeground },
button: { textTransform: 'none' as const, fontWeight: 600, fontSize: '0.875rem' },
overline: {
fontSize: '0.6875rem',
fontWeight: 600,
letterSpacing: '0.08em',
textTransform: 'uppercase' as const,
color: mutedForeground,
},
},
shape: {
borderRadius: radius,
},
shadows,
components: {
MuiCssBaseline: {
styleOverrides: {
body: {
fontSize: 14,
backgroundColor: bgPage,
color: foreground,
},
},
},
MuiButton: {
defaultProps: {
disableElevation: true,
},
styleOverrides: {
root: {
borderRadius: 'var(--radius-md)',
textTransform: 'none' as const,
fontWeight: 600,
fontSize: '0.875rem',
transition: 'background-color var(--duration-fast) var(--ease-default), box-shadow var(--duration-fast) var(--ease-default)',
'&:focus-visible': {
outline: `2px solid ${ring}`,
outlineOffset: 2,
},
// MUI v9: containedPrimary slot removed — class selector instead
'&.MuiButton-containedPrimary:hover': {
backgroundColor: primaryHover,
},
},
outlined: {
borderColor: border,
'&:hover': {
borderColor: 'var(--border-hover)',
backgroundColor: 'var(--secondary)',
},
},
},
},
MuiTextField: {
defaultProps: {
size: 'small' as const,
},
},
MuiOutlinedInput: {
styleOverrides: {
root: {
backgroundColor: inputBackground,
borderRadius: 'var(--radius-md)',
'& .MuiOutlinedInput-notchedOutline': {
borderColor: border,
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: 'var(--border-hover)',
},
'&.Mui-focused': {
boxShadow: `0 0 0 3px color-mix(in srgb, ${ring} 25%, transparent)`,
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: ring,
borderWidth: 2,
},
},
input: {
padding: '8.5px 13px',
fontSize: '0.875rem',
'&.MuiOutlinedInput-inputAdornedEnd': {
paddingRight: 40,
},
},
},
},
MuiTableCell: {
styleOverrides: {
root: {
fontSize: '0.875rem',
borderColor: border,
padding: '10px 16px',
},
head: {
fontWeight: 600,
backgroundColor: 'var(--color-bg-muted)',
color: foreground,
},
},
},
MuiTableRow: {
styleOverrides: {
root: {
'&:last-child .MuiTableCell-root': {
borderBottom: 0,
},
},
},
},
MuiChip: {
styleOverrides: {
root: {
borderRadius: 'var(--radius-sm)',
fontWeight: 500,
fontSize: '0.75rem',
},
outlined: {
borderColor: border,
},
},
},
MuiPaper: {
defaultProps: {
elevation: 0,
},
styleOverrides: {
root: {
backgroundImage: 'none',
borderRadius: radius,
},
outlined: {
borderColor: border,
},
},
},
MuiDialog: {
styleOverrides: {
paper: {
borderRadius: 'var(--radius-xl)',
boxShadow: shadowLg,
},
},
},
MuiAlert: {
styleOverrides: {
root: {
borderRadius: 'var(--radius-md)',
fontSize: '0.875rem',
// MUI v9: standard* slots removed — class selectors instead
'&.MuiAlert-standardSuccess': {
backgroundColor: 'var(--status-completed-bg)',
color: 'var(--status-completed-text)',
},
'&.MuiAlert-standardError': {
backgroundColor: 'var(--doc-no-bg)',
color: 'var(--doc-no-text)',
},
'&.MuiAlert-standardWarning': {
backgroundColor: 'var(--status-pending-bg)',
color: 'var(--status-pending-text)',
},
'&.MuiAlert-standardInfo': {
backgroundColor: 'var(--color-primary-tint)',
color: 'var(--sidebar-accent-foreground)',
},
},
},
},
},
});

View file

@ -6,6 +6,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider, CssBaseline } from '@mui/material'; import { ThemeProvider, CssBaseline } from '@mui/material';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import './styles/globals.css';
import './index.css'; import './index.css';
import { store } from './app/store'; import { store } from './app/store';
import { queryClient } from './lib/queryClient'; import { queryClient } from './lib/queryClient';

View file

@ -56,7 +56,7 @@ function StatCard({ icon, label, value, color, subtitle }: { icon: React.ReactNo
<Typography sx={{ fontSize: '12px', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, mb: 1 }}> <Typography sx={{ fontSize: '12px', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, mb: 1 }}>
{label} {label}
</Typography> </Typography>
<Typography sx={{ fontSize: '30px', fontWeight: 700, color: '#111827', letterSpacing: '-0.02em' }}> <Typography sx={{ fontSize: '30px', fontWeight: 700, color: 'var(--foreground)', letterSpacing: '-0.02em' }}>
{value} {value}
</Typography> </Typography>
{subtitle && ( {subtitle && (

View file

@ -310,7 +310,7 @@ export default function AdminWorkspace() {
component="button" component="button"
underline="hover" underline="hover"
onClick={() => guardedNavigate('/admin')} onClick={() => guardedNavigate('/admin')}
sx={{ fontSize: 14, color: '#2563EB', cursor: 'pointer' }} sx={{ fontSize: 14, color: 'var(--primary)', cursor: 'pointer' }}
> >
Admin Queue Admin Queue
</Link> </Link>

View file

@ -1,7 +1,9 @@
// Login — SHOC auth-card treatment (ADR 0003, mirrors AuthPageShell/AuthCardHeader):
// centered 384px card on --color-bg-page, brand lockup, Montserrat heading.
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { Alert, Box, Button, Card, CardContent, Typography, Divider, Stack } from '@mui/material'; import { Alert, Box, Button, Typography, Divider, Stack } from '@mui/material';
import GoogleIcon from '@mui/icons-material/Google'; import GoogleIcon from '@mui/icons-material/Google';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import EngineeringIcon from '@mui/icons-material/Engineering'; import EngineeringIcon from '@mui/icons-material/Engineering';
@ -9,6 +11,7 @@ import PersonIcon from '@mui/icons-material/Person';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
import { authApi } from '../../lib/api/auth'; import { authApi } from '../../lib/api/auth';
import { setUser, setError } from '../../app/slices/authSlice'; import { setUser, setError } from '../../app/slices/authSlice';
import BrandLockup from '../../components/brand/BrandLockup';
const COGNITO_DOMAIN = import.meta.env.VITE_COGNITO_DOMAIN || ''; const COGNITO_DOMAIN = import.meta.env.VITE_COGNITO_DOMAIN || '';
const CLIENT_ID = import.meta.env.VITE_COGNITO_CLIENT_ID || ''; const CLIENT_ID = import.meta.env.VITE_COGNITO_CLIENT_ID || '';
@ -24,6 +27,12 @@ function buildLoginUrl(): string {
return `https://${COGNITO_DOMAIN}/oauth2/authorize?${params.toString()}`; return `https://${COGNITO_DOMAIN}/oauth2/authorize?${params.toString()}`;
} }
const devButtonSx = {
py: 1.5,
height: 56,
justifyContent: 'flex-start',
} as const;
export default function LoginPage() { export default function LoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -62,126 +71,135 @@ export default function LoginPage() {
<Box <Box
sx={{ sx={{
minHeight: '100vh', minHeight: '100vh',
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'flex-start', justifyContent: 'center',
pt: '7%', backgroundColor: 'var(--color-bg-page)',
background: 'linear-gradient(135deg, #111827 0%, #172554 100%)', px: 2,
}} }}
> >
<Box sx={{ width: '100%', maxWidth: 420, mx: 2 }}> <Box
<Card sx={{ borderRadius: '10px', boxShadow: '0 12px 32px rgba(15,23,42,0.24)', border: 'none' }}> sx={{
<Box width: '100%',
maxWidth: 384,
borderRadius: 'var(--radius)',
border: '1px solid var(--border)',
backgroundColor: 'var(--card)',
boxShadow: 'var(--shadow-sm)',
p: 5,
}}
>
<Box sx={{ mb: 4 }}>
<BrandLockup />
</Box>
<Typography
component="h2"
sx={{
textAlign: 'center',
fontFamily: 'var(--font-display)',
fontWeight: 700,
fontSize: 'var(--text-xl)',
color: 'var(--foreground)',
}}
>
Welcome back
</Typography>
<Typography
sx={{
textAlign: 'center',
color: 'var(--muted-foreground)',
fontSize: 'var(--text-base)',
mt: '6px',
mb: 3,
}}
>
Sign in to your account
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2, textAlign: 'left' }}>
{error}
</Alert>
)}
{isDevMode ? (
<>
<Stack spacing={1.5}>
<Button
variant="contained"
fullWidth
startIcon={<AdminPanelSettingsIcon />}
onClick={() => handleDevLogin('SysAdmin')}
disabled={loading}
sx={devButtonSx}
>
<Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>SysAdmin</Box>
<Box sx={{ fontSize: 'var(--text-xs)', fontWeight: 400, opacity: 0.8 }}>Full access</Box>
</Box>
</Button>
<Button
variant="contained"
fullWidth
startIcon={<EngineeringIcon />}
onClick={() => handleDevLogin('Admin')}
disabled={loading}
sx={devButtonSx}
>
<Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>Admin</Box>
<Box sx={{ fontSize: 'var(--text-xs)', fontWeight: 400, opacity: 0.8 }}>Review, price, and approve proposals</Box>
</Box>
</Button>
<Button
variant="contained"
fullWidth
startIcon={<PersonIcon />}
onClick={() => handleDevLogin('Dispatcher')}
disabled={loading}
sx={devButtonSx}
>
<Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>Dispatcher</Box>
<Box sx={{ fontSize: 'var(--text-xs)', fontWeight: 400, opacity: 0.8 }}>Submit and track proposals</Box>
</Box>
</Button>
</Stack>
<Divider sx={{ my: 2 }} />
<Typography variant="caption" sx={{ display: 'block', textAlign: 'center' }}>
Development sign-in — only shown when Cognito is not configured.
</Typography>
</>
) : (
<Button
variant="contained"
fullWidth
startIcon={<GoogleIcon />}
href={buildLoginUrl()}
sx={{ sx={{
height: 100, height: 42,
display: 'flex', fontFamily: 'var(--font-display)',
alignItems: 'center', fontSize: 'var(--text-base)',
justifyContent: 'center', fontWeight: 600,
bgcolor: '#fff',
borderBottom: '1px solid #E5E7EB',
px: 3,
}} }}
> >
<Typography sx={{ fontSize: '24px', fontWeight: 700, color: '#111827', letterSpacing: '-0.02em' }}> Sign in with Google
Sea Haven Industries </Button>
</Typography> )}
</Box>
<Box <Typography
sx={{ sx={{
bgcolor: '#111827', mt: '20px',
height: 40, display: 'block',
display: 'flex', textAlign: 'center',
alignItems: 'center', fontFamily: 'var(--font-sans)',
justifyContent: 'center', fontSize: 'var(--text-xs)',
borderBottom: '1px solid #E5E7EB', color: 'var(--muted-foreground)',
}} }}
> >
<Typography sx={{ fontSize: '14px', fontWeight: 700, color: '#fff' }}> Having trouble? Contact your administrator.
Proposal System
</Typography>
</Box>
<CardContent sx={{ p: '30px', textAlign: 'center' }}>
{error && (
<Alert severity="error" sx={{ mb: 2, textAlign: 'left' }}>
{error}
</Alert>
)}
{isDevMode ? (
<>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Development Sign-In — Select a role to preview the app.
</Typography>
<Stack spacing={2}>
<Button
variant="contained"
size="large"
fullWidth
startIcon={<AdminPanelSettingsIcon />}
onClick={() => handleDevLogin('SysAdmin')}
disabled={loading}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#2563EB', '&:hover': { bgcolor: '#1D4ED8' } }}
>
<Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>SysAdmin</Box>
<Box sx={{ fontSize: '11px', fontWeight: 400, opacity: 0.8 }}>Full access</Box>
</Box>
</Button>
<Button
variant="contained"
size="large"
fullWidth
startIcon={<EngineeringIcon />}
onClick={() => handleDevLogin('Admin')}
disabled={loading}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#2563EB', '&:hover': { bgcolor: '#1D4ED8' } }}
>
<Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>Admin</Box>
<Box sx={{ fontSize: '11px', fontWeight: 400, opacity: 0.8 }}>Review, price, and approve proposals</Box>
</Box>
</Button>
<Button
variant="contained"
size="large"
fullWidth
startIcon={<PersonIcon />}
onClick={() => handleDevLogin('Dispatcher')}
disabled={loading}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#2563EB', '&:hover': { bgcolor: '#1D4ED8' } }}
>
<Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>Dispatcher</Box>
<Box sx={{ fontSize: '11px', fontWeight: 400, opacity: 0.8 }}>Submit and track proposals</Box>
</Box>
</Button>
</Stack>
<Divider sx={{ my: 2 }} />
<Typography variant="caption" color="text.secondary">
Dev login is only shown when Cognito is not configured.
</Typography>
</>
) : (
<>
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
Sign in with your Sea Haven Google account to continue.
</Typography>
<Button
variant="contained"
size="large"
fullWidth
startIcon={<GoogleIcon />}
href={buildLoginUrl()}
sx={{ py: 1.5, height: 64, borderRadius: '8px', bgcolor: '#2563EB', '&:hover': { bgcolor: '#1D4ED8' } }}
>
Sign in with Google
</Button>
</>
)}
</CardContent>
</Card>
<Typography sx={{ textAlign: 'center', color: 'rgba(255,255,255,0.5)', fontSize: '12px', mt: 2 }}>
Sea Haven Industries &copy; {new Date().getFullYear()}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>

View file

@ -34,11 +34,11 @@ function KpiCard({ icon, label, value, subtitle }: { icon: React.ReactNode; labe
return ( return (
<Card sx={{ borderRadius: '8px', transition: 'border-color 120ms ease', '&:hover': { borderColor: '#9CA3AF' } }}> <Card sx={{ borderRadius: '8px', transition: 'border-color 120ms ease', '&:hover': { borderColor: '#9CA3AF' } }}>
<CardContent sx={{ textAlign: 'center', py: 2.5 }}> <CardContent sx={{ textAlign: 'center', py: 2.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 1, color: '#2563EB' }}>{icon}</Box> <Box sx={{ display: 'flex', justifyContent: 'center', mb: 1, color: 'var(--primary)' }}>{icon}</Box>
<Typography sx={{ fontSize: '12px', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, mb: 1 }}> <Typography sx={{ fontSize: '12px', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, mb: 1 }}>
{label} {label}
</Typography> </Typography>
<Typography sx={{ fontSize: '30px', fontWeight: 700, color: '#111827', letterSpacing: '-0.02em' }}> <Typography sx={{ fontSize: '30px', fontWeight: 700, color: 'var(--foreground)', letterSpacing: '-0.02em' }}>
{value} {value}
</Typography> </Typography>
{subtitle && ( {subtitle && (

12
web/src/styles/fonts.css Normal file
View file

@ -0,0 +1,12 @@
/* Self-hosted fonts (@fontsource) — same weights as shoc-frontend-new dev. */
@import "@fontsource/montserrat/600.css";
@import "@fontsource/montserrat/700.css";
@import "@fontsource/montserrat/800.css";
@import "@fontsource/dm-sans/400.css";
@import "@fontsource/dm-sans/500.css";
@import "@fontsource/dm-sans/600.css";
@import "@fontsource/jetbrains-mono/400.css";
@import "@fontsource/jetbrains-mono/500.css";
@import "@fontsource/jetbrains-mono/600.css";

View file

@ -0,0 +1,5 @@
/* Global stylesheet — import order mirrors shoc-frontend-new dev (minus Tailwind). */
@import "./fonts.css";
@import "./theme.css";
@import "./typography.css";
@import "./motion.css";

View file

@ -0,0 +1,6 @@
/* Motion tokens — mirrors shoc-frontend-new dev src/styles/motion.css. */
:root {
--duration-fast: 150ms;
--duration-normal: 250ms;
--ease-default: cubic-bezier(0.4, 0, 0.2, 1);
}

188
web/src/styles/theme.css Normal file
View file

@ -0,0 +1,188 @@
/*
* Sea Haven design tokens — single source of truth (ADR 0003).
* Ported verbatim from shoc-frontend-new dev src/styles/theme.css (values-only
* changes belong here; never rewrite the MUI adapter to change a color).
* The SHOC @theme inline Tailwind block is intentionally omitted — this app is MUI-only.
*/
:root {
/* Body typography reference — do not set on html (rem uses 16px root) */
--font-size: 14px;
/* ── TYPOGRAPHY ─────────────────────────────────────────────────── */
--font-family-display: "Montserrat", ui-sans-serif, system-ui, sans-serif;
--font-family-sans: "DM Sans", ui-sans-serif, system-ui, sans-serif;
--font-family-mono: "JetBrains Mono", ui-monospace, monospace;
--font-display: var(--font-family-display);
--font-sans: var(--font-family-sans);
--font-mono: var(--font-family-mono);
--font-heading: var(--font-family-display);
/* ── TYPOGRAPHY SCALE ───────────────────────────────────────────── */
--text-xs: 11px;
--text-sm: 12px;
--text-base-sm: 13px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 22px;
--text-2xl: 28px;
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.625;
--tracking-tight: -0.01em;
--tracking-normal: 0;
--tracking-wide: 0.05em;
--tracking-wider: 0.08em;
/* ── LAYOUT SPACING ─────────────────────────────────────────────── */
--spacing-sidebar: 244px;
--spacing-topbar: 64px;
--spacing-sidebar-collapsed: 76px;
/* ── RADIUS SCALE (scale resolves to 4 / 6 / 8 / 12) ────────────── */
--radius: 8px;
--radius-md: 6px;
--radius-sm: 4px;
--radius-xl: 12px;
/* ── ELEVATION ──────────────────────────────────────────────────── */
--shadow-sm: 0 1px 3px rgba(22, 30, 56, 0.06), 0 1px 2px rgba(22, 30, 56, 0.04);
--shadow-md: 0 4px 6px -1px rgba(22, 30, 56, 0.08), 0 2px 4px -2px rgba(22, 30, 56, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(22, 30, 56, 0.1), 0 4px 6px -4px rgba(22, 30, 56, 0.08);
--shadow-brand-mark: 0 2px 6px -1px rgba(28, 117, 188, 0.45);
/* ── SHADCN BASE (Seahaven DS) ──────────────────────────────────── */
--background: #ffffff;
--foreground: #262262;
--placeholder-foreground: color-mix(in srgb, var(--foreground) 50%, transparent);
--card: #ffffff;
--card-foreground: #262262;
--popover: #ffffff;
--popover-foreground: #262262;
--primary: #1c75bc;
--primary-hover: #155a92;
--primary-foreground: #ffffff;
--secondary: #f1f4f9;
--secondary-foreground: #262262;
--muted: #e6e7e8;
--muted-foreground: #58595b;
--accent: #27aae1;
--accent-foreground: #ffffff;
--success: #00a79d;
--warning: #d97706;
--destructive: #dc2626;
--destructive-foreground: #ffffff;
--border: #dfe3ea;
--border-hover: #c8d0e0;
--input: transparent;
--input-background: #f9fafb;
--switch-background: #c8d0e0;
--ring: #1c75bc;
--chart-1: #1c75bc;
--chart-2: #00a79d;
--chart-3: #d97706;
--chart-4: #7c3aed;
--chart-5: #dc2626;
/* ── SIDEBAR (light surface — Seahaven DS) ──────────────────────── */
--sidebar: #f6f8fb;
--sidebar-bg: #f6f8fb;
--sidebar-foreground: #262262;
--sidebar-primary: #1c75bc;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #e6f4fb;
--sidebar-accent-foreground: #155a92;
--sidebar-border: #dfe3ea;
--sidebar-ring: #1c75bc;
/* ── SEAHAVEN SEMANTIC ALIASES (consumed across the app) ────────── */
--color-primary: var(--primary);
--color-primary-hover: var(--primary-hover);
--color-primary-foreground: #ffffff;
--color-primary-tint: #e6f4fb;
--color-accent: var(--accent);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--foreground);
--color-ring: var(--ring);
--color-bg-page: #f9fafb;
--color-bg-surface: var(--card);
--color-bg-muted: #f1f4f9;
--color-border: var(--border);
--color-border-hover: #c8d0e0;
--color-text-primary: var(--foreground);
--color-text-secondary: #58595b;
--color-text-muted: #6b7a96;
--color-text-inverse: #ffffff;
/* ── SIDEBAR ALIASES (light) ────────────────────────────────────── */
--color-sidebar-bg: var(--sidebar-bg);
--color-sidebar-text: #262262;
--color-sidebar-text-hover: #155a92;
--color-sidebar-active-bg: var(--sidebar-accent);
--color-sidebar-active-text: var(--sidebar-accent-foreground);
--color-sidebar-hover-bg: #eef2f7;
--color-sidebar-label: #6b7a96;
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-badge-bg: #e6f4fb;
--color-sidebar-badge-text: #155a92;
--color-sidebar-item-active-bg: var(--card);
--color-sidebar-item-active-shadow: 0 1px 3px rgba(38, 34, 98, 0.08);
/* ── SEMANTIC (universal meaning) ───────────────────────────────── */
--color-destructive: var(--destructive);
--color-destructive-fg: #ffffff;
--color-warning: var(--warning);
--color-success: var(--success);
/* ── STATUS TOKENS (immutable — mirror SHOC; consumed by MuiAlert
mapping and proposal status badges) ──────────────────────────── */
--status-incomplete-bg: #f3f4f6;
--status-incomplete-text: #374151;
--status-pending-bg: #fff7ed;
--status-pending-text: #92400e;
--status-scheduled-bg: #eff6ff;
--status-scheduled-text: #1e40af;
--status-inprogress-bg: #eff6ff;
--status-inprogress-text: #1d4ed8;
--status-completed-bg: #f0fdf4;
--status-completed-text: #166534;
--status-canceled-bg: #f9fafb;
--status-canceled-text: #6b7280;
--status-pastdue-bg: #fff1f2;
--status-pastdue-text: #9f1239;
/* ── COMPLETION DOCUMENT (immutable — mirror SHOC) ──────────────── */
--doc-yes-bg: #f0fdf4;
--doc-yes-text: #166534;
--doc-no-bg: #fff1f2;
--doc-no-text: #be123c;
--doc-nn-bg: #f3f4f6;
--doc-nn-text: #6b7280;
/* ── SLA (immutable — mirror SHOC) ──────────────────────────────── */
--sla-safe: #16a34a;
--sla-warning: #d97706;
--sla-critical: #dc2626;
/* ── HEADER (app shell top bar) ─────────────────────────────────── */
--color-header-bg-start: #1b1f52;
--color-header-bg-mid: #1c4f8f;
--color-header-bg-end: var(--primary);
--color-header-gradient: linear-gradient(
100deg,
var(--color-header-bg-start) 0%,
var(--color-header-bg-mid) 58%,
var(--color-header-bg-end) 100%
);
--color-header-shadow: 0 1px 0 rgba(38, 34, 98, 0.18), 0 6px 18px -10px rgba(28, 117, 188, 0.5);
--color-header-surface: rgba(255, 255, 255, 0.08);
--color-header-surface-hover: rgba(255, 255, 255, 0.18);
--color-header-chip-hover: rgba(255, 255, 255, 0.12);
--color-header-icon: #eaf3fb;
--color-header-text: #ffffff;
--color-header-text-muted: #bdd8f0;
--color-header-avatar-ring: rgba(255, 255, 255, 0.25);
}

View file

@ -0,0 +1,52 @@
/* Base element typography — mirrors shoc-frontend-new dev src/styles/typography.css. */
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
font-family: var(--font-sans);
color: var(--foreground);
background-color: var(--color-bg-page);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
color: var(--foreground);
margin: 0;
}
h1 { font-size: var(--text-2xl); font-weight: 800; }
h2 { font-size: var(--text-xl); font-weight: 700; }
h3 { font-size: var(--text-lg); font-weight: 600; }
h4 { font-size: var(--text-md); font-weight: 600; }
h5 { font-size: var(--text-base); font-weight: 600; }
h6 { font-size: var(--text-base-sm); font-weight: 600; }
.text-caption {
font-family: var(--font-sans);
font-size: var(--text-sm);
line-height: var(--leading-normal);
color: var(--muted-foreground);
}
.text-label {
font-family: var(--font-sans);
font-size: var(--text-xs);
font-weight: 600;
line-height: 1.5;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--color-sidebar-label);
}
.text-nav-group {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted-foreground);
}

View file

@ -1,353 +1,4 @@
import { createTheme } from '@mui/material/styles'; // Sea Haven design system (ADR 0003) — SHOC dev token port.
// Tokens live in src/styles/theme.css (single source of truth);
// Sea Haven Ops design system — flat, dense, border-driven enterprise UI. // src/lib/theme/mui-theme.ts is the getCssVar → createTheme adapter.
// Neutral navy/slate/gray structure; action-blue #2563EB is the only brand accent. export { muiTheme as theme } from './lib/theme/mui-theme';
// Tokens mirror the Sea Haven Ops design bundle (tokens/colors.css, spacing.css, typography.css).
export const theme = createTheme({
palette: {
primary: {
main: '#2563EB', // action blue — primary buttons, links, selected
light: '#3B82F6',
dark: '#1D4ED8', // contained-button hover/pressed
contrastText: '#FFFFFF',
},
secondary: {
main: '#4B5563', // slate-600
light: '#6B7280',
dark: '#374151',
contrastText: '#FFFFFF',
},
success: {
main: '#16A34A',
light: '#DCFCE7',
dark: '#166534',
contrastText: '#FFFFFF',
},
warning: {
main: '#D97706', // orange — Urgent
light: '#FFEDD5',
dark: '#9A3412',
contrastText: '#FFFFFF',
},
error: {
main: '#DC2626', // red — Emergency / overdue / destructive
light: '#FEE2E2',
dark: '#991B1B',
contrastText: '#FFFFFF',
},
info: {
main: '#2563EB',
light: '#DBEAFE',
dark: '#1E40AF',
contrastText: '#FFFFFF',
},
background: {
default: '#F3F4F6', // gray-100 page background
paper: '#FFFFFF',
},
text: {
primary: '#111827', // navy-900
secondary: '#4B5563', // slate-600
disabled: '#9CA3AF', // gray-400
},
divider: '#E5E7EB', // gray-200
},
typography: {
fontFamily: '"Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
fontSize: 14,
h4: {
fontWeight: 700,
fontSize: '24px',
lineHeight: 1.25,
letterSpacing: '-0.02em',
},
h5: {
fontWeight: 600,
fontSize: '18px',
lineHeight: 1.3,
},
h6: {
fontWeight: 600,
fontSize: '16px',
lineHeight: 1.4,
},
subtitle1: {
fontWeight: 600,
fontSize: '14px',
lineHeight: 1.4,
},
body1: {
fontSize: '14px',
lineHeight: 1.5,
},
body2: {
fontSize: '13px',
lineHeight: 1.45,
},
caption: {
fontSize: '12px',
lineHeight: 1.35,
color: '#6B7280',
},
button: {
fontSize: '14px',
fontWeight: 600,
textTransform: 'none' as const,
},
overline: {
fontSize: '11px',
lineHeight: '16px',
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase' as const,
color: '#6B7280',
},
},
shape: {
borderRadius: 6,
},
components: {
MuiCssBaseline: {
styleOverrides: {
body: {
fontFeatureSettings: '"tnum"',
backgroundColor: '#F3F4F6',
},
},
},
MuiAppBar: {
defaultProps: {
elevation: 0,
},
styleOverrides: {
root: {
backgroundColor: '#111827', // navy-900
boxShadow: 'none',
borderRadius: 0,
borderBottom: '1px solid rgba(255,255,255,0.08)',
},
},
},
MuiDrawer: {
styleOverrides: {
paper: {
backgroundColor: '#FFFFFF',
borderRight: '1px solid #E5E7EB', // border-subtle
width: 216,
},
},
},
MuiCard: {
defaultProps: {
elevation: 0,
},
styleOverrides: {
// Cards are border-driven — no shadow (overlays only).
root: {
border: '1px solid #E5E7EB',
boxShadow: 'none',
borderRadius: 8,
},
},
},
MuiPaper: {
defaultProps: {
elevation: 0,
},
styleOverrides: {
root: {
backgroundImage: 'none',
borderRadius: 6,
},
},
},
MuiButton: {
styleOverrides: {
root: {
borderRadius: 6,
boxShadow: 'none',
textTransform: 'none' as const,
fontWeight: 600,
height: 36,
paddingLeft: 14,
paddingRight: 14,
},
contained: {
boxShadow: 'none',
'&:hover': {
boxShadow: 'none',
},
},
outlined: {
borderColor: '#D1D5DB', // border-default
color: '#111827',
'&:hover': {
borderColor: '#9CA3AF', // border-strong
backgroundColor: '#F9FAFB', // gray-50
},
},
},
},
MuiTextField: {
defaultProps: {
size: 'small' as const,
},
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
borderRadius: 6,
},
},
},
},
MuiOutlinedInput: {
styleOverrides: {
root: {
borderRadius: 6,
minHeight: 38,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: '#D1D5DB',
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: '#9CA3AF',
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: '#2563EB',
borderWidth: 2,
},
},
input: {
fontSize: 14,
padding: '9px 12px',
},
},
},
MuiInputLabel: {
styleOverrides: {
root: {
fontSize: 13,
color: '#6B7280',
'&.Mui-focused': {
color: '#2563EB',
},
},
},
},
MuiTableHead: {
styleOverrides: {
root: {
'& .MuiTableCell-head': {
color: '#374151', // slate-700
fontWeight: 700,
fontSize: 13,
paddingTop: 12,
paddingBottom: 12,
borderBottom: '1px solid #E5E7EB',
backgroundColor: '#F9FAFB', // gray-50
whiteSpace: 'nowrap',
},
},
},
},
MuiTableCell: {
styleOverrides: {
root: {
color: '#111827',
fontSize: 14,
padding: '12px 14px',
borderBottom: '1px solid #E5E7EB',
},
},
},
MuiTableRow: {
styleOverrides: {
root: {
'&:hover': {
backgroundColor: '#F9FAFB',
},
},
},
},
MuiChip: {
styleOverrides: {
root: {
height: 24,
borderRadius: 6,
fontSize: 12,
fontWeight: 700,
},
sizeSmall: {
height: 22,
fontSize: 12,
},
label: {
paddingLeft: 8,
paddingRight: 8,
},
icon: {
fontSize: 14,
marginLeft: 6,
},
},
},
MuiListItemButton: {
styleOverrides: {
root: {
borderRadius: 6,
marginLeft: 8,
marginRight: 8,
marginBottom: 4,
color: '#4B5563',
'&.Mui-selected': {
backgroundColor: '#EFF6FF', // blue-50
borderLeft: '3px solid #2563EB',
color: '#111827',
'& .MuiListItemIcon-root': {
color: '#2563EB',
},
'&:hover': {
backgroundColor: '#DBEAFE', // blue-100
},
},
'&:hover': {
backgroundColor: '#F3F4F6', // gray-100
},
},
},
},
MuiListItemIcon: {
styleOverrides: {
root: {
color: '#9CA3AF', // gray-400 — inactive nav icon
minWidth: 36,
},
},
},
MuiTooltip: {
styleOverrides: {
tooltip: {
fontSize: 12,
backgroundColor: '#1F2937', // navy-800
},
},
},
},
});