Files
jobtrackingapp/job-tracker-ui/src/theme.ts
T
cesnimda 1ab92e5c9d
CI and Deploy / test (push) Successful in 2m21s
CI and Deploy / deploy (push) Successful in 39s
feat(ui): establish premium design foundation from mockups
Extract design tokens from the mockup set (F:\Pictures\website\jobtracker\new
dashboard, pipeline, job-workspace, features, workflow screens) into the
central theme so every screen picks the change up automatically:

- Heading weight: h1-h4 go bold/black (800/700) to match the mockups' heavy
  display type; h5/h6 stay a lighter semibold so dense screens don't turn
  into a wall of black text.
- Card shadow: replace the flat 1px "section" shadow + visible border with a
  soft floating shadow and no border, matching how mockup cards sit on the
  grey page background.
- Border radius: 10/12/8px -> 14/16/10px across shape/card/button defaults,
  matching the mockups' rounder corners.
- New GradientButton component wrapping the mockup's signature indigo->cyan
  CTA gradient ("Tailor my CV for this role", "See the interface tour"),
  reserved for the single most important AI-assist/hero action per screen.

The dark navy sidebar (#0f172a) already matched the mockups from an earlier
pass -- untouched here.

Verified: tsc clean, full frontend suite green (65/65). Live visual
screenshot verification wasn't possible -- the Browser pane's screenshot
tool times out in this environment; verified structurally via read_page
and the app rendering without console errors instead.
2026-07-13 08:50:26 +02:00

378 lines
11 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)}`,
// Mockup cards float on the grey page background with no visible border, just a soft,
// fairly generous shadow (dashboard stat tiles, pipeline columns' cards, job-workspace
// panels) -- the old 1px "section" shadow read as flat/bordered rather than elevated.
section: `0px 1px 2px 0px ${alpha(shadowColor, 0.04)}, 0px 8px 24px -12px ${alpha(shadowColor, 0.12)}`,
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)}`,
// Gradient CTA (mockup's "Tailor my CV" / "See the interface tour" buttons) gets its own
// colored glow instead of the flat neutral button shadow.
gradientButton: `0px 4px 14px -4px ${alpha(primaryColor, 0.45)}`,
};
}
// Mockup's signature AI/primary-action gradient (dashboard "Tailor my CV", landing "See the
// interface tour"). Exported so every gradient CTA across the app stays pixel-identical
// instead of each screen inventing its own two-color mix -- consumed via <GradientButton>
// (src/components/GradientButton.tsx), not applied as a MUI Button variant (MUI's variant
// override machinery needs TS module augmentation for a genuinely new variant name; a plain
// wrapper component is far less code for the same result).
export const GRADIENT_CTA = "linear-gradient(90deg, #6366F1 0%, #22D3EE 100%)";
function buildTypography() {
return {
fontFamily: "'Archivo', system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif",
letterSpacing: 0,
// Mockup headings (F:\Pictures\website\jobtracker\new) are bold/black-weight display
// type ("Dashboard", "One workspace for the whole search") -- 500 across the board read
// as noticeably lighter/flatter than that reference. h1-h4 (page titles, hero, section
// headers) go heavy; h5/h6 (card titles, in-context labels) stay a lighter semibold so
// dense screens don't turn into a wall of black text.
h1: { fontWeight: 800, fontSize: 36, lineHeight: "40px", letterSpacing: "-0.02em" },
h2: { fontWeight: 800, fontSize: 30, lineHeight: "34px", letterSpacing: "-0.02em" },
h3: { fontWeight: 800, fontSize: 26, lineHeight: "30px", letterSpacing: "-0.01em" },
h4: { fontWeight: 700, fontSize: 22, lineHeight: "26px", letterSpacing: "-0.01em" },
h5: { fontWeight: 700, fontSize: 18, lineHeight: "22px" },
h6: { fontWeight: 600, fontSize: 16, lineHeight: "20px" },
subtitle1: { fontWeight: 600, fontSize: 14, lineHeight: "18px" },
subtitle2: { fontWeight: 600, 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: 700, 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, fontWeight: 600 },
};
}
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) },
},
// Mockup radius reads noticeably rounder than the previous 10/8px defaults across cards,
// stat tiles, kanban cards and buttons.
shape: { borderRadius: 14 },
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) => ({
// No border -- mockup cards float on the grey page background purely via shadow.
border: "none",
borderRadius: 16,
backgroundImage: "none",
boxShadow: theme.vars.customShadows.section,
}),
},
},
MuiButton: {
defaultProps: { disableFocusRipple: true },
styleOverrides: {
root: ({ theme }: any) => ({
borderRadius: 10,
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;
};