UI components refactor — use the shadcn/ui primitives consistently¶
Goal¶
Replace hand-styled HTML (<input>, <select>, <label>, <ul><li>,
ad-hoc back-links) in the four non-viewer routes with the existing
app/components/ui/* primitives, so the app has one consistent look
instead of duplicated Tailwind per route.
Scope¶
In:
- app/routes/create.tsx —
Field/FieldLabel/Input/Textarea/FieldError - app/routes/add-map.tsx —
Field/FieldLabel/Input/Select/FieldError,Breadcrumb - app/routes/project.tsx —
Breadcrumb,Card,ItemGroup/Item - app/routes/home.tsx —
Card,ItemGroup/Item - Component defaults are adopted as-is (borders/padding/rounding from
CardandItemare an intentional visual change from today's plain links/divs)
Out:
- app/routes/project-map.tsx — deferred to the future toolbar/layers panel work
Badge,Drawer,DropdownMenu,Checkbox,RadioGroup,Progress,Questionnaire,Attachment,ScrollArea,HoverCard,ButtonGroup— no current use case- Any change to loaders/actions, validation rules, or the
MapRef/MapSourcedata model — this is a markup/component swap only create.tsx/add-map.tsxdo not get aCardwrapper (not part of the approved scope for the forms)
Why this shape¶
FieldErrorships in the samefield.tsxmodule asField/FieldLabeland exists specifically to render theactionData?.errorstring both forms already have — using it is in scope even though it wasn't named explicitly, because it's the same primitive file.Select(app/components/ui/select.tsx) wraps Base UI'sSelectRoot, which accepts anameprop and renders a hidden native input for form submission (confirmed innode_modules/@base-ui/react/select/root/SelectRoot.d.ts), so<Select name="parentMap">still lands inrequest.formData()exactly like today's native<select name="parentMap">— no action changes needed.SelectRootalso supportsdefaultValue={null}andSelectValuesupports aplaceholderprop, so "no parent map" is modeled as no-selection-with-a-placeholder rather than aSelectItem value=""sentinel.ItemandBreadcrumb's link parts (Item,BreadcrumbLink) accept arenderprop (useRender), the same pattern already used for<Button render={<Link .../>}>in every route today — reusing it keeps one convention for "this primitive is actually a router link."- Ordered form-routes-then-page-routes below only for review clarity; all four tasks are independent and may land in a single PR per the earlier scoping decision not to split by route.
Tasks¶
T1 — create.tsx: Field/Input/Textarea¶
Goal: Replace the manually-styled <label>/<input>/<textarea> pair
with Field/FieldLabel/Input (name) and Field/FieldLabel/Textarea
(description); replace the raw error <p> with FieldError.
Acceptance criteria:
- Name field renders as
Field>FieldLabel htmlFor="name"+Input id="name" name="name" required - Description field renders as
Field>FieldLabel htmlFor="description" Textarea id="description" name="description"actionData?.errorrenders viaFieldErrorinstead of a hand-styled<p className="text-sm text-destructive">- Form still submits
name/descriptionfields under the same field names;actionin this file is untouched - No leftover
rounded-md border border-border bg-background px-3 py-2 text-smclasses on raw inputs in this file pnpm typecheckandpnpm buildpass
T2 — add-map.tsx: Field/Input/Select/Breadcrumb¶
Goal: Replace the manually-styled form fields with Field primitives,
swap the native <select> for Select, and replace the "← project"
back-link with a Breadcrumb trail.
Acceptance criteria:
- Name field:
Field>FieldLabel+Input id="name" name="name" required - Image field:
Field>FieldLabel+Input id="image" name="image" type="file" required accept="image/png,image/jpeg,image/webp" - Parent map field:
Field>FieldLabel+Select name="parentMap" defaultValue={null}containingSelectTrigger>SelectValue placeholder="None", and aSelectContentwith oneSelectItem value={map.id}perproject.mapsentry (no explicit "None" item — the placeholder covers the unselected state) actionData?.errorrenders viaFieldError- The
← {project.name}link becomes aBreadcrumb>BreadcrumbListwith aBreadcrumbItem>BreadcrumbLink render={<Link to={\/project/${project.id}`} />}for the project name, aBreadcrumbSeparator, and a trailingBreadcrumbItem>BreadcrumbPage` reading "Add map" - Loader and action in this file are untouched; submitted field names are
unchanged (
name,image,parentMap) - Manual check: the file input still opens a native file picker and shows
the chosen filename (the
Inputprimitive'sfile:*classes are designed for this, perapp/components/ui/input.tsx) pnpm typecheckandpnpm buildpass
T3 — project.tsx: Breadcrumb, Card, ItemGroup/Item¶
Goal: Replace the "← Back to projects" link with a Breadcrumb trail,
wrap the page body in a Card, and render the map list with
ItemGroup/Item instead of <ul><li>.
Acceptance criteria:
- Breadcrumb trail:
BreadcrumbItem>BreadcrumbLink render={<Link to="/" />}reading "Map Projects",BreadcrumbSeparator, trailingBreadcrumbItem>BreadcrumbPagereadingproject.name - Page body wrapped in a single
Card:CardHeaderholdsCardTitle(project.name),CardDescription(project.description, only when present), andCardActionholding the existing "Add map"Button;CardContentholds the "Maps" section - Map list renders as
ItemGroupofItementries (render={<Link to={\/project/\({project.id}/map/\)}`} />, each containingItemContent>ItemTitlewithmap.name); the empty state ("No maps yet.") stays a plain muted paragraph insideCardContent, not wrapped inItemGroup` - Loader in this file is untouched
- No leftover
<ul>/<li>or manually underlined<Link>in this file pnpm typecheckandpnpm buildpass
T4 — home.tsx: Card, ItemGroup/Item¶
Goal: Wrap the page body in a Card and render the project list with
ItemGroup/Item instead of <ul><li>.
Acceptance criteria:
- Page body wrapped in a single
Card:CardHeaderholdsCardTitle("Map Projects") andCardActionholding the existing "New project"Button;CardContentholds the project list - Project list renders as
ItemGroupofItementries (render={<Link to={\/project/${project.id}`} />}, each containingItemContent>ItemTitlewithproject.name); the empty state ("No projects yet...") stays a plain muted paragraph insideCardContent, not wrapped inItemGroup` - Loader in this file is untouched
- No leftover
<ul>/<li>or manually underlined<Link>in this file pnpm typecheckandpnpm buildpass
Verification (all tasks)¶
Manual check after all four tasks: click through Home → Create → Project → Add map → Project, confirm every link/button still navigates the same as before, and the add-map form still successfully creates a map with an uploaded image.