---
phase: 13-design-system-foundation
plan: "03"
subsystem: backend/internal/web/ui
tags: [form-components, input, textarea, select, form-field, design-system, tdd]
dependency_graph:
  requires: [13-01, 13-02]
  provides: [input-component, textarea-component, select-component, form-field-component]
  affects:
    - backend/internal/web/ui/input.css
    - backend/internal/web/ui/input.templ
    - backend/internal/web/ui/textarea.css
    - backend/internal/web/ui/textarea.templ
    - backend/internal/web/ui/select.css
    - backend/internal/web/ui/select.templ
    - backend/internal/web/ui/select_helpers.go
    - backend/internal/web/ui/form-field.css
    - backend/internal/web/ui/form_field.templ
    - backend/internal/web/ui/ui_test.go
    - backend/tailwind.input.css
tech_stack:
  added: []
  patterns: [verbatim-port, tdd-red-green, inline-js-htmx-reinit, nil-guard-optional-regions]
key_files:
  created:
    - backend/internal/web/ui/input.css
    - backend/internal/web/ui/input.templ
    - backend/internal/web/ui/textarea.css
    - backend/internal/web/ui/textarea.templ
    - backend/internal/web/ui/select.css
    - backend/internal/web/ui/select.templ
    - backend/internal/web/ui/select_helpers.go
    - backend/internal/web/ui/form-field.css
    - backend/internal/web/ui/form_field.templ
  modified:
    - backend/internal/web/ui/ui_test.go
    - backend/tailwind.input.css
decisions:
  - "InputProps and TextareaProps include Disabled and Required as explicit bool fields (D-CA03) — conditional attribute rendering via templ if blocks rather than Attrs pass-through"
  - "select_helpers.go ported verbatim from go-backend with no naming changes (package-private helpers, not exported)"
  - "tailwind.input.css updated to include all four new CSS imports in order: input, textarea, select, form-field"
metrics:
  duration: "~12 minutes"
  completed_date: "2026-05-16"
  tasks: 2
  files: 11
---

# Phase 13 Plan 03: Form Input Components — Input, Textarea, Select, FormField Summary

Four form-input component types ported from go-backend with matching CSS, typed Props structs, and full test coverage: Input (class ui-input, inputType/inputID helpers), Textarea (class ui-textarea, textareaRows default 4, resize vertical), Select (ui-select-control, inline JS with __uiSelectInitAll and htmx:afterSwap re-init), FormField (conditional label/hint/error regions).

## Tasks Completed

| Task | Name | Commit | Key Files |
|------|------|--------|-----------|
| 1 (RED) | Failing tests for Input and Textarea | ace9f5b | ui_test.go (7 tests) |
| 1 (GREEN) | Port input.templ/input.css and textarea.templ/textarea.css | 9556b20 | input.css, input.templ, textarea.css, textarea.templ, tailwind.input.css |
| 2 (RED) | Failing tests for Select and FormField | 50e3fb0 | ui_test.go (6 tests) |
| 2 (GREEN) | Port select + form-field components with CSS and helpers | 52fb77d | select.css, select.templ, select_helpers.go, form-field.css, form_field.templ, tailwind.input.css |

## What Was Built

**Task 1 — Input and Textarea (TDD):**
- `input.css`: `.ui-input` with `appearance: none`, `background: var(--color-surface-default)`, `border-radius: 0.75rem`, `min-height: 44px`, `padding: 0.75rem 0.95rem`, `width: 100%`, plus placeholder and focus rules.
- `input.templ`: `InputProps` struct with `ID`, `Name`, `Value`, `Placeholder`, `Type`, `Disabled bool`, `Required bool`, `Attrs templ.Attributes`. Uses `inputID(props.ID, props.Name)` and `inputType(props.Type)` helpers. Conditional `disabled`/`required` attributes.
- `textarea.css`: `.ui-textarea` with same base styling as input plus `min-height: 7rem` and `resize: vertical`.
- `textarea.templ`: `TextareaProps` struct with `ID`, `Name`, `Value`, `Placeholder`, `Rows int`, `Disabled bool`, `Required bool`, `Attrs templ.Attributes`. Uses `inputID()` and `textareaRows()` helpers (defaults to 4 rows).
- `tailwind.input.css`: Added `@import` for `input.css` and `textarea.css`.
- 7 tests passing: `TestInput_DefaultType`, `TestInput_EmailType`, `TestInput_IDFallback`, `TestInput_ExplicitID`, `TestTextarea_RendersClass`, `TestTextarea_DefaultRows`, `TestTextarea_ExplicitRows`.

