Files
jobtrackingapp/job-tracker-ui/src/theme.ts
T
cesnimda 33d899c243
CI and Deploy / test (pull_request) Successful in 2m8s
CI and Deploy / deploy (pull_request) Has been skipped
fix(auth): Google Sign-In audience mismatch + remove per-user accent color
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.
2026-07-12 02:43:10 +02:00

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;
};