Frontend Component Architecture
RoundTrip Web — Canonical Directory Structure
Version: 1.0 | Date: March 2026 | Status: Approved
This document is the authoritative reference for where every type of file lives in the RoundTrip React frontend. All new feature work must follow this structure.
The Rule System
Three rules govern where any file lives:
Rule 1 — Feature scoping: If a component, hook, or type is only used within one feature, it lives inside that feature's folder. If it crosses feature boundaries, it lives in a shared location.
Rule 2 — Hook proximity: Hooks live in a hooks/ subfolder within their feature (src/features/tickets/hooks/useTicketList.ts). Hooks shared across multiple features live in src/hooks/.
Rule 3 — Component layers: Dependencies only flow downward — features import from components/common/, never the reverse. components/common/ imports from components/ui/ (shadcn/ui primitives), never from features.
Directory Structure
src/
├── app/ # Router, providers, App.tsx, global error boundary
│
├── components/
│ ├── ui/ # shadcn/ui copy-paste primitives
│ │ └── (Button, Dialog, etc.) # Customised with RoundTrip Command CSS variables
│ └── common/ # Shared app-specific components
│ ├── RoundTripLogo.tsx # The real SVG logo mark, dark/light variants
│ ├── CountBadge.tsx # Numeric pill count
│ ├── StatusBadge.tsx # Ticket status semantic color pill
│ ├── PriorityBadge.tsx # Ticket priority semantic color pill
│ ├── SectionHeader.tsx # Panel header row — icon, title, CountBadge, action slot
│ ├── SurfacePanel.tsx # White card wrapper with correct radius, border, shadow
│ ├── PageHeader.tsx # Barlow Condensed page title + subtitle + action slot
│ ├── FieldLabel.tsx # Form field label with icon mark and required indicator
│ └── MetaRow.tsx # Icon tile + label + value row for detail sidebars
│
├── features/
│ ├── dispatch/
│ │ ├── hooks/
│ │ │ └── useDispatchBoard.ts # TanStack Query — dispatch board data
│ │ └── DispatchBoardPage.tsx # Two-column dispatch board
│ │
│ ├── tickets/
│ │ ├── hooks/
│ │ │ ├── useTicketList.ts # TanStack Query — paginated ticket list
│ │ │ ├── useTicketDetail.ts # TanStack Query — single ticket detail
│ │ │ └── useCreateTicket.ts # Mutation + client search + technician queries
│ │ ├── TicketListPage.tsx # Paginated list with filters
│ │ ├── TicketDetailPage.tsx # Full detail with notes, parts, attachments
│ │ └── CreateTicketModal.tsx # Slide-over create form
│ │
│ ├── clients/ # M8 — client list, detail, create/edit
│ │ └── hooks/
│ │
│ ├── inventory/ # M8 — inventory list, stock adjustment
│ │ └── hooks/
│ │
│ ├── billing/ # M8 — invoice list, detail, generate, send
│ │ └── hooks/
│ │
│ └── technician/ # M9 — PWA views, today's jobs, ticket updates
│ └── hooks/
│
├── hooks/ # Shared hooks (cross-feature)
│ └── useDebounce.ts # Delays value updates — used in search inputs
│
├── layouts/
│ ├── DispatchLayout.tsx # Dark sidebar + command bar + Outlet
│ └── TechnicianLayout.tsx # M9 — mobile layout with bottom tab nav
│
├── lib/
│ └── api/
│ ├── axiosInstance.ts # Single Axios instance with Bearer token interceptor
│ ├── dispatchApi.ts # GET /v1/dispatch/board
│ ├── ticketApi.ts # GET /v1/tickets (list)
│ ├── ticketDetailApi.ts # GET /v1/tickets/:id (detail)
│ └── createTicketApi.ts # POST /v1/tickets + client/tech lookups
│
├── stores/
│ ├── authStore.ts # Zustand — auth state, JWT, role, display name
│ ├── uiStore.ts # Zustand — sidebar collapsed, theme
│ └── dispatchStore.ts # Zustand — SignalR status, optimistic map updates
│
└── index.css # RoundTrip Command design tokens
Shared Component Reference
CountBadge
Numeric pill. Used in SectionHeader and nav items.
<CountBadge count={7} />
<CountBadge count={0} showZero={false} />
StatusBadge
Semantic color pill for ticket status. Correct colors: InProgress=Amber, Assigned=Blue, Completed=Green, Cancelled=Steel, OnHold=Amber, Open=Steel.
<StatusBadge status="InProgress" />
<StatusBadge status={ticket.status} size="md" />
PriorityBadge
Semantic color pill for ticket priority. Also exports priorityBorderColor(priority) for left-border accent on table rows.
<PriorityBadge priority="Urgent" />
// Table row left-border:
borderLeft: `3px solid ${priorityBorderColor(ticket.priority)}`
SectionHeader
Panel header row with icon, uppercase title, optional count badge, and optional right-aligned action slot.
<SectionHeader icon={Ticket} title="Active Tickets" count={7} />
<SectionHeader icon={Note} title="Notes" count={3} action={<AddNoteButton />} />
SurfacePanel
Standard white card wrapper — correct border, radius (--radius-lg), shadow.
<SurfacePanel>
<SectionHeader ... />
<div>content</div>
</SurfacePanel>
PageHeader
Barlow Condensed page title + Inter subtitle. Optional right-aligned action slot.
<PageHeader title="Service Tickets" subtitle="42 tickets" action={<NewTicketButton />} />
FieldLabel
Form field label — icon mark + uppercase text + optional required asterisk. Accepts htmlFor for accessibility.
<FieldLabel icon={User} label="Client" required htmlFor="client-search" />
MetaRow
Sidebar metadata row — icon tile + label + value. Renders "Not set" for null values.
<MetaRow icon={User} label="Technician" value={ticket.technicianName} />
<MetaRow icon={CalendarBlank} label="Created" value={formatDate(ticket.createdAt)} mono />
Hook Decision Tree
When creating a new hook, ask:
- Is it a server state hook (TanStack Query)? → Lives in
src/features/<feature>/hooks/ - Is it a mutation hook? → Lives in
src/features/<feature>/hooks/ - Is it used by more than one feature? → Lives in
src/hooks/ - Is it a utility hook with no API dependency? → Lives in
src/hooks/
Current shared hooks:
useDebounce— delays value updates, used in any search input across all features
API Module Convention
Every API module in src/lib/api/ follows this pattern:
- Named export object:
export const ticketApi = { ... } - All types (request/response) co-located in the same file
- No direct
fetchcalls — always throughapiClient(the Axios instance) - Types are manually maintained until
openapi-typescriptgeneration is wired up (M10)
shadcn/ui Usage
shadcn/ui primitives live in src/components/ui/. The copy-paste components are modified to use RoundTrip Command CSS variables rather than Tailwind color classes. When adding a new shadcn/ui component:
- Copy the component from shadcn/ui
- Replace any hardcoded colors with the appropriate CSS variable from
index.css - Ensure it uses
var(--font-ui)orvar(--font-body)rather than the default font stack
Current shadcn/ui components in use: (to be documented as they are added)
What NOT to do
- ❌ Never import from a feature inside
components/common/orcomponents/ui/ - ❌ Never put a hook directly at the feature root — always in
hooks/subfolder - ❌ Never inline a shared component (badge, header, label) inside a feature file — extract it
- ❌ Never use hardcoded hex colors in component files — always CSS variables
- ❌ Never create a
pages/directory — features live infeatures/, notpages/ - ❌ Never create a new API module for data used by only one hook — co-locate types in the API module file