**Task 2 — Select and FormField (TDD):**
- `select_helpers.go`: 9 helper functions ported verbatim from go-backend: `selectPlaceholder`, `selectNativeID`, `selectMenuID`, `selectBoolData`, `selectSelectedValues`, `selectOptionSelected`, `selectSelectedLabels`, `selectSelectedLabel`, `selectMenuOptionClass`, `selectIsDisabled`. Imports `"strings"` and `"github.com/a-h/templ"`.
- `select.templ`: `SelectOption` struct (`Value`, `Label`, `Disabled bool`) and `SelectProps` struct (`ID`, `Name`, `Placeholder`, `Value`, `Values []string`, `Multiple bool`, `Options []SelectOption`, `Attrs`). Full custom dropdown with hidden native `<select>`, visible `ui-select-control` button, and dropdown `ui-select-menu`. Inline `<script>` block with `window.__uiSelectInitAll` and `document.addEventListener("htmx:afterSwap", ...)` re-init listener (Pitfall 6 compliance).
- `select.css`: `.ui-select` (relative positioned wrapper), `.ui-select-control` (min-height 44px, border-radius 0.75rem), `.ui-select-menu` (absolute positioned, max-height 16rem, overflow-y auto), option states (hover, selected, disabled).
- `form_field.templ`: `FormFieldProps` struct with `Label string`, `For string`, `Field templ.Component`, `Error string`, `Hint string`. Conditional label, field, hint, and error rendering with nil/empty guards.
- `form-field.css`: `.ui-form-field` (grid layout), `.ui-form-label` (font-weight 600), `.ui-form-hint` (muted color), `.ui-form-error` (danger foreground color).
- `tailwind.input.css`: Added `@import` for `select.css` and `form-field.css`.
- 6 tests passing: `TestSelect_RendersControl`, `TestSelect_HasInlineScript`, `TestSelect_HasHtmxListener`, `TestFormField_RendersLabel`, `TestFormField_RendersError`, `TestFormField_NoErrorWhenEmpty`.

## Verification Results

- `just generate`: succeeds (no templ compile errors)
- `go test ./internal/web/ui/... -count=1`: all 29 tests pass
- `go test ./... -count=1`: all packages pass (auth, db, files, jobs, web, web/ui, templates)
- `grep -l 'ui-input\|ui-textarea\|ui-select\|ui-form-field' backend/internal/web/ui/*.css`: lists 4 files
- `grep '__uiSelectInitAll' backend/internal/web/ui/select.templ`: matches (inline JS present)
- `grep 'htmx:afterSwap' backend/internal/web/ui/select.templ`: matches (re-init listener present)

## Deviations from Plan

### Auto-fixed Issues

None — plan executed exactly as written. `Disabled` and `Required` bool fields were added to `InputProps` and `TextareaProps` as specified by D-CA03 (plan action step 3 and 5). This was within the plan's explicit instructions.

## Known Stubs

None — all components are fully implemented with correct CSS selectors and templ rendering logic. 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-03-01 (inline script read-only DOM), T-13-03-02 (templ auto-escapes FormField Error), and T-13-03-03 (static Disabled/Required attributes) are all satisfied:
- The inline select script reads only DOM data attributes — no user-controlled string is eval'd
- `FormFieldProps` strings (Label, Hint, Error) are server-controlled and templ auto-escapes them
- Disabled/Required are Go bool fields rendered as static HTML attributes

## TDD Gate Compliance

- Task 1 RED gate: commit `ace9f5b` — 7 failing tests (undefined: Input, undefined: InputProps, undefined: Textarea, undefined: TextareaProps)
- Task 1 GREEN gate: commit `9556b20` — all 7 TestInput/TestTextarea tests pass; full suite green
- Task 2 RED gate: commit `50e3fb0` — 6 failing tests (undefined: Select, undefined: SelectProps, undefined: FormField, undefined: FormFieldProps)
- Task 2 GREEN gate: commit `52fb77d` — all 6 TestSelect/TestFormField tests pass; full suite green
- REFACTOR gate: Not needed — implementation was clean on first pass

## Self-Check: PASSED
