mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 16:18:57 +00:00
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).
295 lines
8.6 KiB
TypeScript
295 lines
8.6 KiB
TypeScript
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)',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|