diff --git a/.planning/phases/13-design-system-foundation/13-01-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-PLAN.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-01-PLAN.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-01-PLAN.md diff --git a/.planning/phases/13-design-system-foundation/13-01-SUMMARY.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-SUMMARY.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-01-SUMMARY.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-01-SUMMARY.md diff --git a/.planning/phases/13-design-system-foundation/13-02-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-PLAN.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-02-PLAN.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-02-PLAN.md diff --git a/.planning/phases/13-design-system-foundation/13-02-SUMMARY.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-SUMMARY.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-02-SUMMARY.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-02-SUMMARY.md diff --git a/.planning/phases/13-design-system-foundation/13-03-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-03-PLAN.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-03-PLAN.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-03-PLAN.md diff --git a/.planning/phases/13-design-system-foundation/13-03-SUMMARY.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-03-SUMMARY.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-03-SUMMARY.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-03-SUMMARY.md diff --git a/.planning/phases/13-design-system-foundation/13-04-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-04-PLAN.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-04-PLAN.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-04-PLAN.md diff --git a/.planning/phases/13-design-system-foundation/13-04-SUMMARY.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-04-SUMMARY.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-04-SUMMARY.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-04-SUMMARY.md diff --git a/.planning/phases/13-design-system-foundation/13-05-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-05-PLAN.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-05-PLAN.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-05-PLAN.md diff --git a/.planning/phases/13-design-system-foundation/13-05-SUMMARY.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-05-SUMMARY.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-05-SUMMARY.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-05-SUMMARY.md diff --git a/.planning/phases/13-design-system-foundation/13-CONTEXT.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-CONTEXT.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-CONTEXT.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-CONTEXT.md diff --git a/.planning/phases/13-design-system-foundation/13-DISCUSSION-LOG.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-DISCUSSION-LOG.md similarity index 100% rename from .planning/phases/13-design-system-foundation/13-DISCUSSION-LOG.md rename to .planning/milestones/v3.0-phases/13-design-system-foundation/13-DISCUSSION-LOG.md diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-PATTERNS.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-PATTERNS.md new file mode 100644 index 0000000..a2754b9 --- /dev/null +++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-PATTERNS.md @@ -0,0 +1,1084 @@ +# Phase 13: Design System Foundation - Pattern Map + +**Mapped:** 2026-05-16 +**Files analyzed:** 22 (new/modified files across all waves) +**Analogs found:** 22 / 22 (all from go-backend reference implementation) + +--- + +## File Classification + +| New/Modified File | Role | Data Flow | Closest Analog | Match Quality | +|-------------------|------|-----------|----------------|---------------| +| `backend/internal/web/ui/base.css` | config | static | `go-backend/internal/web/ui/base.css` | exact (verbatim port) | +| `backend/internal/web/ui/button.css` | config | static | `go-backend/internal/web/ui/button.css` | exact (replace) | +| `backend/internal/web/ui/badge.css` | config | static | `go-backend/internal/web/ui/badge.css` | exact (replace) | +| `backend/internal/web/ui/card.css` | config | static | `go-backend/internal/web/ui/card.css` | exact (replace) | +| `backend/internal/web/ui/input.css` | config | static | `go-backend/internal/web/ui/input.css` | exact (new file) | +| `backend/internal/web/ui/textarea.css` | config | static | `go-backend/internal/web/ui/textarea.css` | exact (new file) | +| `backend/internal/web/ui/select.css` | config | static | `go-backend/internal/web/ui/select.css` | exact (new file) | +| `backend/internal/web/ui/modal.css` | config | static | `go-backend/internal/web/ui/modal.css` | exact (new file) | +| `backend/internal/web/ui/empty-state.css` | config | static | `go-backend/internal/web/ui/empty-state.css` | exact (new file) | +| `backend/internal/web/ui/table.css` | config | static | `go-backend/internal/web/ui/table.css` | exact (new file) | +| `backend/internal/web/ui/icon-button.css` | config | static | `go-backend/internal/web/ui/icon-button.css` | exact (new file) | +| `backend/internal/web/ui/form-field.css` | config | static | `go-backend/internal/web/ui/form-field.css` | exact (new file) | +| `backend/internal/web/ui/spacing.css` | config | static | `go-backend/internal/web/ui/spacing.css` | exact (new file) | +| `backend/tailwind.input.css` | config | static | current file + RESEARCH.md manifest | exact | +| `backend/internal/web/ui/variants.go` | utility | transform | `go-backend/internal/web/ui/variants.go` | exact (extend) | +| `backend/internal/web/ui/helpers.go` | utility | transform | `go-backend/internal/web/ui/helpers.go` | exact (extend) | +| `backend/internal/web/ui/button.templ` | component | request-response | `go-backend/internal/web/ui/button.templ` | exact | +| `backend/internal/web/ui/card.templ` | component | request-response | `go-backend/internal/web/ui/card.templ` | exact (breaking API change) | +| `backend/internal/web/ui/input.templ` | component | request-response | `go-backend/internal/web/ui/input.templ` | exact | +| `backend/internal/web/ui/textarea.templ` | component | request-response | `go-backend/internal/web/ui/textarea.templ` | exact (new file) | +| `backend/internal/web/ui/select.templ` | component | event-driven | `go-backend/internal/web/ui/select.templ` | exact (new file) | +| `backend/internal/web/ui/select_helpers.go` | utility | transform | `go-backend/internal/web/ui/select_helpers.go` | exact (new file) | +| `backend/internal/web/ui/modal.templ` | component | request-response | `go-backend/internal/web/ui/modal.templ` | exact (new file) | +| `backend/internal/web/ui/empty_state.templ` | component | request-response | `go-backend/internal/web/ui/empty_state.templ` | exact (new file) | +| `backend/internal/web/ui/table.templ` | component | request-response | `go-backend/internal/web/ui/table.templ` | exact (new file) | +| `backend/internal/web/ui/icon_button.templ` | component | request-response | `go-backend/internal/web/ui/icon_button.templ` | exact (new file) | +| `backend/internal/web/ui/form_field.templ` | component | request-response | `go-backend/internal/web/ui/form_field.templ` | exact (new file) | +| `backend/internal/web/ui/space.templ` | component | request-response | `go-backend/internal/web/ui/space.templ` | exact (new file) | +| `backend/internal/web/ui/ui_test.go` | test | transform | `go-backend/internal/web/ui/ui_test.go` | exact (extend) | +| `backend/internal/web/ui/catalog/catalog.templ` | component | request-response | `go-backend/internal/web/ui/catalog/catalog.templ` | role-match (adapted) | +| `backend/internal/web/ui/catalog/examples.go` | utility | transform | `go-backend/internal/web/ui/catalog/examples.go` | role-match (adapted) | +| `backend/internal/web/catalog_route_catalog.go` | middleware | request-response | `backend/internal/web/router.go` | role-match | +| `backend/internal/web/catalog_route_stub.go` | middleware | request-response | build-tag stub pattern (RESEARCH.md) | role-match | +| `backend/internal/web/ui/auth.css` | config | static | `backend/internal/web/ui/button.css` lines 121–180 | exact (extract) | + +--- + +## Pattern Assignments + +### Wave 1: Token and Enum Foundation + +--- + +### `backend/internal/web/ui/base.css` (config, static) + +**Analog:** `go-backend/internal/web/ui/base.css` +**Action:** Replace the 28-line stub entirely. Port the 223-line `:root` block verbatim. + +**Current stub pattern** (`backend/internal/web/ui/base.css` lines 1–28): +```css +/* 28-line stub — no tokens, only box-sizing reset and body defaults */ +*, *::before, *::after { box-sizing: border-box; } +body { margin: 0; font-family: system-ui; color: #0f172a; background-color: #ffffff; } +:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; } +``` + +**Target pattern** (`go-backend/internal/web/ui/base.css` lines 1–60, first section only): +```css +:root { + /* Text */ + --color-text-primary: hsl(0 0% 9%); + --color-text-secondary: #475467; + --color-text-muted: hsl(0 0% 43.5%); + --color-text-faint: #9ca3af; + --color-text-inverse: #ffffff; + --color-text-brand: #804eec; + /* ... all 223 lines verbatim ... */ + + /* Surfaces */ + --color-surface-page: hsl(0 0% 100%); + --color-surface-default: #ffffff; + + /* Borders */ + --color-border-default: hsl(0 0% 90.9%); + + /* Brand and focus */ + --color-brand-primary: #804eec; + --color-focus-ring: rgba(124, 58, 237, 0.2); +} +``` + +**Critical rule:** D-T02 — port ALL 223 lines, not just a subset. D-T03 — replace entirely, do not merge. + +--- + +### `backend/internal/web/ui/variants.go` (utility, transform) + +**Analog:** `go-backend/internal/web/ui/variants.go` + +**Current pattern** (`backend/internal/web/ui/variants.go` lines 1–106): +- Exported `ButtonClass()`, `BadgeClass()`, `NormalizedButtonVariant()` etc. +- `ButtonClass` produces compound class: `"ui-button ui-button-solid-default-md"` (line 96) +- Missing: `IconButtonVariant`, `IconButtonTone`, `SpacingStep` enums +- Missing: `ButtonVariantGhost`, `BadgeVariantPrimary` constants + +**Target multi-class pattern** (`go-backend/internal/web/ui/variants.go` lines 63–64): +```go +func buttonClass(variant ButtonVariant, tone ButtonTone, size Size) string { + return "ui-button ui-button-" + string(normalizedButtonTone(tone)) + + " ui-button-" + string(normalizedButtonVariant(variant)) + + " ui-button-" + string(normalizedSize(size)) +} +// Output: "ui-button ui-button-solid ui-button-default ui-button-md" +``` + +**New enums to add** (`go-backend/internal/web/ui/variants.go` lines 28–51): +```go +type IconButtonVariant string +const ( + IconButtonVariantNeutral IconButtonVariant = "neutral" + IconButtonVariantWarning IconButtonVariant = "warning" + IconButtonVariantSuccess IconButtonVariant = "success" + IconButtonVariantDanger IconButtonVariant = "danger" +) +type IconButtonTone string +const ( + IconButtonToneSolid IconButtonTone = "solid" + IconButtonToneGhost IconButtonTone = "ghost" +) +type SpacingStep string +const ( + SpacingStepXS SpacingStep = "xs" + SpacingStepSM SpacingStep = "sm" + SpacingStepMD SpacingStep = "md" + SpacingStepLG SpacingStep = "lg" + SpacingStepXL SpacingStep = "xl" +) +``` + +**New variant constants to add:** +```go +// Add to ButtonVariant block: +ButtonVariantGhost ButtonVariant = "ghost" + +// Add to BadgeVariant block: +BadgeVariantPrimary BadgeVariant = "primary" +``` + +**New class functions to add** (`go-backend/internal/web/ui/variants.go` lines 67–88): +```go +func IconButtonClass(variant IconButtonVariant, tone IconButtonTone) string { + normalizedVariant := NormalizedIconButtonVariant(variant) + switch NormalizedIconButtonTone(tone) { + case IconButtonToneGhost: + return "borderless-icon-button ui-icon-button-ghost ui-icon-button-" + string(normalizedVariant) + default: + return "ui-icon-button ui-icon-button-solid ui-icon-button-" + string(normalizedVariant) + } +} +func SpaceXClass(step SpacingStep) string { + return "ui-space-x ui-space-x-" + string(NormalizedSpacingStep(step)) +} +func SpaceYClass(step SpacingStep) string { + return "ui-space-y ui-space-y-" + string(NormalizedSpacingStep(step)) +} +``` + +**Updated normalizer for Ghost variant:** +```go +func NormalizedButtonVariant(variant ButtonVariant) ButtonVariant { + switch variant { + case ButtonVariantNeutral, ButtonVariantWarning, ButtonVariantSuccess, ButtonVariantDanger, ButtonVariantGhost: + return variant + default: + return ButtonVariantDefault + } +} +``` + +**Naming convention:** Backend uses exported names (`ButtonClass`, `NormalizedButtonVariant`) while go-backend uses unexported (`buttonClass`, `normalizedButtonVariant`). Keep the exported naming for backend consistency. + +--- + +### `backend/internal/web/ui/helpers.go` (utility, transform) + +**Analog:** `go-backend/internal/web/ui/helpers.go` + +**Current pattern** (`backend/internal/web/ui/helpers.go` lines 1–16): +- `mergeAttrs(base, override templ.Attributes) templ.Attributes` — keep this + +**New functions to add** (`go-backend/internal/web/ui/helpers.go` lines 1–31): +```go +func buttonType(value string) string { + if value == "" { + return "button" + } + return value +} +func inputType(value string) string { + if value == "" { + return "text" + } + return value +} +func inputID(id string, name string) string { + if id != "" { + return id + } + return name +} +func textareaRows(rows int) string { + if rows <= 0 { + rows = 4 + } + return strconv.Itoa(rows) +} +``` + +**Import to add:** `"strconv"` (for `textareaRows`) + +--- + +### Wave 2: Migrate Existing Components + +--- + +### `backend/internal/web/ui/auth.css` (config, static — NEW prerequisite file) + +**Analog:** `backend/internal/web/ui/button.css` lines 121–180 (auth-provider block to extract) + +**Reason:** Before replacing `button.css` entirely with go-backend's version, the auth-provider selectors (`.auth-provider-button`, `.auth-provider-separator`, etc.) must be moved to a standalone `auth.css` file. Go-backend has no equivalent — this is backend-only CSS from Phase 8. + +**Extract pattern** (lines 121–180 of current `backend/internal/web/ui/button.css`): +```css +/* auth.css — sign-in provider controls, extracted from button.css in Phase 13 */ +.auth-provider-stack { ... } +.auth-provider-button { ... } +.auth-provider-button:hover { ... } +.auth-provider-button:focus-visible { ... } +.auth-provider-button-disabled { ... } +.auth-provider-separator { ... } +.auth-provider-separator span { ... } +.auth-provider-separator em { ... } +``` + +Then add `@import "./internal/web/ui/auth.css";` to `tailwind.input.css` to preserve login page styling. + +--- + +### `backend/internal/web/ui/button.css` (config, static — REPLACE) + +**Analog:** `go-backend/internal/web/ui/button.css` + +**Multi-class selector pattern** (`go-backend/internal/web/ui/button.css` lines 1–163): +```css +/* Base button */ +.ui-button { + align-items: center; + border: 0; + border-radius: 0.35rem; + cursor: pointer; + display: inline-flex; + font-weight: 600; + gap: 0.5rem; + justify-content: center; + line-height: 1; + min-height: 44px; + text-decoration: none; + transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; +} +.ui-button-icon, .ui-button-icon svg { height: 1rem; width: 1rem; } +.ui-button:focus-visible { box-shadow: 0 0 0 3px var(--color-focus-ring); outline: none; } + +/* Sizes — standalone, apply to all variants */ +.ui-button-sm { font-size: 0.875rem; min-height: 40px; padding: 0.625rem 0.9rem; } +.ui-button-md { font-size: 0.95rem; padding: 0.7rem 1rem; } +.ui-button-lg { font-size: 1rem; padding: 0.82rem 1.15rem; } + +/* Solid+variant compound selectors */ +.ui-button-solid.ui-button-default { background: var(--color-brand-primary); color: var(--color-text-inverse); } +.ui-button-solid.ui-button-default:hover { background: var(--color-brand-primary-hover); } +/* ... all tone+variant combinations from go-backend ... */ +.ui-button-soft.ui-button-danger { background: var(--color-status-danger-soft-bg-alt); color: var(--color-status-danger-soft-foreground-strong); } +``` + +**Ghost variant to ADD** (not in go-backend — authored new per RESEARCH.md): +```css +/* Ghost variant — new in Phase 13, D-CA01 */ +.ui-button-ghost { + background: transparent; + color: var(--color-brand-primary); +} +.ui-button-ghost:hover { background: var(--color-surface-brand-soft); } +.ui-button-ghost:focus-visible { box-shadow: 0 0 0 3px var(--color-focus-ring); outline: none; } +``` + +**CRITICAL: No nesting rule.** Both the current and go-backend files use flat top-level selectors. `.ui-button-solid.ui-button-default` is a compound selector (two classes on the same element) — not nesting. + +--- + +### `backend/internal/web/ui/button.templ` (component, request-response — UPDATE) + +**Analog:** `go-backend/internal/web/ui/button.templ` + +**Current pattern** (`backend/internal/web/ui/button.templ` lines 1–23): +```go +type ButtonProps struct { + Label string + Variant ButtonVariant + Tone ButtonTone + Size Size + Type string + Attrs templ.Attributes +} +templ Button(props ButtonProps) { + {{ btnType := props.Type }} + if btnType == "" { {{ btnType = "button" }} } + {{ class := ButtonClass(props.Variant, props.Tone, props.Size) }} + +} +``` + +**Target pattern** (`go-backend/internal/web/ui/button.templ` lines 1–22): +```go +type ButtonProps struct { + Label string + Variant ButtonVariant + Tone ButtonTone + Size Size + Type string + Icon string // NEW — added from go-backend + Attrs templ.Attributes +} +templ Button(props ButtonProps) { + +} +``` + +**Notes:** +- Switch from inline `{{ btnType := ... }}` to `buttonType()` helper from `helpers.go` +- `ButtonClass()` output changes from compound to multi-class (via variants.go update) +- Do NOT add `Disabled bool` — use `Attrs: templ.Attributes{"disabled": true}` per RESEARCH.md + +--- + +### Template hardcode migration (4 files) + +**Files with old compound class strings:** +- `backend/templates/planning.templ` — lines 16, 20, 22, 25 +- `backend/templates/tasks.templ` — lines 158, 375 +- `backend/templates/events.templ` — lines 23, 32 +- `backend/templates/etapes.templ` — lines 57, 65, 77, 86, 93 + +**Old → new class string mapping:** +``` +"ui-button ui-button-solid-default-md" → "ui-button ui-button-solid ui-button-default ui-button-md" +"ui-button ui-button-soft-neutral-md" → "ui-button ui-button-soft ui-button-neutral ui-button-md" +"ui-button ui-button-soft-danger-md" → "ui-button ui-button-soft ui-button-danger ui-button-md" +``` + +**Pattern for additional Tailwind utility classes** (tasks.templ, etapes.templ have extras like `flex-shrink-0`, `text-xs`): +Keep the extra classes; replace only the `ui-button-*` part: +``` +class="ui-button ui-button-soft-danger-md flex-shrink-0 text-xs" +→ +class="ui-button ui-button-soft ui-button-danger ui-button-md flex-shrink-0 text-xs" +``` + +--- + +### `backend/internal/web/ui/badge.css` (config, static — REPLACE) + +**Analog:** `go-backend/internal/web/ui/badge.css` + +**Target pattern** (`go-backend/internal/web/ui/badge.css` lines 1–33): +```css +.ui-badge { + border: 1px solid transparent; + border-radius: 999px; /* pill shape — different from current */ + display: inline-flex; + font-size: 0.75rem; + font-weight: 600; + line-height: 1.2; + padding: 0.3rem 0.75rem; +} +.ui-badge-info { background: var(--color-status-info-soft-bg); border-color: var(--color-status-info-soft-border); color: var(--color-status-info-foreground); } +.ui-badge-warning { background: var(--color-status-warning-soft-bg); border-color: var(--color-status-warning-soft-border); color: var(--color-status-warning-foreground); } +.ui-badge-success { background: var(--color-status-success-soft-bg); border-color: var(--color-status-success-soft-border); color: var(--color-status-success-foreground); } +.ui-badge-danger { background: var(--color-status-danger-soft-bg); border-color: var(--color-status-danger-soft-border); color: var(--color-status-danger-foreground); } +``` + +**Primary variant to ADD** (not in go-backend — authored new per D-CA02): +```css +.ui-badge-primary { + background: var(--color-surface-brand-soft); + border-color: rgba(128, 78, 236, 0.3); + color: var(--color-text-brand); +} +``` + +--- + +### `backend/internal/web/ui/card.css` (config, static — REPLACE) + +**Analog:** `go-backend/internal/web/ui/card.css` + +**Target pattern** (`go-backend/internal/web/ui/card.css` lines 1–27): +```css +.ui-card { background: var(--color-surface-default); border: 1px solid var(--color-border-default); border-radius: 1rem; box-shadow: var(--shadow-surface-md); } +.ui-card-header, .ui-card-body, .ui-card-footer { padding: 1.25rem 1.5rem; } +.ui-card-header, .ui-card-footer { border-color: var(--color-border-default); } +.ui-card-header { border-bottom-style: solid; border-bottom-width: 1px; } +.ui-card-footer { border-top-style: solid; border-top-width: 1px; } +``` + +--- + +### `backend/internal/web/ui/card.templ` (component, request-response — BREAKING UPDATE) + +**Analog:** `go-backend/internal/web/ui/card.templ` + +**Current pattern** (`backend/internal/web/ui/card.templ` lines 1–11) — children-based: +```go +templ Card(attrs templ.Attributes) { +
+ { children... } +
+} +``` + +**Target pattern** (`go-backend/internal/web/ui/card.templ` lines 1–27) — typed fields: +```go +type CardProps struct { + Header templ.Component + Body templ.Component + Footer templ.Component +} +templ Card(props CardProps) { +
+ if props.Header != nil { +
@props.Header
+ } + if props.Body != nil { +
@props.Body
+ } + if props.Footer != nil { + + } +
+} +``` + +**Call site impact:** No production templates call `Card` with children — the only breakage is `TestCard_RendersChildren` in `ui_test.go` (see test section below). + +--- + +### Wave 3: New CSS + templ Files + +--- + +### `backend/internal/web/ui/input.css` (config, static — NEW) + +**Analog:** `go-backend/internal/web/ui/input.css` (port verbatim, strip any page-level selectors) + +```css +.ui-input { + appearance: none; + background: var(--color-surface-default); + border: 1px solid var(--color-border-default); + border-radius: 0.75rem; + color: var(--color-text-primary); + font: inherit; + line-height: 1.4; + min-height: 44px; + padding: 0.75rem 0.95rem; + width: 100%; +} +.ui-input::placeholder { color: var(--color-text-faint); } +.ui-input:focus { border-color: var(--color-brand-focus); box-shadow: 0 0 0 3px var(--color-focus-ring-strong); outline: none; } +``` + +--- + +### `backend/internal/web/ui/input.templ` (component, request-response — UPDATE existing stub) + +**Analog:** `go-backend/internal/web/ui/input.templ` + +**Target pattern** (`go-backend/internal/web/ui/input.templ` lines 1–22): +```go +type InputProps struct { + ID string + Name string + Value string + Placeholder string + Type string + Attrs templ.Attributes +} +templ Input(props InputProps) { + +} +``` + +**Note on `Disabled`/`Required`:** Go-backend does NOT have these as explicit fields — pass via `Attrs: templ.Attributes{"disabled": true}`. The UI-SPEC allows adding them as explicit fields (D-CA03 is Claude's discretion). Planner may choose to add them with `if props.Disabled { disabled }` conditionals per RESEARCH.md Props detail section. + +--- + +### `backend/internal/web/ui/textarea.templ` (component, request-response — NEW FILE) + +**Analog:** `go-backend/internal/web/ui/textarea.templ` + +**Pattern** (`go-backend/internal/web/ui/textarea.templ` lines 1–21): +```go +type TextareaProps struct { + ID string + Name string + Value string + Placeholder string + Rows int + Attrs templ.Attributes +} +templ Textarea(props TextareaProps) { + +} +``` + +**Helpers used:** `inputID()` and `textareaRows()` from `helpers.go` (add these in Wave 1). + +--- + +### `backend/internal/web/ui/textarea.css` (config, static — NEW FILE) + +Port verbatim from go-backend. File mirrors input.css styling with `min-height: 7rem` and `resize: vertical`. + +--- + +### `backend/internal/web/ui/select.templ` + `select_helpers.go` (component, event-driven — NEW FILES) + +**Analog:** `go-backend/internal/web/ui/select.templ` + `go-backend/internal/web/ui/select_helpers.go` + +**Props pattern** (`go-backend/internal/web/ui/select.templ` lines 1–18): +```go +type SelectOption struct { + Value string + Label string + Disabled bool +} +type SelectProps struct { + ID string + Name string + Placeholder string + Value string + Values []string + Multiple bool + Options []SelectOption + Attrs templ.Attributes +} +``` + +**Inline script pattern** (`go-backend/internal/web/ui/select.templ` lines 114–249): +The `