diff --git a/.planning/phases/13-design-system-foundation/13-RESEARCH.md b/.planning/phases/13-design-system-foundation/13-RESEARCH.md new file mode 100644 index 0000000..72fbfb9 --- /dev/null +++ b/.planning/phases/13-design-system-foundation/13-RESEARCH.md @@ -0,0 +1,717 @@ +# Phase 13: Design System Foundation - Research + +**Researched:** 2026-05-16 +**Domain:** Go + templ CSS design system port (tokens, component CSS, variant enums, dev catalog) +**Confidence:** HIGH + +## Summary + +Phase 13 is a CSS and Go enum porting task. All eleven templ component files already exist in `backend/internal/web/ui/`. No new templ components are being authored from scratch; the work is: + +1. Replace `backend/internal/web/ui/base.css` (28-line stub) with the 223-line token vocabulary from `go-backend/internal/web/ui/base.css` verbatim. +2. Add ten missing CSS files (`modal.css`, `input.css`, `textarea.css`, `select.css`, `table.css`, `empty-state.css`, `icon-button.css`, `form-field.css`, `spacing.css`) ported from go-backend with page-level selectors stripped. +3. Update `backend/tailwind.input.css` to import all thirteen CSS files. +4. **Migrate `ButtonClass()` from the compound-class pattern** (`ui-button-solid-default-md`) **to go-backend's multi-class pattern** (`ui-button ui-button-solid ui-button-default ui-button-md`) — and update all call sites in templates that use the old compound class directly as string literals. +5. Add `ButtonVariantGhost` and `BadgeVariantPrimary` to `variants.go` (with normalizer updates). +6. Align Props structs with go-backend (Button needs `Icon string`, Card needs `Header/Body/Footer templ.Component` instead of children, several components need `Disabled bool`). +7. Add helper functions for components that currently lack them (`InputClass`, `TextareaClass`, `ModalClass`, etc.) — go-backend keeps these unexported; backend uses exported names. +8. Port `select_helpers.go` from go-backend (select component has significant helper logic). +9. Add a `UIIcon` templ function and a `SpaceX`/`SpaceY` component with `SpacingStep` enum. +10. Create `backend/internal/web/ui/catalog/` with the single-page `catalog.templ` + handler registered under `//go:build catalog`. +11. Extend `ui_test.go` to cover all eleven component types. + +The critical migration risk is the **button class pattern change**: existing templates (`planning.templ`, `tasks.templ`, `events.templ`, `etapes.templ`) use compound class strings like `ui-button-solid-default-md` as raw HTML attribute values — these are NOT routed through `ButtonClass()`, so replacing `button.css` will silently break those templates unless the old compound selectors are kept or the templates are migrated. The plan must make this explicit. + +**Primary recommendation:** Adopt go-backend's multi-class pattern in Phase 13. Replace `ButtonClass()` output, replace `button.css` entirely with go-backend's multi-class selectors, AND migrate all hardcoded compound class strings in templates to the new multi-class format. Do this in one atomic wave. + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +**Token Values** +- D-T01: Port `go-backend/internal/web/ui/base.css` token values verbatim — no adjustments to brand palette, typography, or spacing scale before Phase 13 lands +- D-T02: Port ALL tokens — full 223-line vocabulary — not just the subset referenced by current component CSS files +- D-T03: Replace `backend/internal/web/ui/base.css` entirely (do not merge); go-backend version already includes the same box-sizing/reset foundation +- D-T04: Phase 13 ships the token vocabulary as CSS custom properties; wiring existing component CSS to consume `var(--…)` tokens is Phase 14–17 work — component CSS may still use hardcoded values in Phase 13 + +**app.css Scope** +- D-A01: `app.css` (the 1896-line shell) is deferred to Phases 14–17; Phase 13 ships component CSS only +- D-A02: `backend/tailwind.input.css` imports `base.css` + all component CSS files; no app shell import in Phase 13 +- D-A03: When porting component CSS, strip page-level selectors (`body`, `:root` overrides, `.app-layout`); component CSS stays scoped to `.ui-*` class selectors only +- D-A04: Catalog page uses a hand-built fake shell (sidebar column + content area) for visual context, even though the real `app.css` shell is not yet in backend + +**Catalog Route** +- D-C01: Catalog route is guarded by a Go build tag (`//go:build catalog`); production binary never includes the route — zero attack surface +- D-C02: Catalog page layout: sidebar/anchor nav with section links (`#buttons`, `#badges`, `#cards`, etc.) matching the fake shell layout +- D-C03: Each component section shows rendered variants + the templ call used to produce each variant (rendered + source, not rendered-only) +- D-C04: Static HTML only — no JavaScript interaction; all variants pre-rendered at page load + +**Component API** +- D-CA01: Add `ButtonVariantGhost` to the `ButtonVariant` enum +- D-CA02: Add `BadgeVariantPrimary` to the `BadgeVariant` enum +- D-CA03: Review and align each component's Go-side Props struct with the go-backend equivalent — fill in missing fields +- D-CA04: Extend `ui_test.go` to cover class-generation logic for all 9+ component types + +### Claude's Discretion +- Exact migration of CSS variable names: if go-backend component CSS uses slightly different class names (`.ui-button` vs `.btn`), match go-backend exactly +- Exact fake-shell HTML in the catalog: should mimic the sidebar+main-content layout visible in go-backend's catalog but is not bound to pixel-perfect reproduction +- Order of anchor sections in the catalog sidebar: alphabetical by component name (per UI-SPEC) +- Exact test coverage depth: at minimum one test per component type for the happy path; error/edge cases at planner discretion + +### Deferred Ideas (OUT OF SCOPE) +- `app.css` shell port (sidebar layout, topbar, nav shell, content area) — deferred to Phases 14–17 +- Wiring component CSS to consume `var(--…)` tokens — deferred to Phases 14–17 +- Dark mode tokens / `prefers-color-scheme` counterparts — Future requirement DARK-01 +- Responsive sidebar and mobile layout — Future requirements RESP-01..03 + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| DS-01 | CSS design tokens (colors, spacing, typography, shadows, gradients) defined in `backend/internal/web/ui/base.css` matching the go-backend token vocabulary | Replace 28-line stub with go-backend's 223-line `:root` block verbatim | +| DS-02 | Button component with primary, secondary, ghost, and danger variants | Add `ButtonVariantGhost` to variants.go; port go-backend button.css multi-class pattern; update existing templates | +| DS-03 | Input, Textarea, and Select form field components | Port input.css, textarea.css, select.css; add select_helpers.go; align Props structs with go-backend | +| DS-04 | Card component used across tablo list, detail, and content views | Align Card Props struct (Header/Body/Footer templ.Component); port go-backend card.css | +| DS-05 | Badge component supports semantic tones (primary, warning, success, danger) | Add `BadgeVariantPrimary` to variants.go; port go-backend badge.css (pill shape differs from current) | +| DS-06 | Modal component available for create/edit dialogs | Port go-backend modal.css (no modal CSS currently in backend); add ModalProps templ | +| DS-07 | Empty-state component available for zero-data views | Port go-backend empty-state.css; add EmptyStateProps templ | +| DS-08 | Table component available for list views | Port go-backend table.css; add TableProps templ | +| DS-09 | Icon-button component replaces inline icon-button patterns | Add IconButtonVariant/Tone enums; port icon-button.css; add UIIcon helper; add SpaceX/SpaceY | + + +--- + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| CSS token vocabulary | Static / Build | — | Compiled into `static/tailwind.css` by Tailwind CLI at `just generate` | +| Component CSS rules | Static / Build | — | Same pipeline as tokens | +| Go Props structs + variant enums | Backend Server | — | Compiled into the Go binary | +| templ components (HTML structure) | Backend Server | — | Rendered server-side, no client JS for structure | +| Select open/close toggle JS | Browser | — | Minimal inline script in select.templ manages `is-open` class; no framework | +| Catalog route handler | Backend Server | — | Build-tag gated; registered in router only under `-tags catalog` | +| Tailwind CSS compilation | Build toolchain | — | `./bin/tailwindcss -i tailwind.input.css -o static/tailwind.css` | + +--- + +## Standard Stack + +### Core (already in backend) + +| Tool | Version | Purpose | Status | +|------|---------|---------|--------| +| Go templ | v0.3.1020 | Server-side HTML component authoring | Pinned in justfile | +| Tailwind CSS v4 standalone | v4.0.0 | CSS compilation from `.templ` + `.go` source scanning | Pinned in justfile | +| chi router | v5.2.5 | HTTP routing; catalog route registered with build tag | In go.mod | + +### No New Dependencies + +Phase 13 adds zero new Go modules or npm packages. All work is: +- CSS file creation/replacement +- Go source file edits (`variants.go`, Props structs) +- New Go files (`select_helpers.go`, `catalog/catalog.templ`, `catalog/catalog.go`, `catalog/examples.go`) + +[VERIFIED: codebase inspection] — no missing toolchain dependencies + +--- + +## Architecture Patterns + +### System Architecture Diagram + +``` +backend/tailwind.input.css + └── @import base.css [223-line token vocabulary — DS-01] + └── @import button.css [multi-class pattern — DS-02] + └── @import badge.css [DS-05] + └── @import card.css [DS-04] + └── @import input.css [DS-03 — NEW] + └── @import textarea.css [DS-03 — NEW] + └── @import select.css [DS-03 — NEW] + └── @import modal.css [DS-06 — NEW] + └── @import empty-state.css [DS-07 — NEW] + └── @import table.css [DS-08 — NEW] + └── @import icon-button.css [DS-09 — NEW] + └── @import form-field.css [DS-03 wrapper — NEW] + └── @import spacing.css [DS-09 utility — NEW] + | + v + ./bin/tailwindcss → static/tailwind.css + | + v + Go template + +backend/internal/web/ui/ + variants.go [ButtonVariant, BadgeVariant, IconButtonVariant, SpacingStep enums] + helpers.go [mergeAttrs — unchanged] + tokens.go [semantic token constants — minor additions] + button.templ [ButtonProps — add Icon field] + badge.templ [BadgeProps — unchanged API, no CSS class change needed] + card.templ [CardProps — BREAKING: switch from children to Header/Body/Footer fields] + input.templ [InputProps — add Disabled, Required fields] + textarea.templ [TextareaProps — add Disabled, Required fields — NEW FILE] + select.templ [SelectProps — complex: multi-value, placeholder — NEW FILE] + select_helpers.go [selectPlaceholder, selectOptionSelected, etc. — NEW FILE] + modal.templ [ModalProps — Title/Body/Actions — NEW FILE] + empty_state.templ [EmptyStateProps — NEW FILE] + table.templ [TableProps — Head/Body — NEW FILE] + icon_button.templ [IconButtonProps + UIIcon helper — NEW FILE] + space.templ [SpaceX/SpaceY — NEW FILE] + form_field.templ [FormFieldProps — NEW FILE] + ui_test.go [extended to cover all 11+ components] + + catalog/ + catalog.templ [single-page layout with sidebar + anchor nav — NEW] + catalog.go [//go:build catalog handler + router registration — NEW] + examples.go [typed Example structs, one per component variant — NEW] +``` + +### Multi-Class Button Pattern (Critical Migration) + +[VERIFIED: codebase inspection] The current backend generates compound classes: + +```go +// Current backend/internal/web/ui/variants.go +func ButtonClass(variant ButtonVariant, tone ButtonTone, size Size) string { + return "ui-button ui-button-" + string(t) + "-" + string(v) + "-" + string(s) + // Produces: "ui-button ui-button-solid-default-md" +} +``` + +The go-backend generates separate classes: + +```go +// go-backend/internal/web/ui/variants.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)) + // Produces: "ui-button ui-button-solid ui-button-default ui-button-md" +} +``` + +Go-backend's `button.css` uses compound CSS selectors that match the space-separated output: + +```css +/* go-backend pattern */ +.ui-button-solid.ui-button-default { + background: var(--color-brand-primary); +} +.ui-button-solid.ui-button-default:hover { + background: var(--color-brand-primary-hover); +} +``` + +**Migration scope for template hardcodes:** These files use the OLD compound class pattern directly as HTML attribute strings (not through `ButtonClass()`): + +- `backend/templates/planning.templ` — uses `ui-button-soft-neutral-md`, `ui-button-solid-default-md` +- `backend/templates/tasks.templ` — uses `ui-button-soft-neutral-md`, `ui-button-soft-danger-md` +- `backend/templates/events.templ` — uses `ui-button-soft-neutral-md` +- `backend/templates/etapes.templ` — uses `ui-button-soft-neutral-md`, `ui-button-soft-danger-md` + +[VERIFIED: codebase grep] Each occurrence must be updated to the multi-class pattern: +- `ui-button-solid-default-md` → `ui-button ui-button-solid ui-button-default ui-button-md` +- `ui-button-soft-neutral-md` → `ui-button ui-button-soft ui-button-neutral ui-button-md` +- `ui-button-soft-danger-md` → `ui-button ui-button-soft ui-button-danger ui-button-md` + +Also: existing tests in `ui_test.go` assert the compound string `"ui-button ui-button-solid-default-md"` — these must be updated to the multi-class assertion. + +### Card Props API Change (Breaking) + +[VERIFIED: codebase inspection] The current backend `Card` accepts children via `templ.WithChildren`: + +```go +// Current backend — Card(attrs templ.Attributes) — children via context +templ Card(attrs templ.Attributes) { +
+ { children... } +
+} +// Called as: @ui.Card(nil) {

