Skip to content

Design System

Quick reference for UI patterns. Read before adding UI components.

Default Radix tooltip is blue (bg-primary). Always override to gray:

<TooltipContent
side="right"
className="bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300"
>

When to use what:

  • <InfoButton> - For help/info text next to form fields (shows ℹ️ icon, handles styling automatically)
  • <TooltipContent> with override - For any other tooltip (action labels like “Rename model”, status text, etc.)

Default Value Highlighting - Yellow Background

Section titled “Default Value Highlighting - Yellow Background”

When displaying values that haven’t been edited (still at defaults), use:

// Light mode: yellow, Dark mode: subtle amber
className={cn(isDefault && "bg-amber-50 dark:bg-amber-950/30")}

Components with isDefault prop: Input, Select, NumberInput, MonthPicker

Section titled “Dropdown/Select Hover States - Use bg-muted”

All dropdown items (Select, DropdownMenu) use focus:bg-muted for hover/focus state. Never use bg-accent (yellow) for menu item highlighting.


Info icon with properly styled tooltip. Supports rich content and optional source citations.

import { InfoButton } from "@/components/ui/info-button";
// Simple usage
<InfoButton>Simple explanation text</InfoButton>
// With sources (for reference data fields)
<InfoButton sources={sources} defaultValue={85}>
Percentage of patients with healthcare access
</InfoButton>
// Rich content
<InfoButton variant="amber" size="md">
<div className="space-y-2">
<p className="font-semibold">Title</p>
<ul className="list-disc pl-4"><li>Point 1</li></ul>
</div>
</InfoButton>
PropOptionsDefaultDescription
variant"default" | "amber""default"Icon color variant
size"sm" | "md""sm"Icon size
sourcesSourceEntry[] | nullundefinedReference data sources (shows below description)
defaultValuestring | number | nullundefinedDefault value to display with sources

Sources Display: When sources is provided, a dashed divider separates the description from source cards showing name, URL, sample size, and comments.

import { Button } from "@/components/ui/button";
<Button variant="default">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
<Button size="icon"><Icon className="h-4 w-4" /></Button>

Per-section chart color palette picker. Each chart section has its own independent palette atom.

import { ColorPaletteSelector } from "@/features/reports/forecasting/components/ColorPaletteSelector";
import { salesPaletteAtom } from "@/features/reports/forecasting/atoms";
<ColorPaletteSelector paletteAtom={salesPaletteAtom} />
AtomDefaultUsed by
salesPaletteAtom"monochrome"Sales Chart
monteCarloPaletteAtom"monochrome"Monte Carlo
tornadoPaletteAtom"vibrant"Tornado (Sensitivity)

Palettes: "monochrome", "corporate-blue", "vibrant", "earth-tones", "high-contrast" (defined in constants.ts).

The following components from src/components/ui/ are available but rarely need customization: Accordion, AlertDialog, Badge, Calendar, Card, Checkbox, Collapsible, Dialog, Popover, SectionCard, SectionNav, Separator, Switch, Table. Use them with their default styling.

SectionCard props for header customization:

  • titleTooltip?: ReactNode — renders an InfoButton next to the section title
  • headerMeta?: ReactNode — content between title/tooltip and header actions (e.g., SimulationCountBadge)
<SectionCard id="section-montecarlo" title="Monte Carlo" titleTooltip="..." headerMeta={<SimulationCountBadge />}>

For icon-only buttons (edit, delete), use semantic hover colors:

// Edit/Rename - Amber
<button className="text-muted-foreground hover:text-amber-600 hover:bg-amber-50 rounded p-1 transition-colors">
<Pencil className="h-4 w-4" />
</button>
// Delete - Destructive
<button className="text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded p-1 transition-colors">
<Trash2 className="h-4 w-4" />
</button>
// Info - Blue
<button className="text-muted-foreground hover:text-blue-600 hover:bg-blue-50 rounded p-1 transition-colors">
<Info className="h-4 w-4" />
</button>

