Skip to content

Components

Karthik P edited this page Oct 15, 2026 · 1 revision

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.

Utility

cn

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 isActive

The 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.

Conventions shared by the primitives

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.

Primitives (components/ui)

Button

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

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

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 family

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

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

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

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

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

separator.tsx. Base UI Separator. orientation horizontal (default, h-px w-full) or vertical (w-px self-stretch), bg-border.

Skeleton

skeleton.tsx. A div with animate-pulse rounded-md bg-muted. Size it with className.

Alert dialog

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

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.

Toaster

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.

App-level components

One line each, grouped by where they appear. All are in apps/web/components.

Shared

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.

Landing page (/)

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>.

Sign-in and sign-up

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.

Home (/home)

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.

Setup wizard (/welcome)

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.

Analyze and run details (/analyze, /runs/[id])

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.

Matches (/matches)

Component File Purpose
MatchesView matches-view.tsx Pending / Applied tabs, paginated match list with a status toggle per match, and empty states.

Settings (/settings)

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.

Adding a primitive

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/utils import with cn if 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 default dark variant follows the OS (see Visual System).
  • Run bun run lint and bun run typecheck from the repository root.

Clone this wiki locally