---
phase: 13-design-system-foundation
plan: 04
type: execute
wave: 4
depends_on:
  - 13-01
  - 13-02
  - 13-03
files_modified:
  - 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
autonomous: true
requirements:
  - DS-06
  - DS-07
  - DS-08
  - DS-09

must_haves:
  truths:
    - "Modal component renders ui-modal-backdrop and ui-modal-panel with typed Title/Body/Actions props"
    - "EmptyState component renders ui-empty-state with Icon (templ.Component), Title, Description regions"
    - "Table component renders ui-table-shell wrapping ui-table with typed Head/Body props"
    - "IconButton component renders borderless-icon-button for ghost tone, ui-icon-button for solid tone"
    - "UIIcon templ function renders inline SVG for recognized icon names"
    - "SpaceX/SpaceY utility components render with SpacingStep CSS classes"
    - "button.templ Icon field is wired to UIIcon (completing the Plan 02 TODO)"
    - "tailwind.input.css imports all 13 component CSS files"
    - "go test ./internal/web/ui/... is green for all new component tests"
    - "just generate succeeds"
  artifacts:
    - path: "backend/internal/web/ui/icon_button.templ"
      provides: "IconButton + UIIcon templ components"
      contains: "templ UIIcon"
    - path: "backend/internal/web/ui/modal.templ"
      provides: "Modal component with backdrop and panel"
      contains: "type ModalProps struct"
    - path: "backend/internal/web/ui/empty_state.templ"
      provides: "EmptyState component with Icon templ.Component"
      contains: "type EmptyStateProps struct"
    - path: "backend/internal/web/ui/table.templ"
      provides: "Table component with Head/Body"
      contains: "ui-table-shell"
    - path: "backend/tailwind.input.css"
      provides: "Full CSS import manifest"
      contains: "spacing.css"
  key_links:
    - from: "backend/internal/web/ui/button.templ"
      to: "backend/internal/web/ui/icon_button.templ"
      via: "@UIIcon(props.Icon)"
      pattern: "UIIcon"
    - from: "backend/tailwind.input.css"
      to: "all 13 CSS files"
      via: "@import"
      pattern: "spacing\\.css"
---

## Phase Goal

**As a** developer, **I want to** have Modal, EmptyState, Table, IconButton, and Space components available, and all CSS files imported in tailwind.input.css, **so that** the entire component library is complete and Tailwind compiles every component's CSS into the final stylesheet.

<objective>
Port the five remaining component types (modal, empty-state, table, icon-button, space) from
go-backend. Also wire the UIIcon function into button.templ (completing the Icon field from Plan 02),
and update tailwind.input.css with all 13 component imports.

Purpose: This plan closes DS-06 through DS-09 and makes the full component library available.
After this plan, Plans 03 and 04 together deliver all 11 component types with CSS + templ + tests.
The tailwind.input.css update here ensures Tailwind compiles the complete stylesheet.

Output:
- modal.css + modal.templ (new files)
- empty-state.css + empty_state.templ (new files)
- table.css + table.templ (new files)
- icon-button.css + icon_button.templ (UIIcon included) (new files)
- spacing.css + space.templ (new files)
- button.templ updated to wire @UIIcon(props.Icon)
- tailwind.input.css updated with all 13 CSS imports
- ui_test.go extended with 5 new component tests
</objective>

<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/13-design-system-foundation/13-CONTEXT.md
@.planning/phases/13-design-system-foundation/13-RESEARCH.md
@.planning/phases/13-design-system-foundation/13-PATTERNS.md
@.planning/phases/13-design-system-foundation/13-01-SUMMARY.md
@.planning/phases/13-design-system-foundation/13-02-SUMMARY.md
</context>

<interfaces>
From backend/internal/web/ui/variants.go (after Plan 01):
  type IconButtonVariant string — constants: IconButtonVariantNeutral, Warning, Success, Danger
  type IconButtonTone string — constants: IconButtonToneSolid, IconButtonToneGhost
  type SpacingStep string — constants: SpacingStepXS, SM, MD, LG, XL
  func IconButtonClass(variant IconButtonVariant, tone IconButtonTone) string
    // Ghost: "borderless-icon-button ui-icon-button-ghost ui-icon-button-{variant}"
    // Solid: "ui-icon-button ui-icon-button-solid ui-icon-button-{variant}"
  func SpaceXClass(step SpacingStep) string — "ui-space-x ui-space-x-{step}"
  func SpaceYClass(step SpacingStep) string — "ui-space-y ui-space-y-{step}"

From backend/internal/web/ui/helpers.go (after Plan 01):
  func buttonType(value string) string

