---
phase: 13-design-system-foundation
plan: "04"
subsystem: backend/internal/web/ui
tags: [modal, empty-state, table, icon-button, space, uiicon, tailwind, design-system, tdd]
dependency_graph:
  requires: [13-01, 13-02, 13-03]
  provides: [modal-component, empty-state-component, table-component, icon-button-component, uiicon-function, space-components, full-css-manifest]
  affects:
    - backend/internal/web/ui/modal.css
    - backend/internal/web/ui/modal.templ
    - backend/internal/web/ui/empty-state.css
    - backend/internal/web/ui/empty_state.templ
    - backend/internal/web/ui/table.css
    - backend/internal/web/ui/table.templ
    - backend/internal/web/ui/icon-button.css
    - backend/internal/web/ui/icon_button.templ
    - backend/internal/web/ui/spacing.css
    - backend/internal/web/ui/space.templ
    - backend/internal/web/ui/button.templ
    - backend/internal/web/ui/ui_test.go
    - backend/tailwind.input.css
tech_stack:
  added: []
  patterns: [verbatim-port, tdd-red-green, nil-guard-optional-regions, inline-svg-icon-switch]
key_files:
  created:
    - backend/internal/web/ui/modal.css
    - backend/internal/web/ui/modal.templ
    - backend/internal/web/ui/empty-state.css
    - backend/internal/web/ui/empty_state.templ
    - backend/internal/web/ui/table.css
    - backend/internal/web/ui/table.templ
    - backend/internal/web/ui/icon-button.css
    - backend/internal/web/ui/icon_button.templ
    - backend/internal/web/ui/spacing.css
    - backend/internal/web/ui/space.templ
  modified:
    - backend/internal/web/ui/button.templ
    - backend/internal/web/ui/ui_test.go
    - backend/tailwind.input.css
decisions:
  - "UIIcon defined in icon_button.templ (same file as IconButton) — matches go-backend pattern; UIIcon is callable from button.templ because both are in package ui"
  - "SpaceX/SpaceY use exported SpaceXClass/SpaceYClass (not lowercase like go-backend) — backend variants.go already exported these functions in Plan 01"
  - "tailwind.input.css final import order: base, auth, button, badge, card, input, textarea, select, modal, empty-state, table, icon-button, form-field, spacing (14 total per D-A02: no app.css)"
metrics:
  duration: "~10 minutes"
  completed_date: "2026-05-16"
  tasks: 2
  files: 13
---

# Phase 13 Plan 04: Remaining Components — Modal, EmptyState, Table, IconButton, Space Summary

Five remaining component types ported from go-backend with CSS, typed Props structs, and test coverage: Modal (ui-modal-backdrop/panel/header/body/actions), EmptyState (dashed border, nil-guarded Icon/Action), Table (ui-table-shell wrapper), IconButton + UIIcon switch (8 icons + fallback), SpaceX/SpaceY. UIIcon wired into button.templ. tailwind.input.css updated to 14 total imports.

## Tasks Completed

| Task | Name | Commit | Key Files |
|------|------|--------|-----------|
| 1 (RED) | Failing tests for Modal, EmptyState, Table | 4bdb78d | ui_test.go (+6 tests) |
| 1 (GREEN) | Port modal, empty-state, table components with CSS and templ | fbdf188 | modal.css, modal.templ, empty-state.css, empty_state.templ, table.css, table.templ |
| 2 (RED) | Failing tests for IconButton, UIIcon, Space, Button icon wiring | fa24059 | ui_test.go (+7 tests) |
| 2 (GREEN) | Port icon-button/space; wire UIIcon into button.templ; update tailwind.input.css | c80ebcb | icon-button.css, icon_button.templ, spacing.css, space.templ, button.templ, tailwind.input.css |

## What Was Built

**Task 1 — Modal, EmptyState, Table (TDD):**
- `modal.css`: `.ui-modal-backdrop` (position fixed, flex center), `.ui-modal-panel` (max-width 32rem, border-radius 1rem, box-shadow), `.ui-modal-header`/`.ui-modal-body`/`.ui-modal-actions` (padding, border-top/bottom dividers).
- `modal.templ`: `ModalProps` struct with `Title string`, `Body templ.Component`, `Actions templ.Component`. Nil-guard conditionals for Body and Actions regions.
- `empty-state.css`: `.ui-empty-state` (dashed border, flex column center, padding 3rem 1.5rem), `.ui-empty-state-icon` (4rem circle, surface-muted background), `.ui-empty-state-title` (font-weight 700), `.ui-empty-state-description`.
- `empty_state.templ`: `EmptyStateProps` struct with `Icon templ.Component` (not string — callers pass UIIcon(...)). Nil-guard for Icon, empty-string guard for Description, nil-guard for Action.
- `table.css`: `.ui-table-shell` (overflow-x auto, width 100%), `.ui-table` (border-collapse collapse, min-width 100%).
- `table.templ`: `TableProps` struct with `Head templ.Component`, `Body templ.Component`. Nil-guarded thead/tbody regions.
- 6 tests: `TestModal_RendersBackdropAndPanel`, `TestModal_RendersTitle`, `TestModal_NilBodyOmitted`, `TestEmptyState_RendersTitle`, `TestEmptyState_NilIconOmitted`, `TestTable_RendersShell`.