content

} +``` + +The go-backend uses typed `Header/Body/Footer templ.Component` fields: + +```go +// go-backend — Card(props CardProps) +type CardProps struct { + Header templ.Component + Body templ.Component + Footer templ.Component +} +// Called as: @ui.Card(ui.CardProps{Body: myBodyComponent}) +``` + +**Call-site impact:** The existing test `TestCard_RendersChildren` uses `templ.WithChildren` and asserts `
` block directly in the templ component. This is the approved pattern for the select (per CONTEXT.md interaction contract: "JS adds `is-open` class"). The script uses `window.__uiSelectInitAll` to avoid double-registration across HTMX swaps and re-initializes on `htmx:afterSwap` events. + +The backend `select.templ` must include this same inline script verbatim — it is part of the component contract, not an external JS file. + +### Ghost Button Variant — New CSS Required + +[VERIFIED: codebase inspection] Go-backend's `button.css` does NOT include a ghost variant. `ButtonVariantGhost` is being ADDED (D-CA01). The ghost CSS rules must be authored following the multi-class pattern: + +```css +/* New ghost rules to add to button.css */ +.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; +} +``` + +Ghost does not interact with tone (solid/soft) — it is its own variant class. UI-SPEC confirms: `ui-button-ghost` is a standalone class, not combined with a tone class. + +### Badge Primary Variant — New CSS Required + +[VERIFIED: codebase inspection] Go-backend's `badge.css` does not include `ui-badge-primary`. Per UI-SPEC: + +```css +.ui-badge-primary { + background: var(--color-surface-brand-soft); + border-color: rgba(128, 78, 236, 0.3); /* --color-brand-primary at 30% opacity */ + color: var(--color-text-brand); +} +``` + +### textarea.css — Missing from go-backend base.css + +[VERIFIED: codebase inspection] Go-backend's `textarea.css` is a short file. Textarea does not have a separate CSS class helper — it uses a static `class="ui-textarea"` in the templ. The CSS mirrors input styling with `min-height: 7rem` and `resize: vertical`. + +--- + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Select dropdown open/close | Custom JS state machine | Inline script in select.templ (go-backend pattern) | Already solved with HTMX re-init support | +| Icon SVG delivery | Icon font, external CDN | Inline SVG in UIIcon templ switch | Zero-dependency, go-backend pattern | +| Component token wiring | Replace all hex values with var() | Leave hardcoded in Phase 13 (D-T04) | Token wiring is Phase 14–17 work | +| Catalog HTML generation | Static site generator | Live HTTP handler (-tags catalog) | Simpler, server-rendered | + +--- + +## Common Pitfalls + +### Pitfall 1: Button Class Pattern Mismatch — Silent Visual Break +**What goes wrong:** Port go-backend's `button.css` (which uses `.ui-button-solid.ui-button-default { }` selectors) without updating the old compound class strings in templates. The buttons render with no styling. +**Why it happens:** Templates hardcode `class="ui-button ui-button-soft-neutral-md"` directly as attribute strings — these bypass `ButtonClass()` and are invisible to the Go type system. +**How to avoid:** Grep ALL `.templ` files for `ui-button-` before replacing button.css. Update each hardcoded string to the multi-class equivalent. +**Warning signs:** `just generate` succeeds but buttons appear unstyled in the browser. + +### Pitfall 2: Existing Tests Assert Old Compound Class +**What goes wrong:** `ui_test.go` has `wantClass := "ui-button ui-button-solid-default-md"` — this will fail after `ButtonClass()` is updated. +**Why it happens:** Tests were written against the old pattern. +**How to avoid:** Update test assertions alongside `ButtonClass()` in the same commit. + +### Pitfall 3: Card API Change Breaks Test +**What goes wrong:** `TestCard_RendersChildren` uses `templ.WithChildren(context.Background(), child)` — this breaks when Card switches from `{ children... }` to typed `Header/Body/Footer` fields. +**Why it happens:** The go-backend Card API is structurally different. +**How to avoid:** Rewrite the card test to use `CardProps{Body: child}` when migrating the templ. + +### Pitfall 4: Build Tag File Pair — Linker Error Without Stub +**What goes wrong:** `router.go` calls `RegisterCatalogRoute(r)` unconditionally. When built without `-tags catalog`, the function is undefined → linker error. +**Why it happens:** Go requires all referenced symbols to be defined in the build graph. +**How to avoid:** Provide a `catalog_stub.go` with `//go:build !catalog` that defines `RegisterCatalogRoute` as a no-op. Both files must be in the same package. + +### Pitfall 5: templ Generate Overwrites Hand-Edited `*_templ.go` +**What goes wrong:** Editing a `*_templ.go` file directly — `just generate` (which runs `templ generate`) will overwrite it on the next run. +**Why it happens:** `*_templ.go` files are machine-generated from `.templ` source. +**How to avoid:** Only edit `.templ` files. Run `just generate` after every `.templ` change. Never touch `*_templ.go`. + +### Pitfall 6: select.templ Inline Script and HTMX Re-Init +**What goes wrong:** Omitting the `htmx:afterSwap` listener from the select's inline script means selects inside HTMX-swapped fragments do not initialize. +**Why it happens:** Inline scripts run once on page load; HTMX replaces DOM without re-running scripts. +**How to avoid:** Port the full `__uiSelectInitAll` + `htmx:afterSwap` listener block verbatim from go-backend. + +### Pitfall 7: Modal Rendered Without Backdrop in Catalog +**What goes wrong:** Catalog renders `ui-modal-backdrop` with `position: fixed; inset: 0` — this overlays the entire catalog page. +**Why it happens:** The backdrop class uses fixed positioning for actual use; in the catalog it obscures everything. +**How to avoid:** In the catalog modal section, render only `ui-modal-panel` directly (skip the backdrop wrapper), or render the modal in a relative-positioned container with an override. UI-SPEC says: "Modal section renders an open modal panel (without the backdrop toggle)". + +### Pitfall 8: Missing `ui-button-ghost` CSS for NormalizedButtonVariant +**What goes wrong:** `NormalizedButtonVariant` defaults unknown variants to `ButtonVariantDefault`. If `ButtonVariantGhost` is not added to the switch, ghost falls back to default (silent). +**Why it happens:** The normalizer exhaustively lists known variants. +**How to avoid:** Add `case ButtonVariantGhost: return variant` to `NormalizedButtonVariant`. + +--- + +## Code Examples + +### ButtonClass Multi-Class Output (Updated) + +```go +// Source: go-backend/internal/web/ui/variants.go (verified) +// Updated backend pattern to match: +func ButtonClass(variant ButtonVariant, tone ButtonTone, size Size) string { + v := NormalizedButtonVariant(variant) + t := NormalizedButtonTone(tone) + s := NormalizedSize(size) + return "ui-button ui-button-" + string(t) + " ui-button-" + string(v) + " ui-button-" + string(s) +} +// Output: "ui-button ui-button-solid ui-button-default ui-button-md" +``` + +### Ghost Variant in NormalizedButtonVariant + +```go +// Add to variants.go normalizer: +func NormalizedButtonVariant(variant ButtonVariant) ButtonVariant { + switch variant { + case ButtonVariantNeutral, ButtonVariantWarning, ButtonVariantSuccess, ButtonVariantDanger, ButtonVariantGhost: + return variant + default: + return ButtonVariantDefault + } +} +``` + +### IconButtonClass (New — from go-backend) + +```go +// Source: go-backend/internal/web/ui/variants.go (verified) +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) + } +} +``` + +### SpaceX/SpaceY Class Functions (New) + +```go +// Source: go-backend/internal/web/ui/variants.go (verified) +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)) +} +``` + +### Build Tag File Pair for Catalog Route + +```go +// File: backend/internal/web/catalog_route_catalog.go +//go:build catalog +package web + +import "github.com/go-chi/chi/v5" + +func RegisterCatalogRoute(r chi.Router) { + r.Get("/ui-catalog", catalogPageHandler()) +} +``` + +```go +// File: backend/internal/web/catalog_route_stub.go +//go:build !catalog +package web + +import "github.com/go-chi/chi/v5" + +func RegisterCatalogRoute(r chi.Router) {} // no-op in production builds +``` + +### tailwind.input.css Final State + +```css +/* Source: 13-UI-SPEC.md CSS File Manifest (verified) */ +@import "tailwindcss"; + +@source "./templates/**/*.templ"; +@source "./internal/web/**/*.templ"; +@source "./internal/web/**/*.go"; + +@import "./internal/web/ui/base.css"; +@import "./internal/web/ui/button.css"; +@import "./internal/web/ui/badge.css"; +@import "./internal/web/ui/card.css"; +@import "./internal/web/ui/input.css"; +@import "./internal/web/ui/textarea.css"; +@import "./internal/web/ui/select.css"; +@import "./internal/web/ui/modal.css"; +@import "./internal/web/ui/empty-state.css"; +@import "./internal/web/ui/table.css"; +@import "./internal/web/ui/icon-button.css"; +@import "./internal/web/ui/form-field.css"; +@import "./internal/web/ui/spacing.css"; +``` + +### Catalog Handler Pattern + +```go +// Source: go-backend/cmd/designsystem/main.go (adapted for live HTTP) +// The catalog handler renders the single-page catalog via templ. +// Fake shell uses Tailwind utility classes — no import of real app.css. +``` + +--- + +## Component Delta Table + +This table shows exactly what changes are needed per component — the planner can turn each row into one or more tasks. + +| Component | CSS Status | Props Status | Variants | Helpers | Test Coverage | +|-----------|-----------|--------------|----------|---------|---------------| +| Button | REPLACE (multi-class migration) | Add `Icon string`, `Disabled bool` | Add `Ghost` | Update `ButtonClass()` normalizer | Update existing tests | +| Badge | REPLACE (pill shape, token-based colors) | No change needed | Add `Primary` | Update `BadgeClass()` normalizer | Extend existing tests | +| Card | REPLACE (token-based, add header/footer sections) | BREAKING: replace children with `Header/Body/Footer templ.Component` | — | Add `CardClass()` → returns `"ui-card"` | Rewrite existing test | +| Input | NEW FILE (port from go-backend) | Add `Disabled bool`, `Required bool` | — | Add `InputClass(disabled bool)` | New test | +| Textarea | NEW FILE (port from go-backend) | New file: add `Disabled bool`, `Required bool` | — | Add `TextareaClass()` | New test | +| Select | NEW FILE (port from go-backend) | New file: complex (Values []string, Multiple bool, Placeholder) | — | New `select_helpers.go` (port verbatim) | New test | +| Modal | NEW FILE (port from go-backend) | New file: `Title/Body/Actions templ.Component` | — | Add `ModalClass()` | New test | +| Empty State | NEW FILE (port from go-backend) | New file: `Icon templ.Component` (not string) | — | Add `EmptyStateClass()` | New test | +| Table | NEW FILE (port from go-backend) | New file: `Head/Body templ.Component` | — | Add `TableClass()` | New test | +| Icon Button | NEW FILE (port from go-backend) | New file: `IconButtonProps` + `UIIcon(kind string)` | `IconButtonVariant`, `IconButtonTone` | Add `IconButtonClass()` | New test | +| Form Field | NEW FILE (port from go-backend) | New file: `Label/Hint/Error/ForID` | — | Add `FormFieldClass()` | New test | +| Space X/Y | NEW FILE (port from go-backend) | New file: `SpaceProps{Size SpacingStep}` | `SpacingStep` enum | Add `SpaceXClass()`, `SpaceYClass()` | New test | + +--- + +## Props Struct Alignment Detail + +### Button — add `Icon string` and `Disabled bool` + +Current `backend` ButtonProps is missing `Icon string` (go-backend has it). `Disabled bool` is mentioned in UI-SPEC but go-backend does NOT have it — go-backend relies on `Attrs` pass-through for `disabled`. **Do not add `Disabled bool` to Button** — use `Attrs: templ.Attributes{"disabled": true}` for now. Only add `Icon string`. + +[VERIFIED: go-backend/internal/web/ui/button.templ — no Disabled field in go-backend] + +### Input — `Disabled bool` and `Required bool` + +Go-backend `InputProps` does NOT have `Disabled bool` or `Required bool` — these are passed via `Attrs`. UI-SPEC defines them as explicit fields. Since this is Claude's discretion for Props alignment (D-CA03), and the UI-SPEC is authoritative for the backend design contract, add them as explicit fields in the backend Props with corresponding `if props.Disabled { disabled }` conditionals in the templ. + +### EmptyState — `Icon templ.Component` not `Icon string` + +Go-backend's `EmptyStateProps.Icon` is `templ.Component` (a rendered component, not an icon name string). This means callers pass `ui.UIIcon("grid3x3")` directly. The backend port must match: `Icon templ.Component`. + +### Card — Children to Typed Fields (Breaking) + +Current backend uses `{ children... }` with `templ.WithChildren`. Go-backend uses `Header/Body/Footer templ.Component` struct fields. The switch is breaking at the call site. Since no production template currently calls `Card` with content (it was introduced in Phase 1 but not yet used in pages), the only impact is the test `TestCard_RendersChildren` which must be rewritten. + +--- + +## File Creation Order (Recommended Wave Structure) + +The planner should structure waves to ensure CSS is available before catalog tests run: + +**Wave 1 — Token and enum foundation (no visual output yet)** +- Replace `base.css` with go-backend version (D-T01/T02/T03) +- Update `variants.go`: add Ghost/Primary variants, add IconButtonVariant/Tone, SpacingStep enums +- Update `tokens.go`: no substantive changes needed (existing constants are compatible) + +**Wave 2 — Migrate existing components to go-backend APIs** +- Update `button.css` (multi-class selectors) + `ButtonClass()` (multi-class output) +- Update compound class strings in templates (planning.templ, tasks.templ, events.templ, etapes.templ) +- Update `badge.css` (pill shape, add primary variant) +- Update `card.templ` (Props API migration) + `card.css` (token-based) +- Update `ui_test.go` for Button (multi-class assertions) and Card (typed Props) + +**Wave 3 — Port new CSS + templ files** +- Add `input.css`, `input.templ` (update existing stub) +- Add `textarea.css`, `textarea.templ` (new file) +- Add `modal.css`, `modal.templ` (new file) +- Add `select.css`, `select.templ`, `select_helpers.go` (new files — complex) +- Add `empty-state.css`, `empty_state.templ` (new file) +- Add `table.css`, `table.templ` (new file) +- Add `icon-button.css`, `icon_button.templ` (new file — includes UIIcon) +- Add `form-field.css`, `form_field.templ` (new file) +- Add `spacing.css`, `space.templ` (new file) +- Update `tailwind.input.css` with all new @import entries +- Extend `ui_test.go` for all new components + +**Wave 4 — Catalog** +- Create `backend/internal/web/ui/catalog/catalog.templ` +- Create `backend/internal/web/ui/catalog/examples.go` +- Create `backend/internal/web/catalog_route_catalog.go` (build tag: catalog) +- Create `backend/internal/web/catalog_route_stub.go` (build tag: !catalog) +- Wire `RegisterCatalogRoute(r)` into `NewRouter` in `router.go` +- Add justfile target: `catalog: just generate && go run -tags catalog ./cmd/web` +- Manual visual sign-off checkpoint before Phase 14 + +--- + +## Validation Architecture + +### Test Framework + +| Property | Value | +|----------|-------| +| Framework | Go's built-in testing + testify (none — stdlib only) | +| Config file | none (go test ./...) | +| Quick run command | `go test ./internal/web/ui/...` | +| Full suite command | `just test` (runs `just generate && go test ./...`) | + +### Phase Requirements → Test Map + +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| DS-01 | Token custom properties present in base.css | Manual (visual) + file content check | `grep -c 'color-brand-primary' backend/internal/web/ui/base.css` | ✅ (replace) | +| DS-02 | ButtonClass ghost variant emits `ui-button-ghost` | Unit | `go test ./internal/web/ui/... -run TestButton` | ✅ (extend) | +| DS-03 | Input/Textarea/Select render correct class attributes | Unit | `go test ./internal/web/ui/... -run "TestInput\|TestTextarea\|TestSelect"` | ❌ Wave 3 | +| DS-04 | Card renders ui-card-header/body/footer sections | Unit | `go test ./internal/web/ui/... -run TestCard` | ✅ (rewrite) | +| DS-05 | BadgeClass primary variant emits `ui-badge-primary` | Unit | `go test ./internal/web/ui/... -run TestBadge` | ✅ (extend) | +| DS-06 | Modal renders ui-modal-backdrop and ui-modal-panel | Unit | `go test ./internal/web/ui/... -run TestModal` | ❌ Wave 3 | +| DS-07 | EmptyState renders ui-empty-state with title | Unit | `go test ./internal/web/ui/... -run TestEmptyState` | ❌ Wave 3 | +| DS-08 | Table renders ui-table-shell wrapping ui-table | Unit | `go test ./internal/web/ui/... -run TestTable` | ❌ Wave 3 | +| DS-09 | IconButton ghost neutral emits borderless-icon-button | Unit | `go test ./internal/web/ui/... -run TestIconButton` | ❌ Wave 3 | + +### Wave 0 Gaps (tests needed before implementation) + +- [ ] `backend/internal/web/ui/ui_test.go` — rewrite `TestCard_RendersChildren` → `TestCard_RendersTypedRegions` +- [ ] New test functions for all 8 components added in Wave 3 (see UI-SPEC test coverage table) +- Framework already installed — no setup needed + +### Sampling Rate +- **Per commit:** `go test ./internal/web/ui/...` +- **Per wave merge:** `just test` (includes templ generate) +- **Phase gate:** `just test` green + manual catalog visual sign-off at `/ui-catalog` + +--- + +## Open Questions + +1. **Ghost button — which size rules apply?** + - What we know: `ButtonVariantGhost` is a new variant not in go-backend's button.css. The size rules (`ui-button-sm`, `ui-button-md`, `ui-button-lg`) are defined as standalone classes in go-backend and apply to ALL variants. + - What's unclear: Should ghost buttons have padding/sizing from the size classes, or use a custom minimal padding? + - Recommendation: Inherit the standard size classes (`.ui-button-md { padding: 0.7rem 1rem; }`). Ghost variant only overrides background/color, not sizing. + +2. **Catalog route vs. router.go signature** + - What we know: `NewRouter(...)` in `router.go` has a fixed signature with many named deps. Adding catalog registration changes `router.go`. + - What's unclear: The build-tag stub approach keeps the function call in `router.go` unconditional — this is safe because the stub satisfies the symbol. + - Recommendation: Use the two-file build-tag approach (catalog + !catalog stub) with an unconditional call in `router.go`. Zero diff to router.go signature. + +3. **`auth-provider-*` CSS in button.css — keep or remove?** + - What we know: Current `button.css` has `auth-provider-button`, `auth-provider-separator`, etc. appended from Phase 8. Go-backend does not have these. + - What's unclear: Phase 13 replaces button.css entirely — these auth selectors would be lost. + - Recommendation: Move auth-provider CSS to a separate `auth.css` file BEFORE replacing button.css. Import it in `tailwind.input.css`. This is a prerequisite sub-task in Wave 2. + +--- + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | Ghost button inherits size class padding from `.ui-button-sm/md/lg` | Component Delta Table | Ghost renders with wrong sizing if size classes don't apply | +| A2 | No templates currently call `Card` with children in production pages | Card Props section | Card change breaks rendered pages (not just tests) | +| A3 | `auth-provider-*` CSS from Phase 8 must be preserved | Open Question 3 | Login page loses styling if auth CSS is dropped | + +--- + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| Go | Build | ✓ | 1.26.0 (go.mod) | — | +| templ CLI | `just generate` | ✓ | v0.3.1020 (justfile) | `go install github.com/a-h/templ/cmd/templ@v0.3.1020` | +| tailwindcss standalone | `just generate` | ✓ | v4.0.0 (justfile) | `just bootstrap` | +| just | Task runner | ✓ | system | Run commands manually | + +[VERIFIED: justfile inspection] — toolchain versions pinned. + +--- + +## Security Domain + +> `security_enforcement` not set to false — section required. + +### Applicable ASVS Categories + +| ASVS Category | Applies | Standard Control | +|---------------|---------|-----------------| +| V2 Authentication | no | Phase 13 is CSS only — no auth changes | +| V3 Session Management | no | No session changes | +| V4 Access Control | yes — catalog route | Build tag `//go:build catalog` ensures catalog is never in production binary | +| V5 Input Validation | no | Component CSS and static Props structs, no user input validated | +| V6 Cryptography | no | No crypto operations | + +### Known Threat Patterns + +| Pattern | STRIDE | Standard Mitigation | +|---------|--------|---------------------| +| Catalog route exposed in production | Information Disclosure | `//go:build catalog` + `//go:build !catalog` stub; production binary built without `-tags catalog` | +| XSS via templ text content | Tampering | templ auto-escapes `{ variable }` interpolations — no raw HTML injection via Props | + +--- + +## Sources + +### Primary (HIGH confidence — verified by codebase inspection) + +- `backend/internal/web/ui/` — all existing files read directly +- `go-backend/internal/web/ui/` — all source CSS and templ files read directly +- `go-backend/internal/web/ui/catalog/` — catalog.templ, examples.go, catalog_test.go, pages.go read directly +- `backend/tailwind.input.css` — current import manifest verified +- `backend/internal/web/router.go` — router structure verified +- `backend/justfile` — build targets verified (tailwind_version = v4.0.0) +- `backend/templates/*.templ` — compound class hardcodes verified by grep + +### Secondary (HIGH confidence — from project planning artifacts) + +- `.planning/phases/13-design-system-foundation/13-CONTEXT.md` — locked decisions +- `.planning/phases/13-design-system-foundation/13-UI-SPEC.md` — visual contract +- `.planning/REQUIREMENTS.md` — DS-01 through DS-09 + +--- + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH — verified by direct codebase inspection +- Architecture: HIGH — both source and target codebases fully read +- Pitfalls: HIGH — compound class migration risk verified by grep, card API break verified by test inspection +- Props struct alignment: HIGH — all templ files in both repos read + +**Research date:** 2026-05-16 +**Valid until:** 2026-06-16 (stable — no external dependencies, pure codebase-internal work)