Repository navigation
Components
The web app's components sit in two layers. apps/web/components/ui holds thirteen shadcn-style primitives generated in the base-nova style: thin wrappers around Base UI with Tailwind classes, data-slot attributes for styling hooks, and class-variance-authority (cva) variants where a component has them. apps/web/components holds the app-level components that compose them into pages. Every primitive accepts className and merges it last with cn, so callers can override any class. This page catalogues both layers. Token values are in Design Tokens; the patterns they form are in Visual System.
lib/utils.ts is one line: export { cn } from "cn";. The cn package (v0.4) is a compiled drop-in replacement for the usual clsx + tailwind-merge pair: it joins conditional class values (strings, arrays, objects, falsy values) and resolves Tailwind conflicts so the last class wins.
cn("px-2 py-1 bg-muted", isActive && "bg-primary", className);
// "px-2 py-1 bg-primary …" when isActiveThe primitives import cn from "cn" directly; app code may use either import. buttonVariants wraps its cva output in cn so variant classes override base classes even when used without Button (for example on a Link), where an unmerged border-transparent would hide the outline variant's border.
| Convention | Detail |
|---|---|
data-slot |
Every rendered part carries data-slot="<name>" (button, card-header, field-error, …). Parents style children through it, for example has-data-[slot=card-footer]:pb-0. |
render prop |
Base UI components accept render={<Element />} to change the rendered element while keeping behaviour, for example <DropdownMenuTrigger render={<Button variant="ghost" size="icon" />}> or <DropdownMenuLinkItem render={<Link href="/settings" />}>. |
| State attributes | Base UI sets data-open, data-closed, data-checked, data-unchecked, data-disabled, data-highlighted, data-orientation; the custom variants in shadcn/tailwind.css (data-open:, data-checked:, data-horizontal: …) target them. |
| Focus | Focusable primitives use focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50. |
| Invalid |
aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20. |
| Icons | Unsized child SVGs become size-4 (smaller in small sizes) and pointer-events-none shrink-0. data-icon="inline-start" / "inline-end" on an icon tightens the padding on that side. |
button.tsx. Base UI Button. Exports Button and buttonVariants.
Base: inline-flex, rounded-lg, 1 px transparent border, text-sm font-medium, whitespace-nowrap, transition-all, a 1 px downward nudge on :active (except menu triggers), disabled:opacity-50 disabled:pointer-events-none.
variant |
Classes (light theme) | Used for |
|---|---|---|
default |
bg-primary text-primary-foreground hover:bg-primary/80 |
Primary actions: Analyze, Save, Start a run |
outline |
border-border bg-background hover:bg-muted; aria-expanded:bg-muted
|
Secondary actions, account menu, header links, Load more |
secondary |
bg-secondary text-secondary-foreground; hover mixes 5% foreground into secondary (color-mix(in oklch, …)) |
"Use defaults" in the wizard |
ghost |
hover:bg-muted hover:text-foreground; aria-expanded:bg-muted
|
Primary nav, mobile menu trigger, Hide checklist, dismiss icon |
destructive |
bg-destructive/10 text-destructive hover:bg-destructive/20 |
"Clear data" in the confirmation dialog |
link |
text-primary underline-offset-4 hover:underline |
"Finish later" in the wizard |
size |
Height | Padding / shape | Icon size |
|---|---|---|---|
xs |
h-6 (24 px) |
px-2 text-xs, radius min(--radius-md, 10px)
|
12 px |
sm |
h-7 (28 px) |
px-2.5 text-[0.8rem], radius min(--radius-md, 12px)
|
14 px |
default |
h-8 (32 px) |
px-2.5 gap-1.5 |
16 px |
lg |
h-9 (36 px) |
px-2.5 gap-1.5 |
16 px |
icon-xs |
size-6 |
square | 12 px |
icon-sm |
size-7 |
square | 16 px |
icon |
size-8 |
square | 16 px |
icon-lg |
size-9 |
square | 16 px |
Props: all Base UI Button.Props plus variant and size (defaults default / default). Use buttonVariants({ variant, size, className }) to style links as buttons.
badge.tsx. Rendered with Base UI useRender (default tag span), so it accepts render. Exports Badge and badgeVariants.
Base: h-5, rounded-4xl (a pill), px-2 py-0.5 text-xs font-medium, transparent border, [&>svg]:size-3!.
variant |
Classes |
|---|---|
default |
bg-primary text-primary-foreground; [a]:hover:bg-primary/80 when rendered as a link |
secondary |
bg-secondary text-secondary-foreground |
destructive |
bg-destructive/10 text-destructive |
outline |
border-border text-foreground; [a]:hover:bg-muted
|
ghost |
hover:bg-muted hover:text-muted-foreground |
link |
text-primary underline-offset-4 hover:underline |
card.tsx. Plain divs, no cva. Exports Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter.
| Part | Notes |
|---|---|
Card |
size?: "default" | "sm". rounded-xl bg-card ring-1 ring-foreground/10 text-sm, vertical gap and padding from --card-spacing (1rem, or 0.75rem for sm). Drops bottom padding when it has a footer and top padding when it starts with an image. |
CardHeader |
A grid; becomes two columns when it contains a CardAction. Uses a container query name card-header. |
CardTitle |
font-heading text-base font-medium leading-snug (text-sm in a small card). |
CardDescription |
text-sm text-muted-foreground. |
CardAction |
Top-right slot in the header (column 2, spanning two rows). |
CardContent |
Horizontal padding from --card-spacing. |
CardFooter |
border-t bg-muted/50, rounded bottom corners. |
App cards usually set className="[--card-spacing:--spacing(5)]" for 1.25rem padding.
field.tsx. Layout and messaging for form controls. Exports Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle.
| Part | Notes |
|---|---|
Field |
role="group", cva orientation: vertical (default, stacked, full width), horizontal (row, for switches), responsive (stacked until the field-group container is md). data-invalid={true} turns the field's text destructive. |
FieldGroup |
flex flex-col gap-5, the field-group container for responsive fields. |
FieldSet / FieldLegend
|
Native fieldset / legend; FieldLegend has variant: "legend" | "label" (base or small text). |
FieldLabel |
Wraps Label. When it contains a nested Field it becomes a bordered, selectable card (checked state tinted with primary). |
FieldTitle |
A label-styled div for non-label titles. |
FieldContent |
Column for a label plus description beside a control. |
FieldDescription |
text-sm text-muted-foreground; links inside are underlined. |
FieldSeparator |
A Separator with optional centred text. |
FieldError |
role="alert", text-sm text-destructive. Renders children, or one message from errors, or a de-duplicated bulleted list; renders nothing when empty. |
input.tsx. Base UI Input, no variants. h-8 rounded-lg border-input bg-transparent px-2.5, text-base below md and text-sm above, placeholder in muted-foreground, focus ring, invalid styling, disabled:bg-input/50. Styled file-input button. Props: native input props.
label.tsx. Native label: flex items-center gap-2 text-sm font-medium leading-none select-none; dims when a sibling peer control or the parent field is disabled.
switch.tsx. Base UI Switch. size?: "default" \| "sm": track 32 × 18.4 px with a 16 px thumb, or 24 × 14 px with a 12 px thumb. Checked track bg-primary, unchecked bg-input, thumb bg-background. An after: pseudo-element enlarges the hit area by 12 px horizontally and 8 px vertically. Props: Base UI Switch.Root.Props (checked, onCheckedChange, …).
progress.tsx. Base UI Progress. Exports Progress, ProgressTrack, ProgressIndicator, ProgressLabel, ProgressValue. Progress renders its children (label, value) followed by a 4 px (h-1) rounded-full bg-muted track and a bg-primary indicator with transition-all. value={null} is indeterminate. The app recolours the indicator to Match with [&_[data-slot=progress-indicator]]:bg-match.
separator.tsx. Base UI Separator. orientation horizontal (default, h-px w-full) or vertical (w-px self-stretch), bg-border.
skeleton.tsx. A div with animate-pulse rounded-md bg-muted. Size it with className.
alert-dialog.tsx. Base UI AlertDialog. Exports AlertDialog, AlertDialogTrigger, AlertDialogPortal, AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogMedia, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel.
| Part | Notes |
|---|---|
AlertDialogContent |
size?: "default" | "sm". Portalled, centred, max-w-xs (sm:max-w-sm for default), rounded-xl bg-popover p-4 ring-1 ring-foreground/10, fade and zoom in/out over 100 ms. Includes the overlay. |
AlertDialogOverlay |
bg-black/10 with backdrop-blur-xs where supported. |
AlertDialogHeader |
Centred on mobile, left-aligned from sm in the default size; lays out an optional AlertDialogMedia icon beside the text. |
AlertDialogFooter |
Bleeds to the dialog edges with border-t bg-muted/50; buttons stack in reverse on mobile, row from sm, and two equal columns in the small size. |
AlertDialogTitle |
font-heading text-base font-medium. |
AlertDialogDescription |
text-sm text-muted-foreground, balanced text. |
AlertDialogAction |
A Button. |
AlertDialogCancel |
Base UI Close rendered as a Button (variant="outline" by default; accepts variant and size). |
dropdown-menu.tsx. Base UI Menu. Exports DropdownMenu, DropdownMenuTrigger, DropdownMenuPortal, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator.
| Part | Notes |
|---|---|
DropdownMenuContent |
Positioner props align (default start), alignOffset (0), side (bottom), sideOffset (4). min-w-40 rounded-lg bg-popover p-1 shadow-md ring-1 ring-foreground/10, capped at the available height, fade and zoom from the trigger. |
DropdownMenuItem |
variant?: "default" | "destructive". Highlighted item gets bg-muted; aria-[current=page] makes it semibold. |
DropdownMenuLinkItem |
A navigating item (closeOnClick defaults to true); pass render={<Link href="…" />} for client-side navigation. |
DropdownMenuLabel |
text-xs font-medium text-muted-foreground. |
DropdownMenuSeparator |
h-px bg-border, bleeding to the edges. |
sonner.tsx. Wraps Sonner's Toaster with lucide icons (CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, spinning Loader2Icon), colours from the popover tokens, the base radius, and the toast class cn-toast. It reads the theme from next-themes and falls back to "system". Mounted once in app/layout.tsx; call toast.success(…), toast.error(…) or toast.info(…) from sonner anywhere.
One line each, grouped by where they appear. All are in apps/web/components.
| Component | File | Purpose |
|---|---|---|
Logo, LogoMark
|
logo.tsx |
The inline SVG mark (coloured with --primary, --highlight, --highlight-deep) and the linked lockup "Job Matcher". See Brand. |
AppShell, useSessionGate
|
app-shell.tsx |
Signed-in layout: header, primary and mobile nav, user menu, session guard and account-switch isolation. |
AppPage |
app-page.tsx |
Page <main> with the h1; shows a skeleton or a retry until the session gate says signed in. |
HeaderNav |
header-nav.tsx |
Landing page button: "Open app" when signed in, "Sign in" otherwise. |
VerdictBadge, HintBadge, Verdict
|
verdict-badge.tsx |
Badge per verdict (match, no-match, skipped, expired, failed, checking) with a title tooltip and a screen-reader explanation beside it. |
ScoringHelp |
scoring-help.tsx |
A <details> "How matching works" explainer: filters, optional title ranking, then the AI check. |
| Component | File | Purpose |
|---|---|---|
ExampleRun |
example-run.tsx |
Static sample run card with counters, a progress bar and five jobs; matched requirements are highlighted with <mark>. |
| Component | File | Purpose |
|---|---|---|
AuthPage |
auth-page.tsx |
Page frame with the logo for both auth routes. |
AuthForm |
auth-form.tsx |
Email/password form calling better-auth, mapping error codes to friendly messages, clearing the query cache and redirecting to the safe next page. |
| Component | File | Purpose |
|---|---|---|
HomeView |
home-view.tsx |
Overview page: one primary card chosen from active run, setup checklist, all-set, setup and start-run cards; pending matches and latest run cards; next scheduled check line. |
SetupChecklist, AllSetCard, useAllSet
|
setup-checklist.tsx |
The onboarding checklist card and the one-time "You're all set" card. |
| Component | File | Purpose |
|---|---|---|
WelcomeView |
welcome-view.tsx |
Wizard frame: URL-driven step, progress bar, step nav, focus management. |
WelcomeStep, ResumeStep, PreferencesStep, SourceStep, FirstRunStep
|
welcome-steps.tsx |
The five step bodies; three of them embed Settings sections. |
| Component | File | Purpose |
|---|---|---|
AnalyzeView |
analyze-view.tsx |
Source checkboxes and the Analyze button, the latest run card, and past runs; points to Settings when a resume or source is missing. |
RunCard, statusLabels, plannedJobs
|
run-card.tsx |
A run's status, counters, Firecrawl credits, progress and every job result with its scrape-source badge, verdict and reason. |
PastRuns |
past-runs.tsx |
Ruled list of earlier runs linking to their details. |
RetrySaveButton |
retry-save-button.tsx |
Retries saving a match the run could not save. |
RunDetailsView |
run-details-view.tsx |
Run details page with the first-run explainer and an outcome notice (see matches, or how to widen the search). |
FirstRunExplainer |
first-run-explainer.tsx |
Dismissible card defining each run status and counter. |
| Component | File | Purpose |
|---|---|---|
MatchesView |
matches-view.tsx |
Pending / Applied tabs, paginated match list with a status toggle per match, and empty states. |
| Component | File | Purpose |
|---|---|---|
SettingsView |
settings-view.tsx |
Section nav and the six sections; follows the URL hash to a section heading. |
SettingsSection, SettingField, SectionForm, SettingsLoader, EmbeddedSections, useSectionSave, settingsSections
|
settings-section.tsx |
Shared section frame, field wrapper, form with submit button, loader skeleton, heading-less mode for the wizard, and the serialised partial-save hook. |
ResumeSection |
resume-section.tsx |
Resume URL. |
JobSources |
job-sources.tsx |
Add a careers page or a search, list sources with type badges, change a source's type or board name, toggle the site map, accept a detected ATS board, delete. |
MatchingSection |
matching-section.tsx |
Experience, target years, cities, title include/exclude words, include-unknown switch. |
RunSection |
run-section.tsx |
Jobs per run and the title-ranking switch. |
ScheduleSection |
schedule-section.tsx |
Schedule interval and Discord webhook, flagging a saved non-Discord webhook. |
ClearCache |
clear-cache.tsx |
"Clear saved scrape data" with an alert-dialog confirmation. |
Run the shadcn CLI from apps/web (for example bunx shadcn@latest add tooltip). components.json sends the file to components/ui in the base-nova style with token utilities. Then:
- Replace any
@/lib/utilsimport withcnif you want to match the existing files (both work). - Remove
dark:classes or plan for them: the app has no dark theme, but Tailwind's defaultdarkvariant follows the OS (see Visual System). - Run
bun run lintandbun run typecheckfrom the repository root.
Job Matcher · Website · Report an issue · MIT license
Using Job Matcher
System design
Matching engine
Interface and brand
Engineering