docs(13): research design system foundation phase
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e867b735d9
commit
5bb2636577
1 changed files with 717 additions and 0 deletions
717
.planning/phases/13-design-system-foundation/13-RESEARCH.md
Normal file
717
.planning/phases/13-design-system-foundation/13-RESEARCH.md
Normal file
|
|
@ -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>
|
||||
## 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
|
||||
</user_constraints>
|
||||
|
||||
<phase_requirements>
|
||||
## 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 |
|
||||
</phase_requirements>
|
||||
|
||||
---
|
||||
|
||||
## 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 <link rel="stylesheet" href="/static/tailwind.css">
|
||||
|
||||
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) {
|
||||
<section class="ui-card" { attrs... }>
|
||||
{ children... }
|
||||
</section>
|
||||
}
|
||||
// Called as: @ui.Card(nil) { <p>content</p> }
|
||||
```
|
||||
|
||||
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 `<section class="ui-card"`. Migrating to go-backend's API requires updating this test AND any templates calling `Card` with children. Currently no templates use `Card` with content (the component is stubbed in Phase 1 and not yet wired into pages). The test must be rewritten.
|
||||
|
||||
### Catalog Route — Build Tag Pattern
|
||||
|
||||
[VERIFIED: codebase inspection] The go-backend approach is a separate `cmd/designsystem/main.go` that generates static HTML files. The CONTEXT.md specifies a different approach for backend: a live HTTP handler gated by `//go:build catalog`.
|
||||
|
||||
The canonical Go build tag pattern for a catalog handler file:
|
||||
|
||||
```go
|
||||
//go:build catalog
|
||||
|
||||
package web
|
||||
|
||||
import "net/http"
|
||||
|
||||
func RegisterCatalogRoute(mux chi.Router) {
|
||||
mux.Get("/ui-catalog", catalogHandler)
|
||||
}
|
||||
```
|
||||
|
||||
The router calls `RegisterCatalogRoute(r)` only in the catalog build. In the default build, the function is either absent (linker drops it) or provided by a stub file:
|
||||
|
||||
```go
|
||||
//go:build !catalog
|
||||
|
||||
package web
|
||||
|
||||
func RegisterCatalogRoute(mux chi.Router) {} // no-op
|
||||
```
|
||||
|
||||
This pattern requires TWO files — one for each build tag — so the function signature is always available to the compiler, but only the catalog build wires up the handler.
|
||||
|
||||
**Alternative (simpler):** Include the catalog handler in a file tagged `//go:build catalog` and call `RegisterCatalogRoute` unconditionally in `router.go` only if a second stub file satisfies the symbol in non-catalog builds. This is the standard Go build-tag gating pattern.
|
||||
|
||||
### Select Component — Inline JavaScript
|
||||
|
||||
[VERIFIED: codebase inspection] Go-backend's `select.templ` embeds a self-initializing `<script>` 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)
|
||||
Loading…
Reference in a new issue