33d899c243
Root cause of "Google authentication failed": appsettings.Development.json had Auth:GoogleClientId set to the literal placeholder "CHANGE_ME_GOOGLE_CLIENT_ID" while the frontend's .env.development had a real (already-public, already-committed) client ID -- every Google ID token's audience check failed against the backend's placeholder. Fixed by setting the same real client ID on both sides (a client ID is a public identifier, not a secret, safe to commit -- unlike a client secret). Also enabled Auth:AllowRegistration in dev so the existing Google-first self-serve-signup path (auto-create on unmatched verified email, auto-link on matching verified email -- built during Wave 7) is actually exercisable locally. Wired the previously-missing Auth__MicrosoftClientId / NEXT_PUBLIC_MICROSOFT_CLIENT_ID into docker-compose.yml/.env.example (distinct from the existing MICROSOFT_CLIENT_ID used for Outlook mail linking) -- Microsoft sign-in was never deployable, a leftover gap from when it was built. Fixed a stale env-var name in the Microsoft setup hint copy (still said REACT_APP_*, predates the Next.js migration). Removed the per-user accent color picker entirely: it was purely client-side (localStorage + theme.ts), never touched the backend/DB. theme.ts now hardcodes a single ACCENT constant; themePrefs.ts drops get/set/clearAccentColor; App.tsx and SettingsView.tsx drop the accentColor prop threading. Dead accent-related i18n keys removed from both locales. Consolidated Settings' "Account" tab (duplicated GoogleAuthCard, which already lives on the Profile page) into Profile: moved AuthStatusCard and EmailProviderConnections there alongside the existing Google/ Microsoft auth cards, so identity/account-linking lives in one place. Settings drops from 5 tabs to 4 and its General tab uses a consistent SectionCard layout instead of ad-hoc per-card styling. Verified: dotnet build/test (177/177) and npm build/test (57/57) both green; confirmed live against a running dev server that /auth/config now reports googleEnabled with the corrected client ID, Settings has no accent controls, and Profile shows the consolidated auth section.
356 lines
9.5 KiB
TypeScript
356 lines
9.5 KiB
TypeScript
import { alpha, createTheme, darken, lighten } from "@mui/material/styles";
|
|
|
|
type PaletteLike = Record<string, any>;
|
|
|
|
// Single global brand accent -- matches the dark sidebar/landing page indigo used throughout
|
|
// the app. Not user-configurable; see jobbjakt-nextjs-migration memory / UI rework notes.
|
|
const ACCENT = "#6366F1";
|
|
|
|
function buildPrimary(main: string) {
|
|
return {
|
|
lighter: lighten(main, 0.82),
|
|
light: lighten(main, 0.56),
|
|
main,
|
|
dark: darken(main, 0.22),
|
|
darker: darken(main, 0.72),
|
|
};
|
|
}
|
|
|
|
function buildLightPalette(): PaletteLike {
|
|
const textPrimary = "#1B1B1F";
|
|
const textSecondary = "#46464F";
|
|
|
|
const secondaryMain = "#5A5C78";
|
|
const divider = "#EFEDF4";
|
|
const background = "#FFFFFF";
|
|
|
|
const disabled = "#777680";
|
|
const disabledBackground = "#E4E1E6";
|
|
|
|
return {
|
|
primary: buildPrimary(ACCENT),
|
|
secondary: {
|
|
lighter: "#E0E0FF",
|
|
light: "#C3C4E4",
|
|
main: secondaryMain,
|
|
dark: "#43455F",
|
|
darker: "#171A31",
|
|
},
|
|
error: {
|
|
lighter: "#FFEDEA",
|
|
light: "#FFDAD6",
|
|
main: "#DE3730",
|
|
dark: "#BA1A1A",
|
|
darker: "#690005",
|
|
},
|
|
warning: {
|
|
lighter: "#FFEEE1",
|
|
light: "#FFDCBE",
|
|
main: "#AE6600",
|
|
dark: "#8B5000",
|
|
darker: "#4A2800",
|
|
},
|
|
success: {
|
|
lighter: "#DCFCE7",
|
|
light: "#BBF7D0",
|
|
main: "#16A34A",
|
|
dark: "#15803D",
|
|
darker: "#14532D",
|
|
},
|
|
info: {
|
|
lighter: "#D4F7FF",
|
|
light: "#A1EFFF",
|
|
main: "#008394",
|
|
dark: "#006876",
|
|
darker: "#00363E",
|
|
},
|
|
grey: {
|
|
50: "#FBF8FF",
|
|
100: "#F5F2FA",
|
|
200: divider,
|
|
300: "#EAE7EF",
|
|
400: disabledBackground,
|
|
500: "#DBD9E0",
|
|
600: "#C7C5D0",
|
|
700: disabled,
|
|
800: textSecondary,
|
|
900: textPrimary,
|
|
},
|
|
text: {
|
|
primary: textPrimary,
|
|
secondary: textSecondary,
|
|
disabled,
|
|
},
|
|
divider,
|
|
// Soft grey app background with white paper gives the layered dashboard look
|
|
// from the product mockups; cards/inputs (paper) sit above it.
|
|
background: { default: "#F4F6FB", paper: background },
|
|
action: {
|
|
hover: alpha(ACCENT, 0.05),
|
|
disabled: alpha(disabled, 0.6),
|
|
disabledBackground: alpha(disabledBackground, 0.9),
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildDarkPalette(): PaletteLike {
|
|
const bg = "#0B0B0E";
|
|
const paper = "#111116";
|
|
const divider = alpha("#FFFFFF", 0.10);
|
|
const textPrimary = "#EAEAF2";
|
|
const textSecondary = alpha(textPrimary, 0.72);
|
|
|
|
const secondaryMain = "#A4A6C8";
|
|
const disabled = alpha(textPrimary, 0.5);
|
|
const disabledBackground = alpha("#FFFFFF", 0.08);
|
|
|
|
return {
|
|
primary: buildPrimary(ACCENT),
|
|
secondary: {
|
|
lighter: alpha(secondaryMain, 0.22),
|
|
light: alpha(secondaryMain, 0.14),
|
|
main: secondaryMain,
|
|
dark: alpha(secondaryMain, 0.8),
|
|
darker: alpha(secondaryMain, 0.95),
|
|
},
|
|
error: {
|
|
lighter: alpha("#DE3730", 0.18),
|
|
light: alpha("#DE3730", 0.12),
|
|
main: "#FF5A52",
|
|
dark: "#FF3B33",
|
|
darker: "#FFC2BE",
|
|
},
|
|
warning: {
|
|
lighter: alpha("#AE6600", 0.18),
|
|
light: alpha("#AE6600", 0.12),
|
|
main: "#FFB14A",
|
|
dark: "#FF9A1F",
|
|
darker: "#FFE1B8",
|
|
},
|
|
success: {
|
|
lighter: alpha("#16A34A", 0.18),
|
|
light: alpha("#16A34A", 0.12),
|
|
main: "#4ADE80",
|
|
dark: "#22C55E",
|
|
darker: "#BBF7D0",
|
|
},
|
|
info: {
|
|
lighter: alpha("#008394", 0.18),
|
|
light: alpha("#008394", 0.12),
|
|
main: "#22D3EE",
|
|
dark: "#06B6D4",
|
|
darker: "#BAF3FF",
|
|
},
|
|
grey: {
|
|
50: bg,
|
|
100: "#0F0F13",
|
|
200: "#15151B",
|
|
300: "#1C1C24",
|
|
400: "#23232D",
|
|
500: "#2D2D3A",
|
|
600: "#3A3A4A",
|
|
700: "#4A4A5D",
|
|
800: "#B8B8C6",
|
|
900: textPrimary,
|
|
},
|
|
text: {
|
|
primary: textPrimary,
|
|
secondary: textSecondary,
|
|
disabled,
|
|
},
|
|
divider,
|
|
background: { default: bg, paper },
|
|
action: {
|
|
hover: alpha(ACCENT, 0.16),
|
|
disabled: alpha("#FFFFFF", 0.5),
|
|
disabledBackground,
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildCustomShadows(palette: any) {
|
|
const shadowColor = palette.text.primary;
|
|
const primaryColor = palette.primary.main;
|
|
return {
|
|
button: `0px 0.711px 1.422px 0px ${alpha(shadowColor, 0.05)}`,
|
|
section: `0px 1px 2px 0px ${alpha(shadowColor, 0.07)}`,
|
|
tooltip: `0px 12px 16px -4px ${alpha(shadowColor, 0.08)}, 0px 4px 6px -2px ${alpha(shadowColor, 0.03)}`,
|
|
focus: `0px 0px 0px 3px ${alpha(primaryColor, 0.2)}`,
|
|
};
|
|
}
|
|
|
|
function buildTypography() {
|
|
return {
|
|
fontFamily: "'Archivo', system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif",
|
|
letterSpacing: 0,
|
|
h1: { fontWeight: 500, fontSize: 36, lineHeight: "40px" },
|
|
h2: { fontWeight: 500, fontSize: 30, lineHeight: "34px" },
|
|
h3: { fontWeight: 500, fontSize: 26, lineHeight: "30px" },
|
|
h4: { fontWeight: 500, fontSize: 22, lineHeight: "26px" },
|
|
h5: { fontWeight: 500, fontSize: 18, lineHeight: "22px" },
|
|
h6: { fontWeight: 500, fontSize: 16, lineHeight: "20px" },
|
|
subtitle1: { fontWeight: 500, fontSize: 14, lineHeight: "18px" },
|
|
subtitle2: { fontWeight: 500, fontSize: 13, lineHeight: "17px" },
|
|
body1: { fontWeight: 400, fontSize: 14, lineHeight: "18px" },
|
|
body2: { fontWeight: 400, fontSize: 13, lineHeight: "17px" },
|
|
caption: { fontWeight: 400, fontSize: 12, lineHeight: "16px", letterSpacing: 0 },
|
|
overline: { fontWeight: 600, fontSize: 11, lineHeight: "14px", letterSpacing: "0.08em", textTransform: "uppercase" as const },
|
|
caption1: { fontWeight: 500, fontSize: 12, lineHeight: "16px", letterSpacing: 0 },
|
|
button: { textTransform: "capitalize" as const },
|
|
};
|
|
}
|
|
|
|
export const getTheme = (_mode: "light" | "dark") => {
|
|
const lightPalette = buildLightPalette();
|
|
const darkPalette = buildDarkPalette();
|
|
|
|
const theme = createTheme({
|
|
breakpoints: {
|
|
values: {
|
|
xs: 0,
|
|
sm: 768,
|
|
md: 1024,
|
|
lg: 1266,
|
|
xl: 1440,
|
|
},
|
|
},
|
|
cssVariables: {
|
|
cssVarPrefix: "",
|
|
colorSchemeSelector: "data-color-scheme",
|
|
},
|
|
colorSchemes: {
|
|
light: { palette: lightPalette, customShadows: buildCustomShadows(lightPalette) },
|
|
dark: { palette: darkPalette, customShadows: buildCustomShadows(darkPalette) },
|
|
},
|
|
shape: { borderRadius: 10 },
|
|
typography: buildTypography() as any,
|
|
} as any) as any;
|
|
|
|
theme.components = {
|
|
MuiCssBaseline: {
|
|
styleOverrides: {
|
|
body: {
|
|
WebkitFontSmoothing: "antialiased",
|
|
MozOsxFontSmoothing: "grayscale",
|
|
},
|
|
},
|
|
},
|
|
MuiPaper: {
|
|
defaultProps: { elevation: 0 },
|
|
styleOverrides: {
|
|
root: ({ theme }: any) => ({
|
|
border: `1px solid ${theme.vars.palette.divider}`,
|
|
boxShadow: "none",
|
|
backgroundImage: "none",
|
|
}),
|
|
},
|
|
},
|
|
MuiCard: {
|
|
defaultProps: { elevation: 0 },
|
|
styleOverrides: {
|
|
root: ({ theme }: any) => ({
|
|
border: `1px solid ${theme.vars.palette.divider}`,
|
|
borderRadius: 12,
|
|
backgroundImage: "none",
|
|
boxShadow: theme.vars.customShadows.section,
|
|
}),
|
|
},
|
|
},
|
|
MuiButton: {
|
|
defaultProps: { disableFocusRipple: true },
|
|
styleOverrides: {
|
|
root: ({ theme }: any) => ({
|
|
borderRadius: 8,
|
|
boxShadow: theme.vars.customShadows.button,
|
|
"&:hover": { boxShadow: theme.vars.customShadows.button },
|
|
}),
|
|
},
|
|
},
|
|
MuiTextField: {
|
|
defaultProps: {
|
|
size: "small",
|
|
variant: "outlined",
|
|
},
|
|
},
|
|
MuiOutlinedInput: {
|
|
defaultProps: { size: "small" },
|
|
styleOverrides: {
|
|
root: ({ theme }: any) => ({
|
|
borderRadius: 8,
|
|
boxShadow: theme.vars.customShadows.button,
|
|
background: theme.vars.palette.background.default,
|
|
paddingLeft: 10,
|
|
paddingRight: 10,
|
|
minHeight: 42,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
"&.Mui-disabled": {
|
|
cursor: "not-allowed",
|
|
input: { cursor: "not-allowed" },
|
|
"& .MuiOutlinedInput-notchedOutline": { borderColor: theme.vars.palette.divider },
|
|
},
|
|
}),
|
|
notchedOutline: ({ theme }: any) => ({ borderColor: theme.vars.palette.divider }),
|
|
multiline: {
|
|
padding: 10,
|
|
alignItems: "flex-start",
|
|
minHeight: "unset",
|
|
},
|
|
input: {
|
|
paddingLeft: 0,
|
|
paddingRight: 0,
|
|
paddingTop: 9,
|
|
paddingBottom: 9,
|
|
lineHeight: 1.45,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
},
|
|
inputMultiline: {
|
|
paddingTop: 0,
|
|
paddingBottom: 0,
|
|
lineHeight: 1.5,
|
|
},
|
|
},
|
|
},
|
|
MuiInputBase: {
|
|
styleOverrides: {
|
|
input: {
|
|
"&::placeholder": {
|
|
opacity: 0.72,
|
|
},
|
|
},
|
|
inputMultiline: {
|
|
"&::placeholder": {
|
|
opacity: 0.72,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
MuiListItemButton: {
|
|
styleOverrides: {
|
|
root: ({ theme }: any) => ({
|
|
borderRadius: 8,
|
|
"&.Mui-selected": {
|
|
backgroundColor: theme.vars.palette.action.hover,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
MuiTableCell: {
|
|
styleOverrides: {
|
|
head: ({ theme }: any) => ({
|
|
fontWeight: 600,
|
|
color: theme.vars.palette.text.primary,
|
|
background: theme.vars.palette.grey[100],
|
|
}),
|
|
root: {
|
|
paddingTop: 10,
|
|
paddingBottom: 10,
|
|
},
|
|
},
|
|
},
|
|
} as any;
|
|
|
|
return theme;
|
|
};
|