From 8ce2a5cd8fbf9ae53cf5c38c1a053adb49f3a078 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:06:51 -0400 Subject: [PATCH] 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). --- web/index.html | 3 - web/package-lock.json | 40 ++ web/package.json | 4 + web/src/App.tsx | 36 +- web/src/components/Sidebar.tsx | 289 ++++++++------ web/src/components/Topbar.tsx | 129 ++++--- web/src/components/brand/BrandLockup.tsx | 68 ++++ web/src/components/brand/SeahavenMark.tsx | 21 ++ web/src/constants/index.ts | 2 +- web/src/index.css | 13 +- web/src/lib/theme/css-vars.ts | 32 ++ web/src/lib/theme/mui-theme.ts | 295 +++++++++++++++ web/src/main.tsx | 1 + .../pages/admin/dashboard/AdminDashboard.tsx | 2 +- .../pages/admin/workspace/AdminWorkspace.tsx | 2 +- web/src/pages/auth/LoginPage.tsx | 246 ++++++------ web/src/pages/dashboard/Dashboard.tsx | 4 +- web/src/styles/fonts.css | 12 + web/src/styles/globals.css | 5 + web/src/styles/motion.css | 6 + web/src/styles/theme.css | 188 +++++++++ web/src/styles/typography.css | 52 +++ web/src/theme.ts | 357 +----------------- 23 files changed, 1134 insertions(+), 673 deletions(-) create mode 100644 web/src/components/brand/BrandLockup.tsx create mode 100644 web/src/components/brand/SeahavenMark.tsx create mode 100644 web/src/lib/theme/css-vars.ts create mode 100644 web/src/lib/theme/mui-theme.ts create mode 100644 web/src/styles/fonts.css create mode 100644 web/src/styles/globals.css create mode 100644 web/src/styles/motion.css create mode 100644 web/src/styles/theme.css create mode 100644 web/src/styles/typography.css diff --git a/web/index.html b/web/index.html index 62fb501..7b7a1a9 100644 --- a/web/index.html +++ b/web/index.html @@ -4,9 +4,6 @@ Proposal System - Sea Haven Industries - - -
diff --git a/web/package-lock.json b/web/package-lock.json index f0b34a7..60df75c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,11 +10,15 @@ "dependencies": { "@emotion/react": "^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/material": "^9.1.1", "@reduxjs/toolkit": "^2.11.2", "@tanstack/react-query": "^5.101.2", "axios": "^1.18.1", + "lucide-react": "^1.24.0", "react": "^19.2.7", "react-dom": "^19.2.7", "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": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2943,6 +2974,15 @@ "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": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/web/package.json b/web/package.json index 245c73e..42707cc 100644 --- a/web/package.json +++ b/web/package.json @@ -13,11 +13,15 @@ "dependencies": { "@emotion/react": "^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/material": "^9.1.1", "@reduxjs/toolkit": "^2.11.2", "@tanstack/react-query": "^5.101.2", "axios": "^1.18.1", + "lucide-react": "^1.24.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-redux": "^9.2.0", diff --git a/web/src/App.tsx b/web/src/App.tsx index 94a14ad..f247d00 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,5 @@ 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 ProtectedRoute, { RoleGuard } from './components/ProtectedRoute'; import Topbar from './components/Topbar'; @@ -25,20 +25,21 @@ export default function App() { path="/*" element={ - - + {/* SHOC shell composition (ADR 0003): sidebar column + [topbar / scrolling main] */} + - - - + + + + } /> } /> } /> @@ -55,7 +56,7 @@ export default function App() { - + User Management Users and roles are currently managed in AWS Cognito. Contact the system administrator to update access. @@ -75,8 +76,9 @@ export default function App() { } /> - } /> - + } /> + + diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index 617277b..191c619 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -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 { useSelector, useDispatch } from 'react-redux'; +import { Drawer, Box, Tooltip, useMediaQuery, useTheme } from '@mui/material'; import { - Drawer, - List, - ListItemButton, - ListItemIcon, - ListItemText, - Toolbar, - Box, - Typography, - Tooltip, - useMediaQuery, - 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'; + LayoutDashboard, + FileText, + CirclePlus, + ClipboardList, + FolderOpen, + Building2, + BookOpen, + Users, + type LucideIcon, +} from 'lucide-react'; import { selectSidebarOpen, setSidebarOpen } from '../app/slices/uiSlice'; import { selectUser } from '../app/slices/authSlice'; import type { RootState } from '../app/store'; import { DRAWER_WIDTH } from '../constants'; +import BrandLockup, { BrandMark } from './brand/BrandLockup'; -const COLLAPSED_WIDTH = 72; +const COLLAPSED_WIDTH = 76; // --spacing-sidebar-collapsed -const dispatcherNav = [ - { label: 'Dashboard', path: '/', icon: }, - { label: 'My Proposals', path: '/proposals', icon: }, - { label: 'New Proposal', path: '/proposals/new', icon: }, +type NavEntry = { label: string; path: string; icon: LucideIcon }; + +const dispatcherNav: NavEntry[] = [ + { label: 'Dashboard', path: '/', icon: LayoutDashboard }, + { label: 'My Proposals', path: '/proposals', icon: FileText }, + { label: 'New Proposal', path: '/proposals/new', icon: CirclePlus }, ]; -const adminNav = [ - { label: 'Admin Queue', path: '/admin', icon: }, - { label: 'All Proposals', path: '/admin/proposals', icon: }, - { label: 'Customers', path: '/admin/customers', icon: }, - { label: 'Pricing Library', path: '/admin/pricing-library', icon: }, +const adminNav: NavEntry[] = [ + { label: 'Admin Queue', path: '/admin', icon: ClipboardList }, + { label: 'All Proposals', path: '/admin/proposals', icon: FolderOpen }, + { label: 'Customers', path: '/admin/customers', icon: Building2 }, + { label: 'Pricing Library', path: '/admin/pricing-library', icon: BookOpen }, ]; -const sysadminNav = [ - { label: 'Users', path: '/admin/users', icon: }, +const sysadminNav: NavEntry[] = [ + { label: 'Users', path: '/admin/users', icon: Users }, ]; const ALL_NAV_PATHS = new Set( @@ -56,6 +53,88 @@ function isNavActive(pathname: string, itemPath: string): boolean { return pathname.startsWith(itemPath + '/'); } +function NavItem({ + item, + active, + collapsed, + onClick, +}: { + item: NavEntry; + active: boolean; + collapsed: boolean; + onClick: () => void; +}) { + const Icon = item.icon; + const row = ( + + {active && ( + + )} + + {!collapsed && ( + + {item.label} + + )} + + ); + + if (collapsed) { + return ( + + {row} + + ); + } + return row; +} + export default function Sidebar() { const navigate = useNavigate(); const location = useLocation(); @@ -65,14 +144,14 @@ export default function Sidebar() { const user = useSelector((state: RootState) => selectUser(state)); 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 isSysAdmin = user?.role === 'SysAdmin'; - const navItems = dispatcherNav; - const drawerVariant = isMobile ? 'temporary' : 'persistent'; - const effectiveWidth = isCollapsed ? COLLAPSED_WIDTH : DRAWER_WIDTH; + // Desktop: hamburger toggles expanded (244) ↔ collapsed (76). + // Mobile: temporary drawer, hamburger opens/closes it. + const collapsed = !isMobile && !open; + const width = collapsed ? COLLAPSED_WIDTH : DRAWER_WIDTH; const handleNavClick = (path: string) => { navigate(path); @@ -81,107 +160,69 @@ export default function Sidebar() { } }; - const handleDrawerClose = () => { - dispatch(setSidebarOpen(false)); - }; + const groupLabel = (label: string) => ( + + {label} + + ); - const renderNavItem = (item: { label: string; path: string; icon: React.ReactNode }) => { - const button = ( - handleNavClick(item.path)} - sx={{ - py: 1, - justifyContent: isCollapsed ? 'center' : 'flex-start', - minHeight: 40, - }} - > - - {item.icon} - - {!isCollapsed && ( - - )} - - ); + const groupDivider = ( + + ); - if (isCollapsed) { - return ( - - {button} - - ); - } - - return button; - }; + const renderGroup = (label: string, items: NavEntry[], first: boolean) => ( + <> + {collapsed ? (first ? null : groupDivider) : groupLabel(label)} + {items.map((item) => ( + handleNavClick(item.path)} + /> + ))} + + ); return ( dispatch(setSidebarOpen(false))} sx={{ - width: open ? effectiveWidth : 0, + width: isMobile ? 0 : width, flexShrink: 0, - transition: 'width 250ms ease', '& .MuiDrawer-paper': { - width: effectiveWidth, + width: isMobile ? DRAWER_WIDTH : width, boxSizing: 'border-box', + backgroundColor: 'var(--color-sidebar-bg)', + borderRight: '1px solid var(--color-sidebar-border)', + borderRadius: 0, overflowX: 'hidden', + transition: 'width 200ms var(--ease-default, ease)', }, }} > - - - {isAdmin && ( - <> - {!isCollapsed && ( - - Admin - - )} - - {adminNav.map((item) => renderNavItem(item))} - {isSysAdmin && sysadminNav.map((item) => renderNavItem(item))} - - - )} - {!isCollapsed && ( - - Proposals - - )} - - {navItems.map((item) => renderNavItem(item))} - + {/* Brand header row — 64px, white, aligns with the topbar */} + + {collapsed ? : } + + + + {isAdmin && renderGroup('Admin', [...adminNav, ...(isSysAdmin ? sysadminNav : [])], true)} + {renderGroup('Proposals', dispatcherNav, !isAdmin)} ); diff --git a/web/src/components/Topbar.tsx b/web/src/components/Topbar.tsx index 9a04831..5ba17f6 100644 --- a/web/src/components/Topbar.tsx +++ b/web/src/components/Topbar.tsx @@ -1,7 +1,7 @@ +// App shell top bar — SHOC design system port (ADR 0003, mirrors app-topbar.tsx). import { useDispatch } from 'react-redux'; -import { AppBar, Toolbar, IconButton, Typography, Box, Avatar } from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; -import LogoutIcon from '@mui/icons-material/Logout'; +import { AppBar, Toolbar, IconButton, Box, Avatar, Typography } from '@mui/material'; +import { Menu as MenuIcon, LogOut } from 'lucide-react'; import { toggleSidebar } from '../app/slices/uiSlice'; import { useAuth } from '../hooks/useAuth'; @@ -11,70 +11,103 @@ export default function Topbar() { const initials = user?.displayName ? user.displayName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) - : ''; + : 'U'; const isDevMode = !import.meta.env.VITE_COGNITO_DOMAIN; return ( - theme.zIndex.drawer + 1, height: 64 }}> - + + dispatch(toggleSidebar())} - sx={{ mr: 2 }} 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)' }, + }} > - + - - Proposal System - {isDevMode && ( - + DEV + + )} + + + + {user && ( + + - DEV + {initials} + + + + {user.displayName} + + + {user.role} + - )} - - {user && ( - - - - {initials} - - - - {user.displayName} - - - {user.role} - - - - - + )} diff --git a/web/src/components/brand/BrandLockup.tsx b/web/src/components/brand/BrandLockup.tsx new file mode 100644 index 0000000..4472ac9 --- /dev/null +++ b/web/src/components/brand/BrandLockup.tsx @@ -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 ( + + + + ); +} + +export function BrandWordmark({ size = 'default' }: { size?: BrandLockupSize }) { + const compact = size === 'compact'; + return ( + + + SEAHAVEN + + + PROPOSAL SYSTEM + + + ); +} + +export default function BrandLockup({ size = 'default' }: { size?: BrandLockupSize }) { + return ( + + + + + ); +} diff --git a/web/src/components/brand/SeahavenMark.tsx b/web/src/components/brand/SeahavenMark.tsx new file mode 100644 index 0000000..f60a4b1 --- /dev/null +++ b/web/src/components/brand/SeahavenMark.tsx @@ -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 ( + + ); +} diff --git a/web/src/constants/index.ts b/web/src/constants/index.ts index 36ebcb3..89b2b60 100644 --- a/web/src/constants/index.ts +++ b/web/src/constants/index.ts @@ -38,7 +38,7 @@ export const STATUS_CHIP_STYLES: Record = { Standard: 'Standard', diff --git a/web/src/index.css b/web/src/index.css index 6ad3d12..9acafa1 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -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; } @@ -9,16 +8,12 @@ html, body, #root { } body { - margin: 0; - font-family: "Nunito", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 13px; - background: #f4f6f7; - color: #333; - line-height: 1.5; + font-size: var(--text-base); + line-height: var(--leading-normal); } a { - color: #007bff; + color: var(--primary); text-decoration: none; } diff --git a/web/src/lib/theme/css-vars.ts b/web/src/lib/theme/css-vars.ts new file mode 100644 index 0000000..7f73b3d --- /dev/null +++ b/web/src/lib/theme/css-vars.ts @@ -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; diff --git a/web/src/lib/theme/mui-theme.ts b/web/src/lib/theme/mui-theme.ts new file mode 100644 index 0000000..28be2c5 --- /dev/null +++ b/web/src/lib/theme/mui-theme.ts @@ -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(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)', + }, + }, + }, + }, + }, +}); diff --git a/web/src/main.tsx b/web/src/main.tsx index f56fab5..0d0ee59 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -6,6 +6,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { ThemeProvider, CssBaseline } from '@mui/material'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; +import './styles/globals.css'; import './index.css'; import { store } from './app/store'; import { queryClient } from './lib/queryClient'; diff --git a/web/src/pages/admin/dashboard/AdminDashboard.tsx b/web/src/pages/admin/dashboard/AdminDashboard.tsx index bdf4578..b664728 100644 --- a/web/src/pages/admin/dashboard/AdminDashboard.tsx +++ b/web/src/pages/admin/dashboard/AdminDashboard.tsx @@ -56,7 +56,7 @@ function StatCard({ icon, label, value, color, subtitle }: { icon: React.ReactNo {label} - + {value} {subtitle && ( diff --git a/web/src/pages/admin/workspace/AdminWorkspace.tsx b/web/src/pages/admin/workspace/AdminWorkspace.tsx index ff32e7c..87729e8 100644 --- a/web/src/pages/admin/workspace/AdminWorkspace.tsx +++ b/web/src/pages/admin/workspace/AdminWorkspace.tsx @@ -310,7 +310,7 @@ export default function AdminWorkspace() { component="button" underline="hover" onClick={() => guardedNavigate('/admin')} - sx={{ fontSize: 14, color: '#2563EB', cursor: 'pointer' }} + sx={{ fontSize: 14, color: 'var(--primary)', cursor: 'pointer' }} > Admin Queue diff --git a/web/src/pages/auth/LoginPage.tsx b/web/src/pages/auth/LoginPage.tsx index 9f2e7a2..8694088 100644 --- a/web/src/pages/auth/LoginPage.tsx +++ b/web/src/pages/auth/LoginPage.tsx @@ -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 { useNavigate } from 'react-router-dom'; 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 AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; import EngineeringIcon from '@mui/icons-material/Engineering'; @@ -9,6 +11,7 @@ import PersonIcon from '@mui/icons-material/Person'; import { useAuth } from '../../hooks/useAuth'; import { authApi } from '../../lib/api/auth'; import { setUser, setError } from '../../app/slices/authSlice'; +import BrandLockup from '../../components/brand/BrandLockup'; const COGNITO_DOMAIN = import.meta.env.VITE_COGNITO_DOMAIN || ''; 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()}`; } +const devButtonSx = { + py: 1.5, + height: 56, + justifyContent: 'flex-start', +} as const; + export default function LoginPage() { const navigate = useNavigate(); const dispatch = useDispatch(); @@ -62,126 +71,135 @@ export default function LoginPage() { - - - + + + + + + Welcome back + + + Sign in to your account + + + {error && ( + + {error} + + )} + + {isDevMode ? ( + <> + + + + + + + + Development sign-in — only shown when Cognito is not configured. + + + ) : ( + - - - - - - Dev login is only shown when Cognito is not configured. - - - ) : ( - <> - - Sign in with your Sea Haven Google account to continue. - - - - )} - - - - Sea Haven Industries © {new Date().getFullYear()} + Sign in with Google + + )} + + + Having trouble? Contact your administrator. diff --git a/web/src/pages/dashboard/Dashboard.tsx b/web/src/pages/dashboard/Dashboard.tsx index 3728186..56931a9 100644 --- a/web/src/pages/dashboard/Dashboard.tsx +++ b/web/src/pages/dashboard/Dashboard.tsx @@ -34,11 +34,11 @@ function KpiCard({ icon, label, value, subtitle }: { icon: React.ReactNode; labe return ( - {icon} + {icon} {label} - + {value} {subtitle && ( diff --git a/web/src/styles/fonts.css b/web/src/styles/fonts.css new file mode 100644 index 0000000..bf759c9 --- /dev/null +++ b/web/src/styles/fonts.css @@ -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"; diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css new file mode 100644 index 0000000..db4a57b --- /dev/null +++ b/web/src/styles/globals.css @@ -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"; diff --git a/web/src/styles/motion.css b/web/src/styles/motion.css new file mode 100644 index 0000000..afba9f4 --- /dev/null +++ b/web/src/styles/motion.css @@ -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); +} diff --git a/web/src/styles/theme.css b/web/src/styles/theme.css new file mode 100644 index 0000000..bb1ce7d --- /dev/null +++ b/web/src/styles/theme.css @@ -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); +} diff --git a/web/src/styles/typography.css b/web/src/styles/typography.css new file mode 100644 index 0000000..c9fb518 --- /dev/null +++ b/web/src/styles/typography.css @@ -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); +} diff --git a/web/src/theme.ts b/web/src/theme.ts index 353e284..abba78e 100644 --- a/web/src/theme.ts +++ b/web/src/theme.ts @@ -1,353 +1,4 @@ -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: '#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 - }, - }, - }, - }, -}); +// Sea Haven design system (ADR 0003) — SHOC dev token port. +// Tokens live in src/styles/theme.css (single source of truth); +// src/lib/theme/mui-theme.ts is the getCssVar → createTheme adapter. +export { muiTheme as theme } from './lib/theme/mui-theme';