diff --git a/job-tracker-ui/src/components/GradientButton.tsx b/job-tracker-ui/src/components/GradientButton.tsx index 7876eed..e5c60e4 100644 --- a/job-tracker-ui/src/components/GradientButton.tsx +++ b/job-tracker-ui/src/components/GradientButton.tsx @@ -4,6 +4,14 @@ import Button, { ButtonProps } from "@mui/material/Button"; import { GRADIENT_CTA } from "../theme"; +// Static, not derived from theme.vars.customShadows: an inline sx callback always executes +// against whatever theme is in context, including MUI's bare default theme when a component +// renders without this app's ThemeProvider (every test in this repo does exactly that -- no +// test wraps components in one), and the default theme has no `.vars`. theme.components +// styleOverrides (e.g. MuiCard) don't have this problem since they only run when this app's +// real theme is actually provided, but inline sx isn't gated that way. +const GRADIENT_SHADOW = "0px 4px 14px -4px rgba(99,102,241,0.45)"; + /** * The mockup's gradient AI/primary-action pill ("Tailor my CV for this role", "See the * interface tour"). Reserve this for the single most important AI-assist or hero action on a @@ -17,14 +25,14 @@ export default function GradientButton({ sx, disabled, ...props }: ButtonProps) disableElevation disabled={disabled} sx={[ - (theme: any) => ({ + { background: disabled ? undefined : GRADIENT_CTA, color: disabled ? undefined : "#fff", - boxShadow: disabled ? undefined : theme.vars.customShadows.gradientButton, + boxShadow: disabled ? undefined : GRADIENT_SHADOW, "&:hover": disabled ? undefined - : { background: GRADIENT_CTA, filter: "brightness(1.06)", boxShadow: theme.vars.customShadows.gradientButton }, - }), + : { background: GRADIENT_CTA, filter: "brightness(1.06)", boxShadow: GRADIENT_SHADOW }, + }, ...(Array.isArray(sx) ? sx : sx ? [sx] : []), ]} {...props} diff --git a/job-tracker-ui/src/components/JobDetailsDialog.tsx b/job-tracker-ui/src/components/JobDetailsDialog.tsx index c3d5877..4dfbbb5 100644 --- a/job-tracker-ui/src/components/JobDetailsDialog.tsx +++ b/job-tracker-ui/src/components/JobDetailsDialog.tsx @@ -29,6 +29,7 @@ import { emptyTailoredCvDraft, joinLines, normalizeTailoredCvDraft, splitLines } import Correspondence from "./Correspondence"; import Attachments from "./Attachments"; import JobFlowBar from "./JobFlowBar"; +import GradientButton from "./GradientButton"; import { useI18n } from "../i18n/I18nProvider"; import { useJobWorkspaceBaseData } from "./job-workspace/useJobWorkspaceBaseData"; import { useWorkspaceTabCache } from "./job-workspace/useWorkspaceTabCache"; @@ -414,7 +415,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0, const showAiAttachmentPicker = tab >= 3 && tab <= 7 && jobAttachments.length > 0; const attachmentPicker = showAiAttachmentPicker ? ( - + {t("jobDetailsAttachmentContextPicker")} @@ -620,22 +621,37 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0, return ( - + - {t("jobTableOpen")} - {title} + {t("jobTableOpen")} + {title} - {job && } + {job && } - + {summaryFirstText} - setTab(v)} sx={{ mb: 2 }} variant="scrollable" allowScrollButtonsMobile> + setTab(v)} + sx={{ mb: 2, "& .MuiTab-root": { fontWeight: 600, textTransform: "none", minHeight: 44 } }} + variant="scrollable" + allowScrollButtonsMobile + > @@ -672,8 +688,8 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0, {tab === 0 && ( - {t("jobDetailsStrategySnapshot")} - + }}>{loadingStrategySnapshot ? t("jobDetailsRefreshing") : t("jobDetailsGenerateStrategySnapshot")} {candidateFit || focusPlan ? ( - + {candidateFit ? = 75 ? "success" : candidateFit.matchScore >= 55 ? "warning" : "default"} label={t("jobDetailsMatchPercent", { count: candidateFit.matchScore })} /> : null} {candidateFit?.fitLevel ? : null} @@ -949,13 +965,13 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0, - + Rendered CV snapshot This plain-text snapshot stays deterministic and is what the job stores immediately after saving the draft. {t("jobDetailsLastUpdated", { value: job?.tailoredCvUpdatedAt ? new Date(job.tailoredCvUpdatedAt).toLocaleString() : t("jobDetailsNotSavedYet") })} - + PDF-style preview Preview and PDF export use the same HTML template contract. Accent color and photo settings apply here. {tailoredCvPreview ? ( @@ -964,7 +980,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0, Build the PDF layout preview to inspect the ATS template before downloading. )} - + Saved job material Saving the tailored draft updates the job-scoped CV text without touching your master profile. Tailored CV: {(job?.tailoredCvText ?? "").trim() ? "Saved on this job" : "Not saved yet"} @@ -1045,7 +1061,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0, statusLabel={recruiterMessageStatus.label} statusColor={recruiterMessageStatus.color} /> - + Saved working material These saved copies are what follow-up drafting and later slices can trust and reuse. @@ -1067,7 +1083,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0, {loadingDraft ? : followUpDraft ? ( - + Follow-up context @@ -1214,7 +1230,7 @@ function MatchScoreCard({ score, loading }: { score: MatchScore | null; loading: if (loading && !score) { return ( - + {t("matchScoreLoading")} @@ -1228,7 +1244,7 @@ function MatchScoreCard({ score, loading }: { score: MatchScore | null; loading: const bandLabel = t(`matchScoreBand_${score.band}` as any) || score.band; return ( - + {score.hasEnoughSignal ? ( @@ -1314,7 +1330,7 @@ function ListCard({ title, items, subtitle }: { title: string; items: string[]; const { t } = useI18n(); return ( - + {title} @@ -1333,7 +1349,7 @@ function WorkspaceDraftCard({ title, value, onChange, statusLabel, statusColor } const { t } = useI18n(); return ( - + {title} @@ -1355,7 +1371,7 @@ function DraftCard({ title, content, onSave, saving }: { title: string; content: }, [content]); return ( - + {title}