Problem
The SPA has grown to many routes (home, summary, CRUD lists/detail/edit, import/export, reference tools, settings, map) but page chrome and primitives are inconsistent. Each route composes Mantine directly with ad hoc layout choices:
| Area |
Today |
Pain |
| Page shell |
Container sizes vary (sm on Home, lg on most CRUD/report pages); some routes wrap content in ReportPage, others inline their own Container/Stack/Title |
|
| Section cards |
Import/export (/#/import-export) uses Paper withBorder panels in a SimpleGrid — the best-looking pattern today; most other pages are flat stacks with no visual grouping |
|
| Data tables |
EntityTable exists but is minimal (no shared empty state, toolbar, mobile column policy, or list-page header/actions); channel list adds optional columns locally; #68 tracks mobile collapse separately |
|
| Page headers |
Title + dimmed description repeated by hand; hierarchy (order={1} vs order={2}) not uniform |
|
| Navigation |
AppShell + dual AppNav / SectionNav works but header, primary nav, and section nav styles are not extracted as reusable primitives |
|
| Forms & actions |
Edit pages each lay out Stack/Group/buttons differently; no shared sticky action bar or form section pattern |
|
| Theme |
src/theme.ts sets brand colours only — no documented spacing, radius, or component defaults beyond Mantine defaults |
|
Operators get a functional but uneven UI. Future pages (#92, #103, …) will copy whichever local pattern is nearest, compounding drift.
Intended outcome
Design, build, demo, and roll out a small internal UI kit under src/components/ui/ (name TBD) so every standard HTML/Mantine surface shares one look and feel. Deliver in one PR with atomic conventional commits per layer.
1. Design pass (look + feel)
- Codify layout tokens on top of existing Mantine theme (
theme.ts): page max-widths, section gap, card padding/radius, heading scale, dimmed helper text style.
- Reference pattern: import/export screen (
src/routes/ImportExport.tsx) — Container size="lg", page title + description, Paper withBorder section cards, SimpleGrid where side-by-side panels help.
- Document decisions briefly in
docs/features/ui/README.md (tier-1 feature doc — internal SPA conventions, not vendor wire).
2. Shared components (initial inventory)
Build thin wrappers — props-forwarding to Mantine where sensible, opinionated defaults where not:
| Component |
Role |
Page / PageLayout |
Standard outer shell: width, vertical padding, optional max-width variant (narrow / default / wide) |
PageHeader |
Title order={1} + optional description + optional actions slot (right-aligned on desktop) |
PageSection |
Bordered card (Paper) with optional title, description, and body — mirrors import/export panels |
PageSectionGrid |
Responsive SimpleGrid for 1–2 column section layouts |
DataTable |
Evolve or wrap EntityTable: consistent borders/striping, empty state copy, optional caption; hook point for #68 mobile column collapse later |
ListPage |
Compose Page + PageHeader + toolbar slot + DataTable — standard list-route layout |
FormPage |
Edit-route shell: header, form body, sticky/fixed Save/Cancel footer on mobile (feeds #69) |
FormSection |
Grouped field block with subheading — accordion variant optional follow-up inside same PR if time |
EmptyState |
Icon + message + optional CTA for zero-row tables and empty projects |
AppHeader / NavLink primitives |
Extract repeated header + nav link styling from App.tsx / AppNav / SectionNav without changing behaviour |
In scope: all standard HTML/Mantine primitives we use today — buttons, inputs, selects, modals (ConfirmDeleteModal), pills (ModePill, BandPill), alerts, dividers, etc. Either wrap or document the canonical variant in the styleguide.
Out of scope for v1: redesigning map UI, new features, or changing business logic.
3. Styleguide page (hidden route)
- New route e.g.
/#/styleguide — no link in nav; reachable by URL only (for dev/design review).
- Renders every kit component in realistic combinations: page shell, sections, tables (with/without rows), form fields, buttons (variants), pills, modals, empty states, nav samples.
- Include light/dark if Mantine color scheme is toggled anywhere; otherwise default scheme only.
- Gate behind nothing (public SPA) — acceptable because it demos static UI only; add a one-line comment in route registry that it is intentionally unlinked.
4. Verify + adopt across the app
After components exist and pass review on the styleguide:
- Migrate all routes to the kit — priority order:
- Import/export (already closest — refactor into shared
PageSection without visual regression)
- List pages: channels, zones, contacts, talk groups, RX group lists
- Detail pages using
ReportPage / DetailSections
- Edit forms
- Home, Summary, Settings, reference routes
- Deprecate or thin
ReportPage → re-export Page/PageHeader if redundant.
- Keep
EntityTable as implementation detail behind DataTable or merge — one table abstraction only.
5. Tests
- Smoke test: styleguide route renders without throw.
- One test per critical primitive if behaviour is non-trivial (e.g.
ListPage renders header + children).
- Existing route tests (
App.test.tsx, list tests) must still pass after migration.
Current reference files
- Best page layout:
src/routes/ImportExport.tsx
- Page wrapper today:
src/components/report/ReportPage.tsx
- Tables:
src/components/report/EntityTable.tsx (+ BandPlanTable.tsx for reference data)
- Shell:
src/App.tsx, src/components/AppNav/AppNav.tsx, src/components/SectionNav/
- Theme:
src/theme.ts
Affected
src/components/ui/ (new)
src/routes/styleguide.tsx (new) + route in App.tsx
src/theme.ts — extended defaults if needed
- All routes under
src/routes/ — adopt shared layout
src/components/report/ReportPage.tsx, EntityTable.tsx — merge or delegate
docs/features/ui/README.md + index in docs/features/README.md
Related issues
Out of scope
- Brand redesign / custom illustration / marketing site.
- Replacing Mantine with another UI library.
- Map-specific controls (
CodeplugMap, leaflet chrome).
- Accessibility audit beyond Mantine defaults (follow-up welcome).
Manual verify
- Open
/#/styleguide — every component renders; visually matches import/export quality.
- Spot-check migrated routes: Home, Import/export, Channels list, Channel edit, Settings, Summary — consistent page width, headers, and section cards.
npm run lint && npm run test && npm run build
- Mobile viewport: list pages and form footers remain usable.
- No new nav link to styleguide; direct URL still works.
Workflow note
Single PR preferred. Branch from origin/main (e.g. {num}/paddy/ui-component-kit). Atomic conventional commits per slice — e.g. feat(ui): add Page and PageHeader, feat(ui): add styleguide route, refactor(routes): adopt Page on ImportExport, … — do not batch the whole migration into one commit at the end. Link PR with Closes #.
Problem
The SPA has grown to many routes (home, summary, CRUD lists/detail/edit, import/export, reference tools, settings, map) but page chrome and primitives are inconsistent. Each route composes Mantine directly with ad hoc layout choices:
Containersizes vary (smon Home,lgon most CRUD/report pages); some routes wrap content inReportPage, others inline their ownContainer/Stack/Title/#/import-export) usesPaper withBorderpanels in aSimpleGrid— the best-looking pattern today; most other pages are flat stacks with no visual groupingEntityTableexists but is minimal (no shared empty state, toolbar, mobile column policy, or list-page header/actions); channel list adds optional columns locally;#68tracks mobile collapse separatelyorder={1}vsorder={2}) not uniformAppShell+ dualAppNav/SectionNavworks but header, primary nav, and section nav styles are not extracted as reusable primitivesStack/Group/buttons differently; no shared sticky action bar or form section patternsrc/theme.tssets brand colours only — no documented spacing, radius, or component defaults beyond Mantine defaultsOperators get a functional but uneven UI. Future pages (#92, #103, …) will copy whichever local pattern is nearest, compounding drift.
Intended outcome
Design, build, demo, and roll out a small internal UI kit under
src/components/ui/(name TBD) so every standard HTML/Mantine surface shares one look and feel. Deliver in one PR with atomic conventional commits per layer.1. Design pass (look + feel)
theme.ts): page max-widths, section gap, card padding/radius, heading scale, dimmed helper text style.src/routes/ImportExport.tsx) —Container size="lg", page title + description,Paper withBordersection cards,SimpleGridwhere side-by-side panels help.docs/features/ui/README.md(tier-1 feature doc — internal SPA conventions, not vendor wire).2. Shared components (initial inventory)
Build thin wrappers — props-forwarding to Mantine where sensible, opinionated defaults where not:
Page/PageLayoutnarrow/default/wide)PageHeaderTitle order={1}+ optional description + optional actions slot (right-aligned on desktop)PageSectionPaper) with optional title, description, and body — mirrors import/export panelsPageSectionGridSimpleGridfor 1–2 column section layoutsDataTableEntityTable: consistent borders/striping, empty state copy, optional caption; hook point for#68mobile column collapse laterListPagePage+PageHeader+ toolbar slot +DataTable— standard list-route layoutFormPage#69)FormSectionEmptyStateAppHeader/NavLinkprimitivesApp.tsx/AppNav/SectionNavwithout changing behaviourIn scope: all standard HTML/Mantine primitives we use today — buttons, inputs, selects, modals (
ConfirmDeleteModal), pills (ModePill,BandPill), alerts, dividers, etc. Either wrap or document the canonical variant in the styleguide.Out of scope for v1: redesigning map UI, new features, or changing business logic.
3. Styleguide page (hidden route)
/#/styleguide— no link in nav; reachable by URL only (for dev/design review).4. Verify + adopt across the app
After components exist and pass review on the styleguide:
PageSectionwithout visual regression)ReportPage/DetailSectionsReportPage→ re-exportPage/PageHeaderif redundant.EntityTableas implementation detail behindDataTableor merge — one table abstraction only.5. Tests
ListPagerenders header + children).App.test.tsx, list tests) must still pass after migration.Current reference files
src/routes/ImportExport.tsxsrc/components/report/ReportPage.tsxsrc/components/report/EntityTable.tsx(+BandPlanTable.tsxfor reference data)src/App.tsx,src/components/AppNav/AppNav.tsx,src/components/SectionNav/src/theme.tsAffected
src/components/ui/(new)src/routes/styleguide.tsx(new) + route inApp.tsxsrc/theme.ts— extended defaults if neededsrc/routes/— adopt shared layoutsrc/components/report/ReportPage.tsx,EntityTable.tsx— merge or delegatedocs/features/ui/README.md+ index indocs/features/README.mdRelated issues
DataTableshould not block this — leave extension points or implement collapse as part of adoption if in same PR.FormPage/FormSectionshould align with accordion work there (coordinate so two PRs don't fight).Out of scope
CodeplugMap, leaflet chrome).Manual verify
/#/styleguide— every component renders; visually matches import/export quality.npm run lint && npm run test && npm run buildWorkflow note
Single PR preferred. Branch from
origin/main(e.g.{num}/paddy/ui-component-kit). Atomic conventional commits per slice — e.g.feat(ui): add Page and PageHeader,feat(ui): add styleguide route,refactor(routes): adopt Page on ImportExport, … — do not batch the whole migration into one commit at the end. Link PR withCloses #.