Skip to content

Component Patterns

Conventions for building consistent UI components.

Layout Components

PageShell

Standard page wrapper with consistent layout.

<PageShell maxWidth="md" scrollable>
  <PageHeader title={t("markers")} />
  <SectionCard>
    <MarkerList markers={markers} />
  </SectionCard>
</PageShell>

Props: - maxWidth — Container max width (xs, sm, md, lg, xl) - scrollable — Enable vertical scrolling

SectionCard

Groups related content with visual separation.

<SectionCard title={t("accountSettings")}>
  <ProfileForm />
</SectionCard>

<SectionCard>
  <DangerZone />
</SectionCard>

EmptyState

Placeholder when no content exists.

<EmptyState
  icon={<MarkerIcon />}
  title={t("noMarkers")}
  description={t("noMarkersDescription")}
  action={
    <Button onClick={openCreate}>
      {t("createFirst")}
    </Button>
  }
/>

Form Components

Specialized Fields

Component Location Purpose
ColorSelector forms/selectors/ Color selection from palette
IconSelector forms/selectors/ Icon selection grid
DatePickerField forms/fields/ Date selection
MonthYearSelector forms/selectors/ Month/year navigation
SelectField forms/fields/ Dropdown select
SearchableSelectField forms/fields/ Searchable autocomplete
SearchField forms/fields/ Search input with clear
AccountSelectField forms/fields/ Budget account select
CategorySelectField forms/fields/ Category select
PasswordField forms/fields/ Password input with toggle

Naming convention: Form field wrappers use *Field suffix; selectors use *Selector suffix.

ColorSelector

<ColorSelector
  value={color}
  onChange={setColor}
  colors={CATEGORY_COLORS}
/>

IconSelector

<IconSelector
  value={icon}
  onChange={setIcon}
  icons={ICONS}
/>

Button Patterns

Use custom Button component, not raw MUI:

import Button from "@/components/common/Button";

<Button variant="contained" color="primary">
  {t("save")}
</Button>

<Button variant="outlined" color="error" loading={isDeleting}>
  {t("delete")}
</Button>

Features: - Loading state with spinner - Disabled during loading - Consistent styling

List Patterns

Sortable Images

SortableImageGrid for drag-to-reorder image galleries with @dnd-kit/sortable.

Infinite Scroll

Use useInfiniteScroll hook with TanStack Query's useInfiniteQuery for paginated lists.

Error Boundary

Catches rendering errors with fallback UI.

<ErrorBoundary
  fallback={<ErrorFallback onRetry={retry} />}
  onError={(error) => captureException(error)}
>
  <FeatureComponent />
</ErrorBoundary>

Development mode shows error details; production shows user-friendly message.

Loading States

Skeleton

{isLoading ? (
  <MarkerCardSkeleton count={3} />
) : (
  <MarkerList markers={markers} />
)}

Suspense

<Suspense fallback={<PageSkeleton />}>
  <LazyMarkerPage />
</Suspense>

Component Organization

components/
├── common/              # Commonly used (Button, InfoTooltip)
├── display/             # Visual display (AmountDisplay, TruncatedText, UserAvatar)
├── feedback/            # User feedback (EmptyState, ErrorBoundary, ListItemSkeleton)
├── forms/
│   ├── fields/          # Field wrappers (*Field components)
│   ├── selectors/       # Selection UI (ColorSelector, IconSelector)
│   ├── security/        # Form security (Captcha, HoneypotField)
│   └── accessibility/   # Accessibility (FormErrorAnnouncer)
├── tables/              # Tabular data (PaginatedList, PaginatedTable)
├── images/              # Image handling (ImageCard, SortableImageCard)
├── dialogs/             # Modal dialogs (ConfirmDeleteDialog, UnsavedChangesDialog)
├── layout/              # Page layout (PageShell, SectionCard)
├── navigation/          # Navigation (HubButton)
├── pwa/                 # PWA components (PwaUpdateDialog, PwaUpdateIndicator)
└── routing/             # Route guards (ProtectedRoute, GuestRoute)

Domain-specific components live in their page folders (e.g., pages/budget/components/).