Always include aria-label for accessibility.

<div className="flex items-center gap-2">
<Label>Field Name</Label>
<InfoButton>Explanation of what this field does</InfoButton>
</div>
<NumberInput value={value} onChange={setValue} isDefault={isDefault} />

For fields that have reference data (amber highlighted), use useReferenceSource to look up sources:

import { useReferenceSource } from "@/features/reports/forecasting/hooks/useReferenceSource";
// Inside component
const { sources, value } = useReferenceSource("variableName", lineId); // lineId optional
<div className="flex items-center gap-2">
<Label>Field Name</Label>
<InfoButton sources={sources} defaultValue={value}>
Explanation of what this field does
</InfoButton>
</div>
<NumberInput value={fieldValue} onChange={setValue} isDefault={isDefault} />

The hook looks up sources from reference data based on current geo/indication context.

When a parent field has editable sub-breakdowns (e.g., Early Stage → Localized + Locally Advanced):

{/* Parent field - read-only when computed from subs */}
<div className="flex items-center justify-between">
<Label>Parent Field</Label>
<div className="text-sm text-muted-foreground tabular-nums px-3 py-1 bg-muted rounded-md">
{computedValue}%
</div>
</div>
{/* Sub-variables - indented with left border */}
<div className="ml-4 space-y-3 border-l-2 border-muted pl-4">
<div className="flex items-center justify-between">
<Label className="text-muted-foreground">Sub Field</Label>
<div className="flex items-center gap-2">
<RollbackButton visible={!isDefault} onRollback={handleReset} />
<NumberInput value={value} onChange={onChange} />
</div>
</div>
</div>

Pattern: RollbackButton always sits next to the value input (right side), inside flex items-center gap-2.

For optional filters that show additional info when active (e.g., biomarker filtering):

{/* Section header */}
<div className="pt-4 border-t">
<div className="flex items-center gap-1.5 mb-3">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Filter Name
</div>
<InfoButton>Explanation of what the filter does</InfoButton>
</div>
{/* Toggle */}
<div className="flex items-center justify-between">
<span className="text-sm">{label}</span>
<Switch
checked={isActive}
onCheckedChange={(checked) => onToggle(checked ? id : null)}
aria-label={`Toggle ${label} filter`}
/>
</div>
{/* Conditional details (shown when active) */}
{isActive && (
<div className="mt-3 space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1">
<span className="text-muted-foreground">Detail Label</span>
<InfoButton sources={sources}>Tooltip text</InfoButton>
</div>
<span className="font-medium tabular-nums">{value}%</span>
</div>
</div>
)}
</div>

Used in: FirstLineExtras in LinePanel (biomarker filter)

Standalone card inside Patient Flow (bg-[#F9FAFA] dark:bg-muted/30, border-muted) with Dna icon header. Active sub-fields use border-l-2 border-primary/20 indent guide. Rendered via FirstLineExtras on first-line panels only, positioned before Healthcare Access.

Reference lines show line name as uppercase label, addressable count as text-3xl font-bold text-primary, and an italic origin footnote (border-t, CornerDownRight icon) linking back to stage totals. Custom lines use an editable name input + delete button + badge instead.

Displayed at the end of the Patient Flow section for all lines (reference and custom). Uses rounded-lg border border-secondary bg-secondary/40 with Users icon header matching the Biomarker card pattern. Shows eligible count as text-lg font-bold text-primary.


ElementClasses
Tooltip bgbg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300
Default highlightbg-amber-50 dark:bg-amber-950/30
Edit hoverhover:text-amber-600 hover:bg-amber-50
Delete hoverhover:text-destructive hover:bg-destructive/10
Icon sizeh-4 w-4
Icon button paddingp-1
Nested sub-variable indentml-4 border-l-2 border-muted pl-4
Read-only computed valuetext-sm text-muted-foreground tabular-nums px-3 py-1 bg-muted rounded-md
Section paddingp-8
Gap (small)gap-2
Gap (medium)gap-4