UIIcon (being created in this plan):
  templ UIIcon(kind string) — switch on kind, renders inline SVG for: plus, grid3x3, list, filter,
    search, calendar, pencil, trash. Default: <span aria-hidden="true">{ kind }</span>
</interfaces>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Port modal, empty-state, table components + CSS + tests</name>
  <files>
    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/ui_test.go
  </files>
  <read_first>
    - go-backend/internal/web/ui/modal.templ (source Props struct and rendering)
    - go-backend/internal/web/ui/modal.css (source CSS)
    - go-backend/internal/web/ui/empty_state.templ (source Props struct — note Icon is templ.Component)
    - go-backend/internal/web/ui/empty-state.css (source CSS)
    - go-backend/internal/web/ui/table.templ (source Props struct)
    - go-backend/internal/web/ui/table.css (source CSS)
    - 13-PATTERNS.md (modal.templ, empty_state.templ, table.templ, and their CSS sections)
    - 13-RESEARCH.md (Pitfall 7 — modal backdrop in catalog)
    - 13-UI-SPEC.md (Modal, Empty State, Table component inventory sections)
  </read_first>
  <behavior>
    - Test: Modal(ModalProps{Title: "Confirm action"}) HTML contains "ui-modal-backdrop" and "ui-modal-panel"
    - Test: Modal(ModalProps{Title: "Confirm action"}) HTML contains "Confirm action" in a heading element
    - Test: Modal with nil Body does NOT render "ui-modal-body"
    - Test: EmptyState(EmptyStateProps{Title: "Nothing here yet"}) HTML contains "ui-empty-state" and "Nothing here yet"
    - Test: EmptyState with nil Icon does NOT render "ui-empty-state-icon"
    - Test: Table(TableProps{}) HTML contains "ui-table-shell" and "ui-table"
  </behavior>
  <action>
    Step 1 — Write failing tests in ui_test.go (RED):
    Add TestModal_RendersBackdropAndPanel, TestModal_RendersTitle, TestModal_NilBodyOmitted,
    TestEmptyState_RendersTitle, TestEmptyState_NilIconOmitted, TestTable_RendersShell.
    The textComponent helper from Plan 02's ui_test.go changes is already in scope.
    Run go test ./internal/web/ui/... — confirm failures.

    Step 2 — Create modal.css: port go-backend/internal/web/ui/modal.css verbatim.
    Must contain .ui-modal-backdrop { position: fixed; inset: 0; ... flex center },
    .ui-modal-panel { max-width: 32rem; border-radius: 1rem; ... },
    .ui-modal-header { font-size: 1.125rem; font-weight: 600; ... },
    .ui-modal-body { ... }, .ui-modal-actions { ... }.

    Step 3 — Create modal.templ: port go-backend/internal/web/ui/modal.templ.
    ModalProps struct: Title string, Body templ.Component, Actions templ.Component.
    Render structure: <div class="ui-modal-backdrop"> → <div class="ui-modal-panel"> →
    <div class="ui-modal-header"><h2>{ props.Title }</h2></div> →
    nil-guarded Body region → nil-guarded Actions region.

    Step 4 — Create empty-state.css: port go-backend/internal/web/ui/empty-state.css verbatim.
    Must contain .ui-empty-state { border: 1px dashed var(--color-border-subtle); border-radius: 1rem; ... },
    .ui-empty-state-icon { width: 4rem; height: 4rem; ... },
    .ui-empty-state-title { font-size: 1.125rem; font-weight: 600; }.

    Step 5 — Create empty_state.templ: port go-backend/internal/web/ui/empty_state.templ.
    EmptyStateProps struct: Icon templ.Component (not string — callers pass UIIcon(...)),
    Title string, Description string, Action templ.Component.
    Render .ui-empty-state-icon region only if props.Icon != nil.
    Render .ui-empty-state-description only if Description != "".
    Render .ui-empty-state-action only if Action != nil.

    Step 6 — Create table.css: port go-backend/internal/web/ui/table.css verbatim.
    Must contain .ui-table-shell { overflow-x: auto; width: 100%; },
    .ui-table { border-collapse: collapse; min-width: 100%; }.

    Step 7 — Create table.templ: port go-backend/internal/web/ui/table.templ.
    TableProps struct: Head templ.Component, Body templ.Component.
    Render structure: <div class="ui-table-shell"> → <table class="ui-table"> →
    <thead> nil-guarded Head → <tbody> nil-guarded Body.

    Run just generate && go test ./internal/web/ui/... — all GREEN.
  </action>
  <verify>
    <automated>cd backend && just generate && go test ./internal/web/ui/... -run "TestModal|TestEmptyState|TestTable" -v</automated>
  </verify>
  <acceptance_criteria>
    - TestModal_RendersBackdropAndPanel passes
    - TestModal_RendersTitle passes (HTML contains title text in heading)
    - TestModal_NilBodyOmitted passes (no "ui-modal-body" when Body is nil)
    - TestEmptyState_RendersTitle passes
    - TestEmptyState_NilIconOmitted passes
    - TestTable_RendersShell passes (HTML contains "ui-table-shell" and "ui-table")
    - modal.css contains ".ui-modal-backdrop {"
    - modal.css contains ".ui-modal-panel {"
    - empty-state.css contains ".ui-empty-state {"
    - empty-state.css contains ".ui-empty-state-icon {"
    - table.css contains ".ui-table-shell {"
    - empty_state.templ contains "type EmptyStateProps struct"
    - empty_state.templ Icon field is type "templ.Component" (not string)
    - just generate succeeds
    - Full go test ./internal/web/ui/... is green
  </acceptance_criteria>
  <done>modal, empty-state, and table components ported with CSS and tests; all passing</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Port icon-button + UIIcon + space components; wire button.templ Icon; update tailwind.input.css</name>
  <files>
    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
  </files>
  <read_first>
    - go-backend/internal/web/ui/icon_button.templ (full file — UIIcon switch with all icon cases)
    - go-backend/internal/web/ui/icon-button.css (source CSS)
    - go-backend/internal/web/ui/space.templ (source)
    - go-backend/internal/web/ui/spacing.css (source CSS)
    - backend/internal/web/ui/button.templ (current after Plan 02 — find the Icon placeholder comment)
    - backend/tailwind.input.css (current state — need to add 9 new imports)
    - 13-PATTERNS.md (icon_button.templ, space.templ, tailwind.input.css sections)
    - 13-RESEARCH.md (IconButtonClass code example; tailwind.input.css Final State section)
    - 13-UI-SPEC.md (Icon Button and CSS File Manifest sections)
  </read_first>
  <behavior>
    - Test: IconButton(IconButtonProps{Icon: "plus", Variant: IconButtonVariantNeutral, Tone: IconButtonToneGhost, Label: "Add"}) HTML contains "borderless-icon-button" and "aria-label=\"Add\""
    - Test: IconButton(IconButtonProps{Icon: "trash", Variant: IconButtonVariantDanger, Tone: IconButtonToneSolid, Label: "Delete"}) HTML contains "ui-icon-button-solid" and "ui-icon-button-danger"
    - Test: UIIcon("plus") renders an <svg> element
    - Test: UIIcon("unknown-kind") renders a <span> fallback
    - Test: SpaceX(SpaceProps{Size: SpacingStepMD}) HTML contains "ui-space-x-md"
    - Test: SpaceY(SpaceProps{Size: SpacingStepLG}) HTML contains "ui-space-y-lg"
    - Test: Button with Icon set renders an <svg> element (via UIIcon wiring)
  </behavior>
  <action>
    Step 1 — Write failing tests in ui_test.go (RED):
    Add TestIconButton_GhostNeutral, TestIconButton_SolidDanger, TestUIIcon_Plus, TestUIIcon_Fallback,
    TestSpaceX_MD, TestSpaceY_LG, TestButton_IconRendered.
    Run go test ./internal/web/ui/... — confirm failures.

    Step 2 — Create icon-button.css: port go-backend/internal/web/ui/icon-button.css verbatim.
    Must contain .ui-icon-button { min-height: 44px; min-width: 44px; ... },
    .ui-icon-button-solid { ... }, .borderless-icon-button { ... },
    .ui-icon-button-ghost { ... }, and variant tone/color rules.

    Step 3 — Create icon_button.templ: port go-backend/internal/web/ui/icon_button.templ.
    IconButtonProps struct: Label string, Icon string, Variant IconButtonVariant, Tone IconButtonTone,
    Type string, Attrs templ.Attributes.
    IconButton templ: renders <button type={buttonType(props.Type)} class={IconButtonClass(props.Variant, props.Tone)} aria-label={props.Label} {props.Attrs...}>@UIIcon(props.Icon)</button>.
    UIIcon templ function: switch on kind with cases for: "plus", "grid3x3", "list", "filter",
    "search", "calendar", "pencil", "trash". Each case renders the inline SVG from go-backend.
    Default case: <span aria-hidden="true">{ kind }</span>.
    Note: UIIcon is defined in icon_button.templ (same file as IconButton) — this is the go-backend pattern.

    Step 4 — Create spacing.css: port go-backend/internal/web/ui/spacing.css verbatim.
    Must contain .ui-space-x-xs, .ui-space-x-sm, .ui-space-x-md, .ui-space-x-lg, .ui-space-x-xl
    and matching .ui-space-y-* classes.

    Step 5 — Create space.templ: port go-backend/internal/web/ui/space.templ.
    SpaceProps struct: Size SpacingStep.
    SpaceX templ: renders <span class={SpaceXClass(props.Size)} aria-hidden="true"></span>.
    SpaceY templ: renders <div class={SpaceYClass(props.Size)} aria-hidden="true"></div>.

    Step 6 — Wire UIIcon into button.templ:
    Find the placeholder comment "// UIIcon added in Plan 04" in button.templ (added in Plan 02 Task 1).
    Replace it with the actual icon rendering:
      if props.Icon != "" {
          <span class="ui-button-icon">@UIIcon(props.Icon)</span>
      }
    UIIcon is now available because icon_button.templ is in the same package.

    Step 7 — Update backend/tailwind.input.css to match the final state from 13-RESEARCH.md:
    The complete import order after base.css + auth.css: button.css, badge.css, card.css,
    input.css, textarea.css, select.css, modal.css, empty-state.css, table.css,
    icon-button.css, form-field.css, spacing.css.
    Per D-A02: do NOT add an app.css import.

    Run just generate && go test ./internal/web/ui/... — all GREEN.
    Run just generate && go test ./... — full suite GREEN.
  </action>
  <verify>
    <automated>cd backend && just generate && go test ./... -count=1</automated>
  </verify>
  <acceptance_criteria>
    - TestIconButton_GhostNeutral passes (HTML contains "borderless-icon-button")
    - TestIconButton_SolidDanger passes (HTML contains "ui-icon-button-solid" and "ui-icon-button-danger")
    - TestUIIcon_Plus passes (HTML contains "<svg")
    - TestUIIcon_Fallback passes (HTML contains "<span" fallback for unknown kind)
    - TestSpaceX_MD passes (HTML contains "ui-space-x-md")
    - TestSpaceY_LG passes (HTML contains "ui-space-y-lg")
    - TestButton_IconRendered passes (Button with Icon="plus" renders an SVG)
    - icon_button.templ contains "templ UIIcon(kind string)"
    - icon_button.templ contains 'case "plus"' (icon switch)
    - button.templ contains "@UIIcon(props.Icon)" (wired icon rendering)
    - spacing.css contains ".ui-space-x-md {"
    - tailwind.input.css contains "@import "./internal/web/ui/spacing.css";"
    - tailwind.input.css contains "@import "./internal/web/ui/icon-button.css";"
    - tailwind.input.css contains all 13 component CSS imports (count: grep -c '@import.*web/ui' backend/tailwind.input.css should return 14 — base + auth + 12 components)
    - just generate succeeds
    - go test ./... is fully green
  </acceptance_criteria>
  <done>icon-button/space components ported; UIIcon wired into button.templ; tailwind.input.css updated with all 13 imports; full test suite green</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| UIIcon switch → HTML | Icon names are developer-supplied string constants (not user input at runtime); inline SVGs are static Go source |
