Component Hierarchy
Components are organized into three tiers based on reusability and abstraction level.
For the full frontend architecture (five-layer data flow, controller hooks, etc.), see the Frontend Architecture guide.
- Tier 1 — Primitives
- Tier 2 — Patterns
- Tier 3 — Pages
Location: components/common/ui/
Atomic UI elements with no business logic. Shared across everything.
Inputs
| Component | Purpose | Key Props |
|---|---|---|
InputNumber | Numeric input with scientific notation, min/max validation | value, onValueChange, format, decimals, variant, min, max |
InputText | Text input with pattern validation and status warnings | value, onValueChange, variant, pattern, warnWhen |
InputTextArea | Multi-line text with optional character counter | value, onValueChange, rows, maxLength, showCounter |
Select | Dropdown with option normalization | value, onChange, options, variant, fullWidth |
TextInput | Bare text input (no wrapper) — basic HTML input styling | variant, type |
Checkbox | Native checkbox with size variants | checked, onChange, size (sm/md/lg) |
Actions & Overlays
| Component | Purpose | Key Props |
|---|---|---|
Button | Clickable actions with variant support and optional link behavior | variant (default/bare), disabled, to (converts to Link) |
Modal | Full-screen overlay dialog with header, footer, and close button | isOpen, title, onClose, onSave, showFooter |
Popovers
All popovers share the same props: title, children, placement (left/center/right), width, offset, id.
| Component | Trigger | Usage |
|---|---|---|
InfoPopover | "?" button (steel blue) | Contextual help alongside labels and checkboxes |
NotesPopover | "!" button (red) | Notes and warnings about a field or section |
DemoPopover | "Demo" toggle button | Demo/placeholder content indicators |
FeaturePopover | "!" button (purple) | Feature request information |
Display
| Component | Purpose | Key Props |
|---|---|---|
Bubble | Rounded display bubble with inset shading — used for gradation classification results | value, size (sm/md/lg), variant (neutral/meets/fails/etc.) |
Input Variants
All input primitives (InputNumber, InputText, Select) share a consistent size variant system:
| Variant | Height | Usage |
|---|---|---|
default | 36px | Standard form fields |
compact | 32px | Inline fields, tight spaces |
condensed | 32px | Very tight spaces, minimal padding |
xs | 28px | Smallest (badges, mini forms) |
table | 40px | Inside table cells |
tablePlain | — | Transparent, borderless table editing |
none | — | Custom styling only |
Reusable compositions that know about a type of content but not about specific pages.
Layout
| Component | Location | Purpose |
|---|---|---|
Section | components/common/layout/ | Primary content container with steel accent bar, title, optional collapse |
PageContainer | components/common/layout/ | Root page wrapper with background color and padding |
FormFieldGroup | components/common/layout/ | Responsive grid for form fields (1–4 columns) |
HorizontalSubNav | components/common/layout/ | Sticky sub-navigation bar with tab links |
Form Patterns
| Component | Location | Purpose |
|---|---|---|
InputField | components/common/ui/ | Label + input wrapper that auto-detects input type from props |
CheckboxRow | components/common/ui/ | Checkbox + label + optional inline input (number, text, or select) |
CheckboxOptionsPanel | components/common/ui/ | Container for a group of checkbox options with title and spacing |
Data Display
| Component | Location | Purpose |
|---|---|---|
EditableGridTable | components/common/table/ | Full-featured editable table with clipboard and keyboard support |
BaseChart | components/common/chart/ | Chart.js wrapper for all chart types |
MessagesBanner | components/common/feedback/ | Collapsible banner for validation messages (inputs/warnings/errors) |
Section Component
Section is the standard container for organizing page content. Use it for all new pages.
import Section from '@components/common/layout/Section';
<Section
title="Embankment Components"
hint="(check all that apply)"
description="Select the structural features present in the embankment."
collapsible={true}
defaultExpanded={true}
contentClassName="space-y-2"
>
{/* Your content here */}
</Section>
| Prop | Type | Description |
|---|---|---|
title | string | Section heading text |
hint | string | Inline secondary text next to title (e.g., "(required)") |
description | string | Description text below the title |
collapsible | boolean | Enable collapse/expand toggle (false by default) |
defaultExpanded | boolean | Initial expanded state when uncontrolled (true by default) |
expanded | boolean | Controlled expanded state (overrides internal state) |
onExpandedChange | function | Callback when expand/collapse toggles |
disabled | boolean | Reduces opacity and disables pointer events |
className | string | Additional classes on the <section> element |
contentClassName | string | Additional classes on the content wrapper (e.g., space-y-2) |
InputField Component
InputField is the standard form input for all pages. It wraps Tier 1 primitives with a label, error text, and layout control.
import InputField from '@components/common/ui/InputField';
{/* Stacked layout (default) */}
<InputField
label="Dam Height (ft)"
inputType="number"
decimals={1}
align="left"
value={height}
onValueChange={setHeight}
/>
{/* Auto-detects number from decimals prop */}
<InputField label="Crest Width (ft)" decimals={1} value={width} onValueChange={setWidth} />
{/* Select — auto-detects from options prop */}
<InputField label="Erodibility" options={erodibilityOptions} value={val} onChange={setVal} />
{/* Inline layout */}
<InputField label="Override" layout="inline" inputType="number" value={v} onValueChange={setV} />
Key props: label, layout (stacked/inline), inputType (number/text/select/textarea), inputVariant, required, error, helpText. All remaining props are forwarded to the underlying input primitive.
Location: pages/
One per route. Thin orchestration layers that fetch data and compose Tier 2 components. Should be mostly JSX with minimal logic.
Feature-specific components are one-off extractions that only exist on a single page but are complex enough to warrant their own file. They live alongside their page component (e.g., ScreeningQuestion lives in the failure-mode-selection page directory, not in components/common/).
Extraction Rule of Thumb
If a component's JSX exceeds ~80–100 lines, or if you're using {/* Section */} comments to demarcate regions, those regions should be extracted to their own components.
Visual Constants Pattern
Every component with visual styling declares named constants at the top of the file:
// ─── Visual constants ────────────────────────────────────────────
const ACCENT_BAR = 'w-16 h-1 rounded-sm bg-dst-steel mb-2';
const HEADING = 'text-xl font-bold text-slate-900';
const HINT = 'text-base font-normal text-dst-text-tertiary';
const LABEL = 'text-sm font-medium text-slate-700';
Rules:
- Name constants by purpose, not color:
TAB_ACTIVE, notBLUE_BG_WHITE_TEXT - Use Tailwind built-in names for neutrals:
text-slate-400,bg-white - Use
dst-tokens for brand colors:bg-dst-steel,text-dst-steel-dark - If multiple components in a feature share styling, extract to
shared/styles.js
For naming convention details, see Case Conventions by Language.