proposal-system/web/src/lib/theme/mui-theme.ts

296 lines
8.6 KiB
TypeScript
Raw Normal View History

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