| Modal backdrop → browser DOM | Backdrop uses position:fixed — this is expected for production use; the catalog renders panel-only (Pitfall 7 mitigation applied in Plan 05) |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-13-04-01 | Tampering | UIIcon default fallback | accept | The fallback renders { kind } which is escaped by templ; no XSS via icon name string |
| T-13-04-02 | Information Disclosure | icon-button.css | accept | CSS is a public static asset; no sensitive information in icon button styling rules |
| T-13-04-03 | Elevation of Privilege | catalog route (tailwind.input.css update) | accept | tailwind.input.css controls CSS only — no route registration, no handler; catalog route security is handled by Plan 05 build-tag gating |
</threat_model>

<verification>
After this plan completes:
- cd backend && just generate — must succeed
- cd backend && go test ./... -count=1 — must be green across all packages
- grep -c '@import.*web/ui' backend/tailwind.input.css — must return 14 (base + auth + 12 components)
- grep 'templ UIIcon' backend/internal/web/ui/icon_button.templ — must match
- grep '@UIIcon' backend/internal/web/ui/button.templ — must match (icon wired)
- grep 'ui-table-shell' backend/internal/web/ui/table.css — must match
- grep 'ui-empty-state' backend/internal/web/ui/empty-state.css — must match
</verification>

<success_criteria>
1. Modal, EmptyState, Table, IconButton (with UIIcon), Space components all exist with CSS + templ
2. UIIcon renders inline SVG for: plus, grid3x3, list, filter, search, calendar, pencil, trash
3. button.templ Icon field renders via @UIIcon
4. tailwind.input.css has all 13 CSS file imports (base, auth, button, badge, card, input, textarea, select, modal, empty-state, table, icon-button, form-field, spacing)
5. All DS-06/07/08/09 component tests pass
6. Full go test ./... is green
</success_criteria>

<output>
After completion, create `.planning/phases/13-design-system-foundation/13-04-SUMMARY.md`
</output>
