proposal-system/web/src/lib/theme/mui-theme.ts
Adam Moussa 8ce2a5cd8f
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).
2026-07-13 19:06:51 -04:00

295 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)',
},
},
},
},
},
});