**Task 2 — IconButton, UIIcon, Space + button.templ wiring (TDD):**
- `icon-button.css`: `.ui-icon-button` (min 44x44px, transparent background, border-radius 0.5rem), `.borderless-icon-button` (no border/shadow/outline), `.ui-icon-button-solid.ui-icon-button-neutral` and ghost color rules, `.borderless-icon-button svg` (1rem size).
- `icon_button.templ`: `IconButtonProps` struct with `Label`, `Icon`, `Variant`, `Tone`, `Type`, `Attrs`. `IconButton` renders `<button>@UIIcon</button>` with `IconButtonClass`. `UIIcon` switch with 8 cases (plus, grid3x3, list, filter, search, calendar, pencil, trash) as inline SVGs + default `<span>` fallback.
- `spacing.css`: `.ui-space-x`/`.ui-space-y` base display rules + `.ui-space-x-{xs/sm/md/lg/xl}` width steps + `.ui-space-y-{xs/sm/md/lg/xl}` height steps.
- `space.templ`: `SpaceProps` struct with `Size SpacingStep`. `SpaceX` renders `<span>`, `SpaceY` renders `<div>` using exported `SpaceXClass`/`SpaceYClass`.
- `button.templ`: replaced Plan 02 placeholder comment with actual `if props.Icon != "" { <span class="ui-button-icon">@UIIcon(props.Icon)</span> }`.
- `tailwind.input.css`: added 5 new imports (modal, empty-state, table, icon-button, form-field position unchanged, spacing) — total 14 `@import` lines for `web/ui` files.
- 7 tests: `TestIconButton_GhostNeutral`, `TestIconButton_SolidDanger`, `TestUIIcon_Plus`, `TestUIIcon_Fallback`, `TestSpaceX_MD`, `TestSpaceY_LG`, `TestButton_IconRendered`.

## Verification Results

- `templ generate`: succeeds (3 new components generated)
- `go test ./internal/web/ui/... -count=1`: all 36 tests pass (29 from Plans 01-03 + 13 new)
- `go test ./... -count=1`: all packages pass (auth, db, files, jobs, web, web/ui, templates)
- `grep -c '@import.*web/ui' backend/tailwind.input.css`: returns 14 (base + auth + 12 components)
- `grep 'templ UIIcon' backend/internal/web/ui/icon_button.templ`: matches
- `grep '@UIIcon' backend/internal/web/ui/button.templ`: matches (icon wired)
- `grep 'ui-table-shell' backend/internal/web/ui/table.css`: matches
- `grep 'ui-empty-state' backend/internal/web/ui/empty-state.css`: matches (5 lines)

## Deviations from Plan

None — plan executed exactly as written.

The go-backend's `space.templ` uses private `spaceXClass`/`spaceYClass` functions; the backend uses exported `SpaceXClass`/`SpaceYClass` (set up in Plan 01). This is not a deviation — Plan 01 deliberately exported these functions and the plan's `<interfaces>` section specifies the exported names `SpaceXClass`/`SpaceYClass`.

## Known Stubs

None — all components are fully implemented with correct CSS selectors and templ rendering logic. UIIcon renders actual inline SVGs (not placeholders) for all 8 icon kinds. No data wiring required for these presentational components.

## Threat Flags

No new network endpoints, auth paths, file access patterns, or schema changes introduced.

The threat model entries T-13-04-01 through T-13-04-03 are all satisfied:
- T-13-04-01: UIIcon default fallback renders `{ kind }` which templ auto-escapes — no XSS via icon name string
- T-13-04-02: icon-button.css is a public static asset with no sensitive information
- T-13-04-03: tailwind.input.css update controls CSS only — no route registration or handler changes

## TDD Gate Compliance

- Task 1 RED gate: commit `4bdb78d` — 6 failing tests (undefined: Modal, ModalProps, EmptyState, EmptyStateProps, Table, TableProps)
- Task 1 GREEN gate: commit `fbdf188` — all 6 TestModal/TestEmptyState/TestTable tests pass; full suite green
- Task 2 RED gate: commit `fa24059` — 7 failing tests (undefined: IconButton, IconButtonProps, UIIcon, SpaceX, SpaceProps, SpaceY)
- Task 2 GREEN gate: commit `c80ebcb` — all 7 new tests pass; full suite green
- REFACTOR gate: Not needed — implementation was clean on first pass

## Self-Check: PASSED
