Skip to content

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:

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/MapSource data model — this is a markup/component swap only
  • create.tsx/add-map.tsx do not get a Card wrapper (not part of the approved scope for the forms)

Why this shape

  • FieldError ships in the same field.tsx module as Field/FieldLabel and exists specifically to render the actionData?.error string 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's SelectRoot, which accepts a name prop and renders a hidden native input for form submission (confirmed in node_modules/@base-ui/react/select/root/SelectRoot.d.ts), so <Select name="parentMap"> still lands in request.formData() exactly like today's native <select name="parentMap"> — no action changes needed.
  • SelectRoot also supports defaultValue={null} and SelectValue supports a placeholder prop, so "no parent map" is modeled as no-selection-with-a-placeholder rather than a SelectItem value="" sentinel.
  • Item and Breadcrumb's link parts (Item, BreadcrumbLink) accept a render prop (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?.error renders via FieldError instead of a hand-styled <p className="text-sm text-destructive">
  • Form still submits name/description fields under the same field names; action in this file is untouched
  • No leftover rounded-md border border-border bg-background px-3 py-2 text-sm classes on raw inputs in this file
  • pnpm typecheck and pnpm build pass

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} containing SelectTrigger > SelectValue placeholder="None", and a SelectContent with one SelectItem value={map.id} per project.maps entry (no explicit "None" item — the placeholder covers the unselected state)
  • actionData?.error renders via FieldError
  • The ← {project.name} link becomes a Breadcrumb > BreadcrumbList with a BreadcrumbItem > 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 Input primitive's file:* classes are designed for this, per app/components/ui/input.tsx)
  • pnpm typecheck and pnpm build pass

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, trailing BreadcrumbItem > BreadcrumbPage reading project.name
  • Page body wrapped in a single Card: CardHeader holds CardTitle (project.name), CardDescription (project.description, only when present), and CardAction holding the existing "Add map" Button; CardContent holds the "Maps" section
  • Map list renders as ItemGroup of Item entries (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 typecheck and pnpm build pass

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: CardHeader holds CardTitle ("Map Projects") and CardAction holding the existing "New project" Button; CardContent holds the project list
  • Project list renders as ItemGroup of Item entries (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 typecheck and pnpm build pass

Verification (all tasks)

pnpm typecheck
pnpm format
pnpm build

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.