docs(13): create phase 13 design system foundation plan (5 plans, 4 waves)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a25421e2cf
commit
9e954a40e7
6 changed files with 1573 additions and 0 deletions
|
|
@ -33,6 +33,7 @@
|
|||
**Mode:** mvp
|
||||
**Status:** Pending
|
||||
**Requirements:** DS-01, DS-02, DS-03, DS-04, DS-05, DS-06, DS-07, DS-08, DS-09
|
||||
**Plans:** 5 plans
|
||||
**Success Criteria:**
|
||||
1. `backend/internal/web/ui/base.css` defines all CSS custom properties (color, spacing, typography, shadows, gradients) matching the go-backend token vocabulary
|
||||
2. All component types are implemented as templ components: button, input, textarea, select, card, badge, modal, empty-state, table, icon-button
|
||||
|
|
@ -40,6 +41,13 @@
|
|||
4. A component catalog page (`/ui-catalog`, dev-only) renders all components for visual verification
|
||||
5. All existing templates compile and unit tests pass with no regressions
|
||||
|
||||
Plans:
|
||||
- [ ] 13-01-PLAN.md — Token vocabulary + enum/helper foundation (base.css, auth.css extraction, variants.go, helpers.go)
|
||||
- [ ] 13-02-PLAN.md — Migrate existing components to go-backend API (button multi-class, badge pill, card typed API, template hardcodes)
|
||||
- [ ] 13-03-PLAN.md — Port form-input components: input, textarea, select, form-field (CSS + templ + tests)
|
||||
- [ ] 13-04-PLAN.md — Port surface components: modal, empty-state, table, icon-button, space + tailwind.input.css manifest
|
||||
- [ ] 13-05-PLAN.md — Catalog route (build-tag gated) + visual sign-off checkpoint
|
||||
|
||||
**User-in-loop:** Review the catalog page before proceeding to per-view application phases. Confirm token choices (brand color, radius, shadow levels) match what you want the product to look like.
|
||||
|
||||
### Phase 14: Auth Pages
|
||||
|
|
|
|||
258
.planning/phases/13-design-system-foundation/13-01-PLAN.md
Normal file
258
.planning/phases/13-design-system-foundation/13-01-PLAN.md
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
---
|
||||
phase: 13-design-system-foundation
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- backend/internal/web/ui/base.css
|
||||
- backend/internal/web/ui/auth.css
|
||||
- backend/internal/web/ui/variants.go
|
||||
- backend/internal/web/ui/helpers.go
|
||||
- backend/tailwind.input.css
|
||||
autonomous: true
|
||||
requirements:
|
||||
- DS-01
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "base.css contains the full 223-line CSS custom property vocabulary from go-backend"
|
||||
- "auth-provider button styles are preserved in auth.css (not lost when button.css is replaced)"
|
||||
- "variants.go declares ButtonVariantGhost, BadgeVariantPrimary, IconButtonVariant, IconButtonTone, and SpacingStep enums"
|
||||
- "helpers.go declares buttonType, inputType, inputID, and textareaRows helper functions"
|
||||
- "tailwind.input.css imports auth.css so the login page retains provider button styling"
|
||||
- "go test ./internal/web/ui/... passes (no regressions from enum additions)"
|
||||
artifacts:
|
||||
- path: "backend/internal/web/ui/base.css"
|
||||
provides: "Full CSS custom property token vocabulary"
|
||||
contains: "--color-brand-primary"
|
||||
- path: "backend/internal/web/ui/auth.css"
|
||||
provides: "Auth provider button CSS extracted from button.css"
|
||||
contains: ".auth-provider-button"
|
||||
- path: "backend/internal/web/ui/variants.go"
|
||||
provides: "All variant enums including Ghost, Primary, IconButton, SpacingStep"
|
||||
contains: "ButtonVariantGhost"
|
||||
- path: "backend/internal/web/ui/helpers.go"
|
||||
provides: "Helper functions for templ components"
|
||||
contains: "buttonType"
|
||||
key_links:
|
||||
- from: "backend/tailwind.input.css"
|
||||
to: "backend/internal/web/ui/auth.css"
|
||||
via: "@import"
|
||||
pattern: "auth\\.css"
|
||||
- from: "backend/internal/web/ui/variants.go"
|
||||
to: "ButtonVariantGhost"
|
||||
via: "const block"
|
||||
pattern: "ButtonVariantGhost"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** developer, **I want to** have a complete CSS token vocabulary and Go variant enums in the backend, **so that** every subsequent plan can port component CSS and templ files against a stable foundation without losing any existing styling.
|
||||
|
||||
<objective>
|
||||
Replace the 28-line backend/base.css stub with the full 223-line token vocabulary from go-backend,
|
||||
extract auth-provider CSS from button.css into auth.css before button.css is replaced,
|
||||
and extend variants.go + helpers.go with all new enums and helper functions.
|
||||
|
||||
Purpose: This is the mandatory prerequisite wave. Without the full token vocabulary, component CSS
|
||||
ported in Plans 02–04 cannot reference var(--...) tokens. Without auth.css extraction, replacing
|
||||
button.css in Plan 02 silently destroys the login page's provider button styling.
|
||||
|
||||
Output:
|
||||
- backend/internal/web/ui/base.css — 223-line token vocabulary (verbatim port, per D-T01/T02/T03)
|
||||
- backend/internal/web/ui/auth.css — auth-provider selectors extracted from current button.css
|
||||
- backend/internal/web/ui/variants.go — extended with Ghost/Primary variants + new enums
|
||||
- backend/internal/web/ui/helpers.go — extended with buttonType/inputType/inputID/textareaRows
|
||||
- backend/tailwind.input.css — auth.css import added (button.css replacement in Plan 02)
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/phases/13-design-system-foundation/13-CONTEXT.md
|
||||
@.planning/phases/13-design-system-foundation/13-RESEARCH.md
|
||||
@.planning/phases/13-design-system-foundation/13-PATTERNS.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Extract auth.css and replace base.css</name>
|
||||
<files>
|
||||
backend/internal/web/ui/auth.css,
|
||||
backend/internal/web/ui/base.css,
|
||||
backend/tailwind.input.css
|
||||
</files>
|
||||
<read_first>
|
||||
- backend/internal/web/ui/button.css (lines 121–180 contain the auth-provider CSS to extract verbatim)
|
||||
- backend/internal/web/ui/base.css (current 28-line stub to understand what is being replaced)
|
||||
- go-backend/internal/web/ui/base.css (the 223-line source to port verbatim)
|
||||
- backend/tailwind.input.css (current 4-import manifest to update)
|
||||
</read_first>
|
||||
<action>
|
||||
Step 1 — Create backend/internal/web/ui/auth.css: copy lines 121–180 from the current
|
||||
backend/internal/web/ui/button.css verbatim. The first line must be a comment:
|
||||
"/* auth.css — sign-in provider controls, extracted from button.css in Phase 13 */".
|
||||
The file must contain the selectors: .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.
|
||||
|
||||
Step 2 — Replace backend/internal/web/ui/base.css entirely with the contents of
|
||||
go-backend/internal/web/ui/base.css (verbatim — per D-T01, D-T02, D-T03).
|
||||
Do NOT merge with the existing 28-line stub. The target file is 223 lines.
|
||||
|
||||
Step 3 — Update backend/tailwind.input.css: add the line
|
||||
"@import "./internal/web/ui/auth.css";" immediately after the base.css import line.
|
||||
Do not yet add imports for the component CSS files being created in Plans 02–04 — those
|
||||
are added in Plan 04.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c 'color-brand-primary' backend/internal/web/ui/base.css</automated>
|
||||
Expected output: at least 3 (the token appears in multiple rules).
|
||||
Also: grep -c 'auth-provider-button' backend/internal/web/ui/auth.css — must return at least 3.
|
||||
Also: grep 'auth\.css' backend/tailwind.input.css — must match.
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- backend/internal/web/ui/base.css is exactly 223 lines (or close — the exact line count from go-backend)
|
||||
- backend/internal/web/ui/base.css contains "--color-brand-primary: #804eec"
|
||||
- backend/internal/web/ui/base.css contains "--color-text-primary"
|
||||
- backend/internal/web/ui/base.css contains "--shadow-surface-md"
|
||||
- backend/internal/web/ui/base.css does NOT contain "box-sizing: border-box" at the top (the stub had that; go-backend version puts it inside :root or omits it)
|
||||
- backend/internal/web/ui/auth.css exists and contains ".auth-provider-button {"
|
||||
- backend/internal/web/ui/auth.css contains ".auth-provider-separator {"
|
||||
- backend/tailwind.input.css contains "@import "./internal/web/ui/auth.css";"
|
||||
- The auth.css import appears after the base.css import in tailwind.input.css
|
||||
</acceptance_criteria>
|
||||
<done>base.css replaced with 223-line token vocabulary; auth.css created with extracted provider styles; tailwind.input.css updated with auth.css import</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Extend variants.go with new enums and helpers.go with new helper functions</name>
|
||||
<files>
|
||||
backend/internal/web/ui/variants.go,
|
||||
backend/internal/web/ui/helpers.go,
|
||||
backend/internal/web/ui/ui_test.go
|
||||
</files>
|
||||
<read_first>
|
||||
- backend/internal/web/ui/variants.go (current file — full read to understand existing enum patterns)
|
||||
- backend/internal/web/ui/helpers.go (current file — full read to understand existing helper pattern)
|
||||
- backend/internal/web/ui/ui_test.go (current test file — understand existing test structure)
|
||||
- go-backend/internal/web/ui/variants.go (source for new enums and class functions)
|
||||
- go-backend/internal/web/ui/helpers.go (source for new helper functions)
|
||||
- 13-PATTERNS.md (Pattern for variants.go and helpers.go changes)
|
||||
- 13-RESEARCH.md (Code Examples section for exact function signatures)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test: NormalizedButtonVariant(ButtonVariantGhost) returns ButtonVariantGhost (not ButtonVariantDefault)
|
||||
- Test: ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) contains "ui-button-ghost"
|
||||
- Test: NormalizedBadgeVariant(BadgeVariantPrimary) returns BadgeVariantPrimary
|
||||
- Test: BadgeClass(BadgeVariantPrimary) == "ui-badge ui-badge-primary"
|
||||
- Test: IconButtonClass(IconButtonVariantNeutral, IconButtonToneGhost) contains "borderless-icon-button"
|
||||
- Test: IconButtonClass(IconButtonVariantNeutral, IconButtonToneSolid) contains "ui-icon-button-solid"
|
||||
- Test: SpaceXClass(SpacingStepMD) == "ui-space-x ui-space-x-md"
|
||||
- Test: SpaceYClass(SpacingStepLG) == "ui-space-y ui-space-y-lg"
|
||||
</behavior>
|
||||
<action>
|
||||
Step 1 — Write the failing tests in ui_test.go first (RED). Add test functions for:
|
||||
TestButtonVariantGhost_Normalizer, TestButtonClass_GhostVariant,
|
||||
TestBadgeVariantPrimary_Normalizer, TestBadgeClass_PrimaryVariant,
|
||||
TestIconButtonClass_GhostNeutral, TestIconButtonClass_SolidNeutral,
|
||||
TestSpaceXClass_MD, TestSpaceYClass_LG.
|
||||
Run go test ./internal/web/ui/... and confirm failures (RED).
|
||||
|
||||
Step 2 — Update variants.go (GREEN):
|
||||
a. Add "ButtonVariantGhost ButtonVariant = "ghost"" to the ButtonVariant const block.
|
||||
b. Add "ButtonVariantGhost" case to NormalizedButtonVariant switch: "case ButtonVariantGhost: return variant".
|
||||
c. Add "BadgeVariantPrimary BadgeVariant = "primary"" to the BadgeVariant const block.
|
||||
d. Add "BadgeVariantPrimary" case to NormalizedBadgeVariant switch.
|
||||
e. Add IconButtonVariant type with constants: IconButtonVariantNeutral="neutral",
|
||||
IconButtonVariantWarning="warning", IconButtonVariantSuccess="success",
|
||||
IconButtonVariantDanger="danger".
|
||||
f. Add IconButtonTone type with constants: IconButtonToneSolid="solid", IconButtonToneGhost="ghost".
|
||||
g. Add SpacingStep type with constants: SpacingStepXS="xs", SpacingStepSM="sm",
|
||||
SpacingStepMD="md", SpacingStepLG="lg", SpacingStepXL="xl".
|
||||
h. Add NormalizedIconButtonVariant function (defaults to IconButtonVariantNeutral).
|
||||
i. Add NormalizedIconButtonTone function (defaults to IconButtonToneSolid).
|
||||
j. Add NormalizedSpacingStep function (defaults to SpacingStepMD).
|
||||
k. Add IconButtonClass(variant IconButtonVariant, tone IconButtonTone) string function.
|
||||
Ghost tone path: returns "borderless-icon-button ui-icon-button-ghost ui-icon-button-" + normalizedVariant.
|
||||
Solid tone path: returns "ui-icon-button ui-icon-button-solid ui-icon-button-" + normalizedVariant.
|
||||
l. Add SpaceXClass(step SpacingStep) string — returns "ui-space-x ui-space-x-" + normalizedStep.
|
||||
m. Add SpaceYClass(step SpacingStep) string — returns "ui-space-y ui-space-y-" + normalizedStep.
|
||||
|
||||
Step 3 — Update helpers.go (GREEN):
|
||||
Add four new unexported helper functions (following the existing mergeAttrs pattern):
|
||||
a. buttonType(value string) string — returns "button" if value is empty, otherwise value.
|
||||
b. inputType(value string) string — returns "text" if value is empty, otherwise value.
|
||||
c. inputID(id string, name string) string — returns id if non-empty, otherwise name.
|
||||
d. textareaRows(rows int) string — returns strconv.Itoa(rows) if rows > 0, else "4".
|
||||
Add import "strconv" to helpers.go.
|
||||
|
||||
Do NOT change ButtonClass() output yet — that is Plan 02's work.
|
||||
Run go test ./internal/web/ui/... and confirm all tests pass (GREEN).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd backend && go test ./internal/web/ui/... -run "TestButtonVariantGhost|TestBadgeVariantPrimary|TestIconButtonClass|TestSpaceXClass|TestSpaceYClass" -v</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- All 8 new test functions pass
|
||||
- go test ./internal/web/ui/... (full suite) is green — no existing tests broken
|
||||
- variants.go contains "ButtonVariantGhost ButtonVariant = "ghost""
|
||||
- variants.go contains "BadgeVariantPrimary BadgeVariant = "primary""
|
||||
- variants.go contains type IconButtonVariant string
|
||||
- variants.go contains type SpacingStep string
|
||||
- variants.go contains func IconButtonClass
|
||||
- variants.go contains func SpaceXClass
|
||||
- helpers.go imports "strconv"
|
||||
- helpers.go contains func buttonType
|
||||
- helpers.go contains func textareaRows
|
||||
- ButtonClass() still returns the OLD compound pattern (ui-button-solid-default-md) — this is intentional; Plan 02 migrates it
|
||||
</acceptance_criteria>
|
||||
<done>variants.go extended with all new enums and class functions; helpers.go extended with component helper functions; all tests green</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Static build → browser | CSS compiled by Tailwind CLI; no runtime input |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-13-01-01 | Information Disclosure | base.css token values | accept | Token values are design constants (brand colors, spacing). No secrets. Acceptable public visibility. |
|
||||
| T-13-01-02 | Tampering | variants.go normalizer | accept | Normalizers return safe defaults for unknown inputs; no user-controlled data flows through variant enums at runtime. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After this plan completes:
|
||||
- cd backend && go test ./internal/web/ui/... — must be green
|
||||
- grep -c 'color-brand-primary' backend/internal/web/ui/base.css — must return >= 3
|
||||
- grep 'ButtonVariantGhost' backend/internal/web/ui/variants.go — must match
|
||||
- grep 'auth-provider-button' backend/internal/web/ui/auth.css — must match
|
||||
- grep 'auth\.css' backend/tailwind.input.css — must match
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. base.css contains the full go-backend token vocabulary (223 lines, --color-brand-primary: #804eec present)
|
||||
2. auth.css exists with .auth-provider-button and is imported in tailwind.input.css
|
||||
3. variants.go has ButtonVariantGhost, BadgeVariantPrimary, IconButtonVariant, IconButtonTone, SpacingStep
|
||||
4. variants.go has IconButtonClass(), SpaceXClass(), SpaceYClass() functions
|
||||
5. helpers.go has buttonType(), inputType(), inputID(), textareaRows()
|
||||
6. go test ./internal/web/ui/... is green
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/13-design-system-foundation/13-01-SUMMARY.md`
|
||||
</output>
|
||||
314
.planning/phases/13-design-system-foundation/13-02-PLAN.md
Normal file
314
.planning/phases/13-design-system-foundation/13-02-PLAN.md
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
---
|
||||
phase: 13-design-system-foundation
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 13-01
|
||||
files_modified:
|
||||
- backend/internal/web/ui/button.css
|
||||
- backend/internal/web/ui/button.templ
|
||||
- backend/internal/web/ui/badge.css
|
||||
- backend/internal/web/ui/badge.templ
|
||||
- backend/internal/web/ui/card.css
|
||||
- backend/internal/web/ui/card.templ
|
||||
- backend/internal/web/ui/variants.go
|
||||
- backend/internal/web/ui/ui_test.go
|
||||
- backend/templates/planning.templ
|
||||
- backend/templates/tasks.templ
|
||||
- backend/templates/events.templ
|
||||
- backend/templates/etapes.templ
|
||||
autonomous: true
|
||||
requirements:
|
||||
- DS-02
|
||||
- DS-04
|
||||
- DS-05
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "ButtonClass() emits four separate classes: ui-button, ui-button-{tone}, ui-button-{variant}, ui-button-{size}"
|
||||
- "All hardcoded compound button class strings in templates are updated to multi-class equivalents"
|
||||
- "ButtonVariantGhost class combo renders with transparent background and brand color text"
|
||||
- "BadgeVariantPrimary renders with brand-purple surface"
|
||||
- "Card uses typed Header/Body/Footer templ.Component fields (not children passthrough)"
|
||||
- "go test ./... passes — no regressions in any package"
|
||||
- "just generate succeeds — all templ files compile"
|
||||
artifacts:
|
||||
- path: "backend/internal/web/ui/button.css"
|
||||
provides: "Multi-class button selectors + ghost variant"
|
||||
contains: ".ui-button-solid.ui-button-default {"
|
||||
- path: "backend/internal/web/ui/card.templ"
|
||||
provides: "Typed Header/Body/Footer Props API"
|
||||
contains: "CardProps"
|
||||
- path: "backend/internal/web/ui/variants.go"
|
||||
provides: "ButtonClass() emitting multi-class output"
|
||||
contains: "ui-button-"
|
||||
key_links:
|
||||
- from: "backend/templates/planning.templ"
|
||||
to: "multi-class button pattern"
|
||||
via: "class attribute strings"
|
||||
pattern: "ui-button ui-button-soft ui-button-neutral"
|
||||
- from: "backend/internal/web/ui/button.templ"
|
||||
to: "ButtonClass()"
|
||||
via: "ButtonClass(props.Variant, props.Tone, props.Size)"
|
||||
pattern: "ButtonClass"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** developer, **I want to** migrate existing Button/Badge/Card components to go-backend's API (multi-class button pattern, typed Card fields, new variants), **so that** buttons render correctly from the ported button.css and templates remain functional with no visual regressions.
|
||||
|
||||
<objective>
|
||||
Migrate the three existing components (Button, Badge, Card) to match go-backend's API and CSS.
|
||||
The critical task is the button multi-class migration — both the CSS selectors and the class
|
||||
generation function must change atomically, and all hardcoded compound class strings in templates
|
||||
must be updated in the same wave.
|
||||
|
||||
Purpose: This wave closes the gap between the current compound-class pattern and go-backend's
|
||||
multi-class pattern. Without this, porting button.css (which uses compound CSS selectors like
|
||||
.ui-button-solid.ui-button-default) would silently break all button rendering.
|
||||
|
||||
Output:
|
||||
- button.css replaced with go-backend's multi-class selector version + new ghost rules
|
||||
- ButtonClass() updated to emit "ui-button ui-button-solid ui-button-default ui-button-md"
|
||||
- button.templ updated with Icon field and buttonType() helper (from Plan 01 helpers.go)
|
||||
- badge.css replaced with go-backend's pill-shape version + new primary variant
|
||||
- card.css replaced with go-backend's token-based version
|
||||
- card.templ migrated from children to typed Header/Body/Footer fields
|
||||
- All compound class strings in planning.templ, tasks.templ, events.templ, etapes.templ updated
|
||||
- ui_test.go updated to match new patterns (multi-class assertions, card typed API)
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/13-design-system-foundation/13-CONTEXT.md
|
||||
@.planning/phases/13-design-system-foundation/13-RESEARCH.md
|
||||
@.planning/phases/13-design-system-foundation/13-PATTERNS.md
|
||||
@.planning/phases/13-design-system-foundation/13-01-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
From backend/internal/web/ui/variants.go (after Plan 01):
|
||||
func ButtonClass(variant ButtonVariant, tone ButtonTone, size Size) string
|
||||
// Currently returns: "ui-button ui-button-solid-default-md" — Plan 02 changes this output
|
||||
// Target: "ui-button ui-button-solid ui-button-default ui-button-md"
|
||||
|
||||
From backend/internal/web/ui/helpers.go (after Plan 01):
|
||||
func buttonType(value string) string // returns "button" if empty
|
||||
func UIIcon(kind string) templ.Component // does NOT exist yet — created in Plan 04
|
||||
|
||||
Compound class → multi-class mapping (all occurrences to replace in templates):
|
||||
"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"
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Update ButtonClass() multi-class output + button.templ + update ui_test.go button assertions</name>
|
||||
<files>
|
||||
backend/internal/web/ui/variants.go,
|
||||
backend/internal/web/ui/button.templ,
|
||||
backend/internal/web/ui/ui_test.go
|
||||
</files>
|
||||
<read_first>
|
||||
- backend/internal/web/ui/variants.go (current — confirm ButtonClass() still uses old pattern after Plan 01)
|
||||
- backend/internal/web/ui/button.templ (current — full file)
|
||||
- backend/internal/web/ui/ui_test.go (current — find TestButton_DefaultSolidMD, TestButtonClass_String assertions to update)
|
||||
- go-backend/internal/web/ui/button.templ (source for updated Props struct with Icon field)
|
||||
- 13-PATTERNS.md (button.templ target pattern section)
|
||||
- 13-RESEARCH.md (Multi-Class Button Pattern section, Code Examples section)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test: ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD) == "ui-button ui-button-solid ui-button-default ui-button-md"
|
||||
- Test: ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) contains "ui-button-ghost" and does NOT contain "ui-button-solid"
|
||||
- Test: Button(ButtonProps{Label: "x"}) renders class attribute containing "ui-button ui-button-solid" (not "ui-button-solid-default-md")
|
||||
</behavior>
|
||||
<action>
|
||||
Step 1 — Update the test assertions in ui_test.go (they will now FAIL — this is intentional RED state):
|
||||
a. TestButton_DefaultSolidMD: change wantClass from "ui-button ui-button-solid-default-md"
|
||||
to a for-loop checking for each of: "ui-button", "ui-button-solid", "ui-button-default", "ui-button-md"
|
||||
using the multi-assertion slice pattern from 13-PATTERNS.md.
|
||||
b. TestButtonClass_String: change want from "ui-button ui-button-solid-default-md"
|
||||
to "ui-button ui-button-solid ui-button-default ui-button-md".
|
||||
Run go test ./internal/web/ui/... — confirm these two tests FAIL (RED).
|
||||
|
||||
Step 2 — Update ButtonClass() in variants.go (GREEN):
|
||||
Change the return statement from:
|
||||
"ui-button ui-button-" + string(t) + "-" + string(v) + "-" + string(s)
|
||||
to:
|
||||
"ui-button ui-button-" + string(t) + " ui-button-" + string(v) + " ui-button-" + string(s)
|
||||
The ghost variant is a special case: when variant is ButtonVariantGhost, the tone class is
|
||||
omitted. Add a conditional: if v == ButtonVariantGhost, return "ui-button ui-button-ghost ui-button-" + string(s).
|
||||
|
||||
Step 3 — Update button.templ:
|
||||
a. Add "Icon string" field to ButtonProps struct (after the Size field).
|
||||
b. Replace the inline btnType variable logic with: type={ buttonType(props.Type) }.
|
||||
c. Replace the class variable lookup with: class={ ButtonClass(props.Variant, props.Tone, props.Size) }.
|
||||
d. Add icon rendering inside the button: if props.Icon != "" { <span class="ui-button-icon"> — leave
|
||||
a placeholder comment "// UIIcon added in Plan 04" because UIIcon does not exist yet.
|
||||
DO NOT reference UIIcon yet — it does not compile until Plan 04. Instead, skip the icon rendering
|
||||
entirely in the templ for now: the Icon field is present in the struct, but icon rendering is
|
||||
wired in Plan 04 when icon_button.templ creates UIIcon.
|
||||
|
||||
Run go test ./internal/web/ui/... — confirm all tests pass (GREEN).
|
||||
Run cd backend && just generate — confirm templ generates without errors.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd backend && just generate && go test ./internal/web/ui/... -run "TestButton" -v</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD) returns "ui-button ui-button-solid ui-button-default ui-button-md" (not the old compound form)
|
||||
- ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) returns "ui-button ui-button-ghost ui-button-md"
|
||||
- TestButton_DefaultSolidMD passes with multi-class assertions
|
||||
- TestButtonClass_String passes with new expected string
|
||||
- button.templ uses buttonType(props.Type) helper
|
||||
- ButtonProps struct contains "Icon string" field
|
||||
- just generate succeeds (no templ compile errors)
|
||||
- All other existing tests still pass (TestButton_PassesThroughAttrs, TestButton_ExplicitTypeSubmit)
|
||||
</acceptance_criteria>
|
||||
<done>ButtonClass() emits multi-class output; button.templ updated with Icon field and buttonType helper; all button tests green</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Replace button.css + badge.css + card.css; migrate card.templ; update template hardcodes; extend ui_test.go</name>
|
||||
<files>
|
||||
backend/internal/web/ui/button.css,
|
||||
backend/internal/web/ui/badge.css,
|
||||
backend/internal/web/ui/card.css,
|
||||
backend/internal/web/ui/card.templ,
|
||||
backend/internal/web/ui/ui_test.go,
|
||||
backend/templates/planning.templ,
|
||||
backend/templates/tasks.templ,
|
||||
backend/templates/events.templ,
|
||||
backend/templates/etapes.templ
|
||||
</files>
|
||||
<read_first>
|
||||
- backend/internal/web/ui/button.css (current — 180 lines; lines 1–120 are button CSS, lines 121–180 are auth-provider CSS that was extracted in Plan 01 but the file still needs full review)
|
||||
- backend/internal/web/ui/badge.css (current — see current selectors)
|
||||
- backend/internal/web/ui/card.css (current — see current selectors)
|
||||
- backend/internal/web/ui/card.templ (current — children-based API to replace)
|
||||
- backend/internal/web/ui/ui_test.go (current — TestCard_RendersChildren to rewrite)
|
||||
- go-backend/internal/web/ui/button.css (source for multi-class selectors)
|
||||
- go-backend/internal/web/ui/badge.css (source for pill shape + token colors)
|
||||
- go-backend/internal/web/ui/card.css (source for header/body/footer pattern)
|
||||
- go-backend/internal/web/ui/card.templ (source for typed Props API)
|
||||
- 13-PATTERNS.md (button.css, badge.css, card.css, card.templ, template hardcode sections)
|
||||
- 13-RESEARCH.md (Pitfall 1 — template compound class strings; Pitfall 3 — Card API break)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test (rewritten): Card(CardProps{Body: textComponent("hello")}) renders "ui-card-body" in HTML and "hello" inside it
|
||||
- Test (new): BadgeClass(BadgeVariantPrimary) returns "ui-badge ui-badge-primary"
|
||||
- Test (new): Badge(BadgeProps{Label: "x", Variant: BadgeVariantPrimary}) HTML contains "ui-badge-primary"
|
||||
</behavior>
|
||||
<action>
|
||||
Step 1 — Rewrite TestCard_RendersChildren in ui_test.go to TestCard_RendersTypedRegions:
|
||||
Add textComponent helper function (from 13-PATTERNS.md) and add imports "io".
|
||||
New test passes CardProps{Header: textComponent("header"), Body: textComponent("body")}
|
||||
and asserts "ui-card-header", "header", "ui-card-body", "body" are all in the output.
|
||||
Add test for nil footer: CardProps{Body: textComponent("x")} must NOT contain "ui-card-footer".
|
||||
Add TestBadge_PrimaryVariant: Badge(BadgeProps{Label: "x", Variant: BadgeVariantPrimary})
|
||||
must contain "ui-badge-primary".
|
||||
Run go test — these will fail RED (card.templ not yet migrated, PrimaryVariant normalizer not yet applied to BadgeClass).
|
||||
|
||||
Step 2 — Replace button.css with go-backend's multi-class version:
|
||||
Port go-backend/internal/web/ui/button.css verbatim (multi-class selectors like
|
||||
".ui-button-solid.ui-button-default { background: var(--color-brand-primary); }").
|
||||
Do NOT include the auth-provider selectors (those are in auth.css from Plan 01).
|
||||
After the go-backend content, append the ghost variant rules (per 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; }
|
||||
|
||||
Step 3 — Replace badge.css with go-backend's version:
|
||||
Port go-backend/internal/web/ui/badge.css verbatim (pill shape, border-radius: 999px).
|
||||
After the go-backend content, append the primary variant (per D-CA02):
|
||||
.ui-badge-primary { background: var(--color-surface-brand-soft); border-color: rgba(128, 78, 236, 0.3); color: var(--color-text-brand); }
|
||||
|
||||
Step 4 — Replace card.css with go-backend's version:
|
||||
Port go-backend/internal/web/ui/card.css verbatim (ui-card, ui-card-header, ui-card-body, ui-card-footer).
|
||||
|
||||
Step 5 — Migrate card.templ to typed Props API:
|
||||
Replace the current children-based Card(attrs templ.Attributes) with:
|
||||
type CardProps struct { Header templ.Component; Body templ.Component; Footer templ.Component }
|
||||
templ Card(props CardProps) — with nil-guard conditionals for each region (see 13-PATTERNS.md).
|
||||
|
||||
Step 6 — Update compound class strings in templates. For each file, replace all occurrences:
|
||||
planning.templ: "ui-button ui-button-soft-neutral-md" → "ui-button ui-button-soft ui-button-neutral ui-button-md",
|
||||
"ui-button ui-button-solid-default-md" → "ui-button ui-button-solid ui-button-default ui-button-md"
|
||||
tasks.templ: "ui-button ui-button-soft-danger-md flex-shrink-0 text-xs" → "ui-button ui-button-soft ui-button-danger ui-button-md flex-shrink-0 text-xs",
|
||||
"ui-button ui-button-soft-neutral-md w-full text-left text-sm mt-2" → "ui-button ui-button-soft ui-button-neutral ui-button-md w-full text-left text-sm mt-2"
|
||||
events.templ: "ui-button ui-button-soft-neutral-md" → "ui-button ui-button-soft ui-button-neutral ui-button-md"
|
||||
etapes.templ: "ui-button ui-button-soft-neutral-md px-2" → "ui-button ui-button-soft ui-button-neutral ui-button-md px-2",
|
||||
"ui-button ui-button-soft-danger-md px-2" → "ui-button ui-button-soft ui-button-danger ui-button-md px-2",
|
||||
"ui-button ui-button-soft-neutral-md flex-shrink-0" → "ui-button ui-button-soft ui-button-neutral ui-button-md flex-shrink-0"
|
||||
Do NOT strip Tailwind utility classes that follow the button classes — keep them as-is.
|
||||
|
||||
Run just generate && go test ./... — all tests must pass GREEN.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd backend && just generate && go test ./... -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- go test ./... passes with no failures
|
||||
- just generate produces no errors
|
||||
- button.css contains ".ui-button-solid.ui-button-default {" (multi-class compound selector)
|
||||
- button.css contains ".ui-button-ghost {" (new ghost variant)
|
||||
- button.css does NOT contain ".auth-provider-button" (that is in auth.css)
|
||||
- badge.css contains "border-radius: 999px" (pill shape from go-backend)
|
||||
- badge.css contains ".ui-badge-primary {"
|
||||
- card.css contains ".ui-card-header,"
|
||||
- card.templ contains "type CardProps struct"
|
||||
- card.templ contains "if props.Header != nil"
|
||||
- planning.templ contains "ui-button ui-button-soft ui-button-neutral ui-button-md" and does NOT contain "ui-button-soft-neutral-md"
|
||||
- tasks.templ does NOT contain "ui-button-soft-danger-md"
|
||||
- TestCard_RendersTypedRegions passes (card renders typed regions correctly)
|
||||
- TestBadge_PrimaryVariant passes (ui-badge-primary in output)
|
||||
</acceptance_criteria>
|
||||
<done>button.css/badge.css/card.css replaced; card.templ migrated to typed API; all template compound class strings updated; full test suite green</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Template HTML → browser | templ auto-escapes string interpolations; CSS class changes are compile-time constants |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-13-02-01 | Tampering | card.templ typed API migration | accept | No user input flows through CardProps fields at runtime; templ auto-escapes all string interpolations |
|
||||
| T-13-02-02 | Information Disclosure | button.css ghost variant | accept | CSS is a public static asset; ghost variant is visual styling only, no sensitive data |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After this plan completes:
|
||||
- cd backend && just generate — must succeed (no templ errors)
|
||||
- cd backend && go test ./... -count=1 — must be green across all packages
|
||||
- grep -r 'ui-button-soft-neutral-md\|ui-button-solid-default-md\|ui-button-soft-danger-md' backend/templates/ — must return nothing
|
||||
- grep 'ui-button-solid.ui-button-default' backend/internal/web/ui/button.css — must match
|
||||
- grep 'ui-badge-primary' backend/internal/web/ui/badge.css — must match
|
||||
- grep 'type CardProps struct' backend/internal/web/ui/card.templ — must match
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. ButtonClass() emits "ui-button ui-button-solid ui-button-default ui-button-md" (multi-class)
|
||||
2. button.css uses multi-class compound selectors, includes ghost variant, excludes auth-provider CSS
|
||||
3. badge.css is pill-shape, includes primary variant
|
||||
4. card.templ uses typed Header/Body/Footer CardProps (no children passthrough)
|
||||
5. All 4 template files have multi-class button strings (no compound "ui-button-solid-default-md")
|
||||
6. Full test suite (go test ./...) is green
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/13-design-system-foundation/13-02-SUMMARY.md`
|
||||
</output>
|
||||
317
.planning/phases/13-design-system-foundation/13-03-PLAN.md
Normal file
317
.planning/phases/13-design-system-foundation/13-03-PLAN.md
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
---
|
||||
phase: 13-design-system-foundation
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on:
|
||||
- 13-01
|
||||
- 13-02
|
||||
files_modified:
|
||||
- backend/internal/web/ui/input.css
|
||||
- backend/internal/web/ui/input.templ
|
||||
- backend/internal/web/ui/textarea.css
|
||||
- backend/internal/web/ui/textarea.templ
|
||||
- backend/internal/web/ui/select.css
|
||||
- backend/internal/web/ui/select.templ
|
||||
- backend/internal/web/ui/select_helpers.go
|
||||
- backend/internal/web/ui/form-field.css
|
||||
- backend/internal/web/ui/form_field.templ
|
||||
- backend/internal/web/ui/ui_test.go
|
||||
autonomous: true
|
||||
requirements:
|
||||
- DS-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Input component renders with class ui-input and respects ID/Name/Placeholder/Type props"
|
||||
- "Textarea component renders with class ui-textarea and respects Rows default (4 when 0)"
|
||||
- "Select component renders with class ui-select-control, inline JS for open/close, and HTMX re-init listener"
|
||||
- "FormField wraps any component with label, hint, and error regions"
|
||||
- "All four CSS files exist and contain their primary .ui-* selectors"
|
||||
- "go test ./internal/web/ui/... passes for all new component tests"
|
||||
- "just generate succeeds — all new .templ files compile"
|
||||
artifacts:
|
||||
- path: "backend/internal/web/ui/input.templ"
|
||||
provides: "Input component with InputProps"
|
||||
contains: "type InputProps struct"
|
||||
- path: "backend/internal/web/ui/textarea.templ"
|
||||
provides: "Textarea component with TextareaProps"
|
||||
contains: "type TextareaProps struct"
|
||||
- path: "backend/internal/web/ui/select.templ"
|
||||
provides: "Select component with inline JS"
|
||||
contains: "SelectProps"
|
||||
- path: "backend/internal/web/ui/select_helpers.go"
|
||||
provides: "Select helper functions"
|
||||
contains: "selectPlaceholder"
|
||||
- path: "backend/internal/web/ui/form_field.templ"
|
||||
provides: "FormField wrapper component"
|
||||
contains: "type FormFieldProps struct"
|
||||
key_links:
|
||||
- from: "backend/internal/web/ui/select.templ"
|
||||
to: "select_helpers.go"
|
||||
via: "selectPlaceholder(), selectOptionSelected() calls"
|
||||
pattern: "selectPlaceholder"
|
||||
- from: "backend/internal/web/ui/textarea.templ"
|
||||
to: "helpers.go"
|
||||
via: "textareaRows(), inputID() calls"
|
||||
pattern: "textareaRows"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** developer, **I want to** have Input, Textarea, Select, and FormField components available as templ components with matching CSS, **so that** form-heavy views in Phases 14–17 can use these components instead of raw HTML form elements.
|
||||
|
||||
<objective>
|
||||
Port all four form-input component types from go-backend to backend. Each component delivers
|
||||
a complete vertical slice: CSS file → templ Props struct → templ rendering → test coverage.
|
||||
|
||||
Purpose: These are the form primitives that all subsequent phases need. Auth pages (Phase 14)
|
||||
need Input and FormField. Tablo create/edit dialogs (Phase 15-16) need all four.
|
||||
|
||||
Output:
|
||||
- input.css + input.templ (update existing stub to match go-backend API)
|
||||
- textarea.css + textarea.templ (new files — port from go-backend)
|
||||
- select.css + select.templ + select_helpers.go (new files — complex, includes inline JS)
|
||||
- form-field.css + form_field.templ (new files — wrapper component)
|
||||
- ui_test.go extended with tests for all four component types
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/13-design-system-foundation/13-CONTEXT.md
|
||||
@.planning/phases/13-design-system-foundation/13-RESEARCH.md
|
||||
@.planning/phases/13-design-system-foundation/13-PATTERNS.md
|
||||
@.planning/phases/13-design-system-foundation/13-01-SUMMARY.md
|
||||
@.planning/phases/13-design-system-foundation/13-02-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
From backend/internal/web/ui/helpers.go (after Plan 01):
|
||||
func inputType(value string) string // returns "text" if empty
|
||||
func inputID(id string, name string) string // returns id if non-empty, else name
|
||||
func textareaRows(rows int) string // returns "4" if rows <= 0
|
||||
|
||||
From go-backend/internal/web/ui/select_helpers.go (source to port):
|
||||
func selectPlaceholder(props SelectProps) string
|
||||
func selectNativeID(id, name string) string
|
||||
func selectMenuID(id, name string) string
|
||||
func selectBoolData(b bool) string
|
||||
func selectOptionSelected(option SelectOption, values []string) bool
|
||||
func selectSelectedLabels(props SelectProps) string
|
||||
func selectSelectedLabel(props SelectProps) string
|
||||
func selectMenuOptionClass(option SelectOption, values []string) string
|
||||
func selectIsDisabled(props SelectProps, option SelectOption) bool
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Port input.templ, input.css, textarea.templ, textarea.css + tests</name>
|
||||
<files>
|
||||
backend/internal/web/ui/input.css,
|
||||
backend/internal/web/ui/input.templ,
|
||||
backend/internal/web/ui/textarea.css,
|
||||
backend/internal/web/ui/textarea.templ,
|
||||
backend/internal/web/ui/ui_test.go
|
||||
</files>
|
||||
<read_first>
|
||||
- backend/internal/web/ui/input.templ (existing stub — see current InputProps fields to understand what is already there)
|
||||
- go-backend/internal/web/ui/input.templ (source Props struct and rendering pattern)
|
||||
- go-backend/internal/web/ui/input.css (source CSS — port verbatim)
|
||||
- go-backend/internal/web/ui/textarea.templ (source Props struct and rendering pattern)
|
||||
- go-backend/internal/web/ui/textarea.css (source CSS — port verbatim)
|
||||
- 13-PATTERNS.md (input.templ, textarea.templ, input.css, textarea.css sections)
|
||||
- 13-RESEARCH.md (Props Struct Alignment Detail — Input and Textarea sections)
|
||||
- 13-UI-SPEC.md (Input and Textarea component inventory sections)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test: Input(InputProps{Name: "email", Type: "email"}) HTML contains 'class="ui-input"' and 'type="email"'
|
||||
- Test: Input(InputProps{Name: "x"}) HTML contains 'type="text"' (default via inputType helper)
|
||||
- Test: Input(InputProps{ID: "my-id", Name: "x"}) HTML contains 'id="my-id"'
|
||||
- Test: Input(InputProps{Name: "x"}) with no ID — HTML contains 'id="x"' (inputID fallback to name)
|
||||
- Test: Textarea(TextareaProps{Name: "body"}) HTML contains 'class="ui-textarea"'
|
||||
- Test: Textarea(TextareaProps{Name: "x", Rows: 0}) HTML contains 'rows="4"' (default via textareaRows)
|
||||
- Test: Textarea(TextareaProps{Name: "x", Rows: 6}) HTML contains 'rows="6"'
|
||||
</behavior>
|
||||
<action>
|
||||
Step 1 — Write failing tests in ui_test.go (RED):
|
||||
Add TestInput_DefaultType, TestInput_EmailType, TestInput_IDFallback,
|
||||
TestTextarea_DefaultRows, TestTextarea_ExplicitRows.
|
||||
Run go test ./internal/web/ui/... — confirm failures.
|
||||
|
||||
Step 2 — Create/update input.css: port go-backend/internal/web/ui/input.css verbatim.
|
||||
The file must contain .ui-input { } selector with: appearance: none, background, border,
|
||||
border-radius: 0.75rem, color, font: inherit, line-height: 1.4, min-height: 44px,
|
||||
padding: 0.75rem 0.95rem, width: 100%.
|
||||
Strip any page-level selectors (body, :root).
|
||||
|
||||
Step 3 — Update input.templ to match go-backend's Props API:
|
||||
InputProps must have: Name string, ID string, Type string, Placeholder string, Value string, Attrs templ.Attributes.
|
||||
The templ function uses: inputID(props.ID, props.Name) for id, inputType(props.Type) for type.
|
||||
Per D-CA03 (Claude's discretion on props alignment) and UI-SPEC explicit fields: also add
|
||||
Disabled bool and Required bool with conditional attribute rendering:
|
||||
if props.Disabled { disabled } and if props.Required { required } in the <input> element.
|
||||
|
||||
Step 4 — Create textarea.css: port go-backend/internal/web/ui/textarea.css verbatim.
|
||||
Must contain .ui-textarea { } with: same appearance/border/border-radius/color/font as input,
|
||||
min-height: 7rem, resize: vertical, width: 100%.
|
||||
|
||||
Step 5 — Create textarea.templ: new file in backend/internal/web/ui/textarea.templ.
|
||||
package ui declaration at top. TextareaProps must have: Name string, ID string,
|
||||
Placeholder string, Value string, Rows int, Disabled bool, Required bool, Attrs templ.Attributes.
|
||||
The templ function renders <textarea> using inputID(props.ID, props.Name),
|
||||
textareaRows(props.Rows), and { props.Attrs... }.
|
||||
|
||||
Run just generate && go test ./internal/web/ui/... — all tests GREEN.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd backend && just generate && go test ./internal/web/ui/... -run "TestInput|TestTextarea" -v</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- TestInput_DefaultType: HTML contains 'type="text"'
|
||||
- TestInput_EmailType: HTML contains 'type="email"'
|
||||
- TestInput_IDFallback: HTML contains 'id="email"' when no explicit ID but Name="email"
|
||||
- TestTextarea_DefaultRows: HTML contains 'rows="4"'
|
||||
- TestTextarea_ExplicitRows: HTML contains 'rows="6"'
|
||||
- input.css contains ".ui-input {" and "min-height: 44px"
|
||||
- textarea.css contains ".ui-textarea {" and "min-height: 7rem" and "resize: vertical"
|
||||
- input.templ contains "type InputProps struct" with Name, ID, Type, Placeholder, Value, Disabled, Required, Attrs fields
|
||||
- textarea.templ contains "type TextareaProps struct" with Name, ID, Placeholder, Value, Rows, Disabled, Required, Attrs fields
|
||||
- just generate succeeds
|
||||
- go test ./internal/web/ui/... is fully green
|
||||
</acceptance_criteria>
|
||||
<done>input.css/input.templ created; textarea.css/textarea.templ created; all input/textarea tests passing</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Port select.templ + select_helpers.go + select.css + form_field.templ + form-field.css + tests</name>
|
||||
<files>
|
||||
backend/internal/web/ui/select.css,
|
||||
backend/internal/web/ui/select.templ,
|
||||
backend/internal/web/ui/select_helpers.go,
|
||||
backend/internal/web/ui/form-field.css,
|
||||
backend/internal/web/ui/form_field.templ,
|
||||
backend/internal/web/ui/ui_test.go
|
||||
</files>
|
||||
<read_first>
|
||||
- go-backend/internal/web/ui/select.templ (full file — port verbatim including inline script block)
|
||||
- go-backend/internal/web/ui/select_helpers.go (full file — port verbatim)
|
||||
- go-backend/internal/web/ui/select.css (full file — port verbatim)
|
||||
- go-backend/internal/web/ui/form_field.templ (full file — port verbatim)
|
||||
- go-backend/internal/web/ui/form-field.css (full file — port verbatim)
|
||||
- 13-PATTERNS.md (select.templ section, select_helpers.go section, form_field.templ section)
|
||||
- 13-RESEARCH.md (Select Component — Inline JavaScript section, Pitfall 6)
|
||||
- 13-UI-SPEC.md (Select and Form Field component inventory sections)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test: Select(SelectProps{Name: "status", Options: []SelectOption{{Value: "a", Label: "Alpha"}}}) HTML contains "ui-select-control"
|
||||
- Test: Select renders a <script> block containing "__uiSelectInitAll" (inline JS for open/close)
|
||||
- Test: Select renders htmx:afterSwap listener reference in the script block
|
||||
- Test: FormField(FormFieldProps{Label: "Name", ForID: "name-input"}) HTML contains "ui-form-field" and "ui-form-label"
|
||||
- Test: FormField with Error set renders "ui-form-error" in HTML
|
||||
- Test: FormField with empty Error does NOT render "ui-form-error"
|
||||
</behavior>
|
||||
<action>
|
||||
Step 1 — Write failing tests in ui_test.go (RED):
|
||||
Add TestSelect_RendersControl, TestSelect_HasInlineScript,
|
||||
TestFormField_RendersLabel, TestFormField_RendersError, TestFormField_NoErrorWhenEmpty.
|
||||
Run go test ./internal/web/ui/... — confirm failures.
|
||||
|
||||
Step 2 — Create select_helpers.go: port go-backend/internal/web/ui/select_helpers.go verbatim.
|
||||
File must be in package ui. Import "strings" and "github.com/a-h/templ".
|
||||
Port all 8 helper functions verbatim: selectPlaceholder, selectNativeID, selectMenuID,
|
||||
selectBoolData, selectOptionSelected, selectSelectedLabels, selectSelectedLabel,
|
||||
selectMenuOptionClass, selectIsDisabled.
|
||||
|
||||
Step 3 — Create select.templ: port go-backend/internal/web/ui/select.templ verbatim.
|
||||
SelectOption struct: Value string, Label string, Disabled bool.
|
||||
SelectProps struct: ID string, Name string, Placeholder string, Value string, Values []string,
|
||||
Multiple bool, Options []SelectOption, Attrs templ.Attributes.
|
||||
The inline <script> block MUST be included verbatim — it contains window.__uiSelectInitAll
|
||||
and the document.addEventListener("htmx:afterSwap", ...) listener (Pitfall 6).
|
||||
Use SelectProps and SelectOption from the local package (matching go-backend structure).
|
||||
|
||||
Step 4 — Create select.css: port go-backend/internal/web/ui/select.css verbatim.
|
||||
Must contain .ui-select { }, .ui-select-control { } (min-height: 44px, border-radius: 0.75rem),
|
||||
.ui-select-menu { } (absolute positioned, max-height: 16rem, overflow-y: auto).
|
||||
|
||||
Step 5 — Create form-field.css: port go-backend/internal/web/ui/form-field.css verbatim.
|
||||
Must contain .ui-form-field { }, .ui-form-label { }, .ui-form-hint { }, .ui-form-error { }.
|
||||
|
||||
Step 6 — Create form_field.templ: port go-backend/internal/web/ui/form_field.templ.
|
||||
FormFieldProps struct: Label string, For string, Field templ.Component, Error string, Hint string.
|
||||
Render <label for={props.For} class="ui-form-label"> only if Label is not empty.
|
||||
Render @props.Field if not nil.
|
||||
Render .ui-form-hint only if Hint is not empty.
|
||||
Render .ui-form-error only if Error is not empty.
|
||||
|
||||
Run just generate && go test ./internal/web/ui/... — all GREEN.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd backend && just generate && go test ./internal/web/ui/... -run "TestSelect|TestFormField" -v</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- TestSelect_RendersControl passes (HTML contains "ui-select-control")
|
||||
- TestSelect_HasInlineScript passes (HTML contains "__uiSelectInitAll")
|
||||
- TestFormField_RendersLabel passes (HTML contains "ui-form-label")
|
||||
- TestFormField_RendersError passes (HTML contains "ui-form-error" when Error is set)
|
||||
- TestFormField_NoErrorWhenEmpty passes (HTML does NOT contain "ui-form-error" when Error is "")
|
||||
- select_helpers.go contains func selectPlaceholder
|
||||
- select_helpers.go contains func selectOptionSelected
|
||||
- select.templ contains "type SelectProps struct"
|
||||
- select.templ contains "__uiSelectInitAll" (inline script present)
|
||||
- select.templ contains "htmx:afterSwap" (re-init listener present)
|
||||
- form_field.templ contains "type FormFieldProps struct"
|
||||
- form_field.templ contains "if props.Error != \"\""
|
||||
- form-field.css contains ".ui-form-field {"
|
||||
- form-field.css contains ".ui-form-error {"
|
||||
- just generate succeeds
|
||||
- Full go test ./internal/web/ui/... is green
|
||||
</acceptance_criteria>
|
||||
<done>select.css/select.templ/select_helpers.go created; form-field.css/form_field.templ created; all tests passing</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Browser → select inline script | The select inline script reads DOM attributes only; it does not fetch URLs or eval strings |
|
||||
| Form field props → HTML output | templ auto-escapes all string interpolations in FormFieldProps (Label, Hint, Error are escaped) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-13-03-01 | Tampering | select.templ inline script | accept | Script only toggles CSS classes (is-open) and the hidden attribute on the dropdown; no user-controlled string is eval'd; script content is static and compiled into the binary |
|
||||
| T-13-03-02 | Tampering | FormField Error prop | accept | Error strings are server-controlled (validation messages from handler); templ auto-escapes any HTML in the string, preventing XSS injection through error messages |
|
||||
| T-13-03-03 | Information Disclosure | Input Disabled/Required props | accept | These are static HTML attributes rendered server-side; no user-controlled data in field metadata |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After this plan completes:
|
||||
- cd backend && just generate — must succeed
|
||||
- cd backend && go test ./internal/web/ui/... -count=1 — must be green
|
||||
- grep -l 'ui-input\|ui-textarea\|ui-select\|ui-form-field' backend/internal/web/ui/*.css — must list 4 files
|
||||
- grep '__uiSelectInitAll' backend/internal/web/ui/select.templ — must match (inline JS present)
|
||||
- grep 'htmx:afterSwap' backend/internal/web/ui/select.templ — must match (re-init listener present)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. Input component: class="ui-input", inputType/inputID helpers, Disabled/Required bool fields
|
||||
2. Textarea component: class="ui-textarea", textareaRows default 4, resize: vertical in CSS
|
||||
3. Select component: ui-select-control, inline JS with __uiSelectInitAll and htmx:afterSwap listener
|
||||
4. FormField component: wraps Field component, conditional label/hint/error regions
|
||||
5. All four component CSS files exist with their primary .ui-* selectors
|
||||
6. go test ./internal/web/ui/... is green for all new test functions
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/13-design-system-foundation/13-03-SUMMARY.md`
|
||||
</output>
|
||||
364
.planning/phases/13-design-system-foundation/13-04-PLAN.md
Normal file
364
.planning/phases/13-design-system-foundation/13-04-PLAN.md
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
---
|
||||
phase: 13-design-system-foundation
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on:
|
||||
- 13-01
|
||||
- 13-02
|
||||
files_modified:
|
||||
- backend/internal/web/ui/modal.css
|
||||
- backend/internal/web/ui/modal.templ
|
||||
- backend/internal/web/ui/empty-state.css
|
||||
- backend/internal/web/ui/empty_state.templ
|
||||
- backend/internal/web/ui/table.css
|
||||
- backend/internal/web/ui/table.templ
|
||||
- backend/internal/web/ui/icon-button.css
|
||||
- backend/internal/web/ui/icon_button.templ
|
||||
- backend/internal/web/ui/spacing.css
|
||||
- backend/internal/web/ui/space.templ
|
||||
- backend/internal/web/ui/button.templ
|
||||
- backend/internal/web/ui/ui_test.go
|
||||
- backend/tailwind.input.css
|
||||
autonomous: true
|
||||
requirements:
|
||||
- DS-06
|
||||
- DS-07
|
||||
- DS-08
|
||||
- DS-09
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Modal component renders ui-modal-backdrop and ui-modal-panel with typed Title/Body/Actions props"
|
||||
- "EmptyState component renders ui-empty-state with Icon (templ.Component), Title, Description regions"
|
||||
- "Table component renders ui-table-shell wrapping ui-table with typed Head/Body props"
|
||||
- "IconButton component renders borderless-icon-button for ghost tone, ui-icon-button for solid tone"
|
||||
- "UIIcon templ function renders inline SVG for recognized icon names"
|
||||
- "SpaceX/SpaceY utility components render with SpacingStep CSS classes"
|
||||
- "button.templ Icon field is wired to UIIcon (completing the Plan 02 TODO)"
|
||||
- "tailwind.input.css imports all 13 component CSS files"
|
||||
- "go test ./internal/web/ui/... is green for all new component tests"
|
||||
- "just generate succeeds"
|
||||
artifacts:
|
||||
- path: "backend/internal/web/ui/icon_button.templ"
|
||||
provides: "IconButton + UIIcon templ components"
|
||||
contains: "templ UIIcon"
|
||||
- path: "backend/internal/web/ui/modal.templ"
|
||||
provides: "Modal component with backdrop and panel"
|
||||
contains: "type ModalProps struct"
|
||||
- path: "backend/internal/web/ui/empty_state.templ"
|
||||
provides: "EmptyState component with Icon templ.Component"
|
||||
contains: "type EmptyStateProps struct"
|
||||
- path: "backend/internal/web/ui/table.templ"
|
||||
provides: "Table component with Head/Body"
|
||||
contains: "ui-table-shell"
|
||||
- path: "backend/tailwind.input.css"
|
||||
provides: "Full CSS import manifest"
|
||||
contains: "spacing.css"
|
||||
key_links:
|
||||
- from: "backend/internal/web/ui/button.templ"
|
||||
to: "backend/internal/web/ui/icon_button.templ"
|
||||
via: "@UIIcon(props.Icon)"
|
||||
pattern: "UIIcon"
|
||||
- from: "backend/tailwind.input.css"
|
||||
to: "all 13 CSS files"
|
||||
via: "@import"
|
||||
pattern: "spacing\\.css"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** developer, **I want to** have Modal, EmptyState, Table, IconButton, and Space components available, and all CSS files imported in tailwind.input.css, **so that** the entire component library is complete and Tailwind compiles every component's CSS into the final stylesheet.
|
||||
|
||||
<objective>
|
||||
Port the five remaining component types (modal, empty-state, table, icon-button, space) from
|
||||
go-backend. Also wire the UIIcon function into button.templ (completing the Icon field from Plan 02),
|
||||
and update tailwind.input.css with all 13 component imports.
|
||||
|
||||
Purpose: This plan closes DS-06 through DS-09 and makes the full component library available.
|
||||
After this plan, Plans 03 and 04 together deliver all 11 component types with CSS + templ + tests.
|
||||
The tailwind.input.css update here ensures Tailwind compiles the complete stylesheet.
|
||||
|
||||
Output:
|
||||
- modal.css + modal.templ (new files)
|
||||
- empty-state.css + empty_state.templ (new files)
|
||||
- table.css + table.templ (new files)
|
||||
- icon-button.css + icon_button.templ (UIIcon included) (new files)
|
||||
- spacing.css + space.templ (new files)
|
||||
- button.templ updated to wire @UIIcon(props.Icon)
|
||||
- tailwind.input.css updated with all 13 CSS imports
|
||||
- ui_test.go extended with 5 new component tests
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/13-design-system-foundation/13-CONTEXT.md
|
||||
@.planning/phases/13-design-system-foundation/13-RESEARCH.md
|
||||
@.planning/phases/13-design-system-foundation/13-PATTERNS.md
|
||||
@.planning/phases/13-design-system-foundation/13-01-SUMMARY.md
|
||||
@.planning/phases/13-design-system-foundation/13-02-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
From backend/internal/web/ui/variants.go (after Plan 01):
|
||||
type IconButtonVariant string — constants: IconButtonVariantNeutral, Warning, Success, Danger
|
||||
type IconButtonTone string — constants: IconButtonToneSolid, IconButtonToneGhost
|
||||
type SpacingStep string — constants: SpacingStepXS, SM, MD, LG, XL
|
||||
func IconButtonClass(variant IconButtonVariant, tone IconButtonTone) string
|
||||
// Ghost: "borderless-icon-button ui-icon-button-ghost ui-icon-button-{variant}"
|
||||
// Solid: "ui-icon-button ui-icon-button-solid ui-icon-button-{variant}"
|
||||
func SpaceXClass(step SpacingStep) string — "ui-space-x ui-space-x-{step}"
|
||||
func SpaceYClass(step SpacingStep) string — "ui-space-y ui-space-y-{step}"
|
||||
|
||||
From backend/internal/web/ui/helpers.go (after Plan 01):
|
||||
func buttonType(value string) string
|
||||
|
||||
UIIcon (being created in this plan):
|
||||
templ UIIcon(kind string) — switch on kind, renders inline SVG for: plus, grid3x3, list, filter,
|
||||
search, calendar, pencil, trash. Default: <span aria-hidden="true">{ kind }</span>
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Port modal, empty-state, table components + CSS + tests</name>
|
||||
<files>
|
||||
backend/internal/web/ui/modal.css,
|
||||
backend/internal/web/ui/modal.templ,
|
||||
backend/internal/web/ui/empty-state.css,
|
||||
backend/internal/web/ui/empty_state.templ,
|
||||
backend/internal/web/ui/table.css,
|
||||
backend/internal/web/ui/table.templ,
|
||||
backend/internal/web/ui/ui_test.go
|
||||
</files>
|
||||
<read_first>
|
||||
- go-backend/internal/web/ui/modal.templ (source Props struct and rendering)
|
||||
- go-backend/internal/web/ui/modal.css (source CSS)
|
||||
- go-backend/internal/web/ui/empty_state.templ (source Props struct — note Icon is templ.Component)
|
||||
- go-backend/internal/web/ui/empty-state.css (source CSS)
|
||||
- go-backend/internal/web/ui/table.templ (source Props struct)
|
||||
- go-backend/internal/web/ui/table.css (source CSS)
|
||||
- 13-PATTERNS.md (modal.templ, empty_state.templ, table.templ, and their CSS sections)
|
||||
- 13-RESEARCH.md (Pitfall 7 — modal backdrop in catalog)
|
||||
- 13-UI-SPEC.md (Modal, Empty State, Table component inventory sections)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test: Modal(ModalProps{Title: "Confirm action"}) HTML contains "ui-modal-backdrop" and "ui-modal-panel"
|
||||
- Test: Modal(ModalProps{Title: "Confirm action"}) HTML contains "Confirm action" in a heading element
|
||||
- Test: Modal with nil Body does NOT render "ui-modal-body"
|
||||
- Test: EmptyState(EmptyStateProps{Title: "Nothing here yet"}) HTML contains "ui-empty-state" and "Nothing here yet"
|
||||
- Test: EmptyState with nil Icon does NOT render "ui-empty-state-icon"
|
||||
- Test: Table(TableProps{}) HTML contains "ui-table-shell" and "ui-table"
|
||||
</behavior>
|
||||
<action>
|
||||
Step 1 — Write failing tests in ui_test.go (RED):
|
||||
Add TestModal_RendersBackdropAndPanel, TestModal_RendersTitle, TestModal_NilBodyOmitted,
|
||||
TestEmptyState_RendersTitle, TestEmptyState_NilIconOmitted, TestTable_RendersShell.
|
||||
The textComponent helper from Plan 02's ui_test.go changes is already in scope.
|
||||
Run go test ./internal/web/ui/... — confirm failures.
|
||||
|
||||
Step 2 — Create modal.css: port go-backend/internal/web/ui/modal.css verbatim.
|
||||
Must contain .ui-modal-backdrop { position: fixed; inset: 0; ... flex center },
|
||||
.ui-modal-panel { max-width: 32rem; border-radius: 1rem; ... },
|
||||
.ui-modal-header { font-size: 1.125rem; font-weight: 600; ... },
|
||||
.ui-modal-body { ... }, .ui-modal-actions { ... }.
|
||||
|
||||
Step 3 — Create modal.templ: port go-backend/internal/web/ui/modal.templ.
|
||||
ModalProps struct: Title string, Body templ.Component, Actions templ.Component.
|
||||
Render structure: <div class="ui-modal-backdrop"> → <div class="ui-modal-panel"> →
|
||||
<div class="ui-modal-header"><h2>{ props.Title }</h2></div> →
|
||||
nil-guarded Body region → nil-guarded Actions region.
|
||||
|
||||
Step 4 — Create empty-state.css: port go-backend/internal/web/ui/empty-state.css verbatim.
|
||||
Must contain .ui-empty-state { border: 1px dashed var(--color-border-subtle); border-radius: 1rem; ... },
|
||||
.ui-empty-state-icon { width: 4rem; height: 4rem; ... },
|
||||
.ui-empty-state-title { font-size: 1.125rem; font-weight: 600; }.
|
||||
|
||||
Step 5 — Create empty_state.templ: port go-backend/internal/web/ui/empty_state.templ.
|
||||
EmptyStateProps struct: Icon templ.Component (not string — callers pass UIIcon(...)),
|
||||
Title string, Description string, Action templ.Component.
|
||||
Render .ui-empty-state-icon region only if props.Icon != nil.
|
||||
Render .ui-empty-state-description only if Description != "".
|
||||
Render .ui-empty-state-action only if Action != nil.
|
||||
|
||||
Step 6 — Create table.css: port go-backend/internal/web/ui/table.css verbatim.
|
||||
Must contain .ui-table-shell { overflow-x: auto; width: 100%; },
|
||||
.ui-table { border-collapse: collapse; min-width: 100%; }.
|
||||
|
||||
Step 7 — Create table.templ: port go-backend/internal/web/ui/table.templ.
|
||||
TableProps struct: Head templ.Component, Body templ.Component.
|
||||
Render structure: <div class="ui-table-shell"> → <table class="ui-table"> →
|
||||
<thead> nil-guarded Head → <tbody> nil-guarded Body.
|
||||
|
||||
Run just generate && go test ./internal/web/ui/... — all GREEN.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd backend && just generate && go test ./internal/web/ui/... -run "TestModal|TestEmptyState|TestTable" -v</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- TestModal_RendersBackdropAndPanel passes
|
||||
- TestModal_RendersTitle passes (HTML contains title text in heading)
|
||||
- TestModal_NilBodyOmitted passes (no "ui-modal-body" when Body is nil)
|
||||
- TestEmptyState_RendersTitle passes
|
||||
- TestEmptyState_NilIconOmitted passes
|
||||
- TestTable_RendersShell passes (HTML contains "ui-table-shell" and "ui-table")
|
||||
- modal.css contains ".ui-modal-backdrop {"
|
||||
- modal.css contains ".ui-modal-panel {"
|
||||
- empty-state.css contains ".ui-empty-state {"
|
||||
- empty-state.css contains ".ui-empty-state-icon {"
|
||||
- table.css contains ".ui-table-shell {"
|
||||
- empty_state.templ contains "type EmptyStateProps struct"
|
||||
- empty_state.templ Icon field is type "templ.Component" (not string)
|
||||
- just generate succeeds
|
||||
- Full go test ./internal/web/ui/... is green
|
||||
</acceptance_criteria>
|
||||
<done>modal, empty-state, and table components ported with CSS and tests; all passing</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Port icon-button + UIIcon + space components; wire button.templ Icon; update tailwind.input.css</name>
|
||||
<files>
|
||||
backend/internal/web/ui/icon-button.css,
|
||||
backend/internal/web/ui/icon_button.templ,
|
||||
backend/internal/web/ui/spacing.css,
|
||||
backend/internal/web/ui/space.templ,
|
||||
backend/internal/web/ui/button.templ,
|
||||
backend/internal/web/ui/ui_test.go,
|
||||
backend/tailwind.input.css
|
||||
</files>
|
||||
<read_first>
|
||||
- go-backend/internal/web/ui/icon_button.templ (full file — UIIcon switch with all icon cases)
|
||||
- go-backend/internal/web/ui/icon-button.css (source CSS)
|
||||
- go-backend/internal/web/ui/space.templ (source)
|
||||
- go-backend/internal/web/ui/spacing.css (source CSS)
|
||||
- backend/internal/web/ui/button.templ (current after Plan 02 — find the Icon placeholder comment)
|
||||
- backend/tailwind.input.css (current state — need to add 9 new imports)
|
||||
- 13-PATTERNS.md (icon_button.templ, space.templ, tailwind.input.css sections)
|
||||
- 13-RESEARCH.md (IconButtonClass code example; tailwind.input.css Final State section)
|
||||
- 13-UI-SPEC.md (Icon Button and CSS File Manifest sections)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test: IconButton(IconButtonProps{Icon: "plus", Variant: IconButtonVariantNeutral, Tone: IconButtonToneGhost, Label: "Add"}) HTML contains "borderless-icon-button" and "aria-label=\"Add\""
|
||||
- Test: IconButton(IconButtonProps{Icon: "trash", Variant: IconButtonVariantDanger, Tone: IconButtonToneSolid, Label: "Delete"}) HTML contains "ui-icon-button-solid" and "ui-icon-button-danger"
|
||||
- Test: UIIcon("plus") renders an <svg> element
|
||||
- Test: UIIcon("unknown-kind") renders a <span> fallback
|
||||
- Test: SpaceX(SpaceProps{Size: SpacingStepMD}) HTML contains "ui-space-x-md"
|
||||
- Test: SpaceY(SpaceProps{Size: SpacingStepLG}) HTML contains "ui-space-y-lg"
|
||||
- Test: Button with Icon set renders an <svg> element (via UIIcon wiring)
|
||||
</behavior>
|
||||
<action>
|
||||
Step 1 — Write failing tests in ui_test.go (RED):
|
||||
Add TestIconButton_GhostNeutral, TestIconButton_SolidDanger, TestUIIcon_Plus, TestUIIcon_Fallback,
|
||||
TestSpaceX_MD, TestSpaceY_LG, TestButton_IconRendered.
|
||||
Run go test ./internal/web/ui/... — confirm failures.
|
||||
|
||||
Step 2 — Create icon-button.css: port go-backend/internal/web/ui/icon-button.css verbatim.
|
||||
Must contain .ui-icon-button { min-height: 44px; min-width: 44px; ... },
|
||||
.ui-icon-button-solid { ... }, .borderless-icon-button { ... },
|
||||
.ui-icon-button-ghost { ... }, and variant tone/color rules.
|
||||
|
||||
Step 3 — Create icon_button.templ: port go-backend/internal/web/ui/icon_button.templ.
|
||||
IconButtonProps struct: Label string, Icon string, Variant IconButtonVariant, Tone IconButtonTone,
|
||||
Type string, Attrs templ.Attributes.
|
||||
IconButton templ: renders <button type={buttonType(props.Type)} class={IconButtonClass(props.Variant, props.Tone)} aria-label={props.Label} {props.Attrs...}>@UIIcon(props.Icon)</button>.
|
||||
UIIcon templ function: switch on kind with cases for: "plus", "grid3x3", "list", "filter",
|
||||
"search", "calendar", "pencil", "trash". Each case renders the inline SVG from go-backend.
|
||||
Default case: <span aria-hidden="true">{ kind }</span>.
|
||||
Note: UIIcon is defined in icon_button.templ (same file as IconButton) — this is the go-backend pattern.
|
||||
|
||||
Step 4 — Create spacing.css: port go-backend/internal/web/ui/spacing.css verbatim.
|
||||
Must contain .ui-space-x-xs, .ui-space-x-sm, .ui-space-x-md, .ui-space-x-lg, .ui-space-x-xl
|
||||
and matching .ui-space-y-* classes.
|
||||
|
||||
Step 5 — Create space.templ: port go-backend/internal/web/ui/space.templ.
|
||||
SpaceProps struct: Size SpacingStep.
|
||||
SpaceX templ: renders <span class={SpaceXClass(props.Size)} aria-hidden="true"></span>.
|
||||
SpaceY templ: renders <div class={SpaceYClass(props.Size)} aria-hidden="true"></div>.
|
||||
|
||||
Step 6 — Wire UIIcon into button.templ:
|
||||
Find the placeholder comment "// UIIcon added in Plan 04" in button.templ (added in Plan 02 Task 1).
|
||||
Replace it with the actual icon rendering:
|
||||
if props.Icon != "" {
|
||||
<span class="ui-button-icon">@UIIcon(props.Icon)</span>
|
||||
}
|
||||
UIIcon is now available because icon_button.templ is in the same package.
|
||||
|
||||
Step 7 — Update backend/tailwind.input.css to match the final state from 13-RESEARCH.md:
|
||||
The complete import order after base.css + auth.css: button.css, badge.css, card.css,
|
||||
input.css, textarea.css, select.css, modal.css, empty-state.css, table.css,
|
||||
icon-button.css, form-field.css, spacing.css.
|
||||
Per D-A02: do NOT add an app.css import.
|
||||
|
||||
Run just generate && go test ./internal/web/ui/... — all GREEN.
|
||||
Run just generate && go test ./... — full suite GREEN.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd backend && just generate && go test ./... -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- TestIconButton_GhostNeutral passes (HTML contains "borderless-icon-button")
|
||||
- TestIconButton_SolidDanger passes (HTML contains "ui-icon-button-solid" and "ui-icon-button-danger")
|
||||
- TestUIIcon_Plus passes (HTML contains "<svg")
|
||||
- TestUIIcon_Fallback passes (HTML contains "<span" fallback for unknown kind)
|
||||
- TestSpaceX_MD passes (HTML contains "ui-space-x-md")
|
||||
- TestSpaceY_LG passes (HTML contains "ui-space-y-lg")
|
||||
- TestButton_IconRendered passes (Button with Icon="plus" renders an SVG)
|
||||
- icon_button.templ contains "templ UIIcon(kind string)"
|
||||
- icon_button.templ contains 'case "plus"' (icon switch)
|
||||
- button.templ contains "@UIIcon(props.Icon)" (wired icon rendering)
|
||||
- spacing.css contains ".ui-space-x-md {"
|
||||
- tailwind.input.css contains "@import "./internal/web/ui/spacing.css";"
|
||||
- tailwind.input.css contains "@import "./internal/web/ui/icon-button.css";"
|
||||
- tailwind.input.css contains all 13 component CSS imports (count: grep -c '@import.*web/ui' backend/tailwind.input.css should return 14 — base + auth + 12 components)
|
||||
- just generate succeeds
|
||||
- go test ./... is fully green
|
||||
</acceptance_criteria>
|
||||
<done>icon-button/space components ported; UIIcon wired into button.templ; tailwind.input.css updated with all 13 imports; full test suite green</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| UIIcon switch → HTML | Icon names are developer-supplied string constants (not user input at runtime); inline SVGs are static Go source |
|
||||
| Modal backdrop → browser DOM | Backdrop uses position:fixed — this is expected for production use; the catalog renders panel-only (Pitfall 7 mitigation applied in Plan 05) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-13-04-01 | Tampering | UIIcon default fallback | accept | The fallback renders { kind } which is escaped by templ; no XSS via icon name string |
|
||||
| T-13-04-02 | Information Disclosure | icon-button.css | accept | CSS is a public static asset; no sensitive information in icon button styling rules |
|
||||
| T-13-04-03 | Elevation of Privilege | catalog route (tailwind.input.css update) | accept | tailwind.input.css controls CSS only — no route registration, no handler; catalog route security is handled by Plan 05 build-tag gating |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After this plan completes:
|
||||
- cd backend && just generate — must succeed
|
||||
- cd backend && go test ./... -count=1 — must be green across all packages
|
||||
- grep -c '@import.*web/ui' backend/tailwind.input.css — must return 14 (base + auth + 12 components)
|
||||
- grep 'templ UIIcon' backend/internal/web/ui/icon_button.templ — must match
|
||||
- grep '@UIIcon' backend/internal/web/ui/button.templ — must match (icon wired)
|
||||
- grep 'ui-table-shell' backend/internal/web/ui/table.css — must match
|
||||
- grep 'ui-empty-state' backend/internal/web/ui/empty-state.css — must match
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. Modal, EmptyState, Table, IconButton (with UIIcon), Space components all exist with CSS + templ
|
||||
2. UIIcon renders inline SVG for: plus, grid3x3, list, filter, search, calendar, pencil, trash
|
||||
3. button.templ Icon field renders via @UIIcon
|
||||
4. tailwind.input.css has all 13 CSS file imports (base, auth, button, badge, card, input, textarea, select, modal, empty-state, table, icon-button, form-field, spacing)
|
||||
5. All DS-06/07/08/09 component tests pass
|
||||
6. Full go test ./... is green
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/13-design-system-foundation/13-04-SUMMARY.md`
|
||||
</output>
|
||||
312
.planning/phases/13-design-system-foundation/13-05-PLAN.md
Normal file
312
.planning/phases/13-design-system-foundation/13-05-PLAN.md
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
---
|
||||
phase: 13-design-system-foundation
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on:
|
||||
- 13-03
|
||||
- 13-04
|
||||
files_modified:
|
||||
- backend/internal/web/ui/catalog/catalog.templ
|
||||
- backend/internal/web/ui/catalog/examples.go
|
||||
- backend/internal/web/catalog_route_catalog.go
|
||||
- backend/internal/web/catalog_route_stub.go
|
||||
- backend/internal/web/router.go
|
||||
- backend/justfile
|
||||
autonomous: false
|
||||
requirements:
|
||||
- DS-01
|
||||
- DS-02
|
||||
- DS-03
|
||||
- DS-04
|
||||
- DS-05
|
||||
- DS-06
|
||||
- DS-07
|
||||
- DS-08
|
||||
- DS-09
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "go run -tags catalog ./cmd/web serves GET /ui-catalog and returns HTTP 200"
|
||||
- "The catalog page renders all 11 component sections with section headings matching DS-XX requirement IDs"
|
||||
- "The default (no -tags catalog) build does not include the catalog route"
|
||||
- "The developer can visually verify all component variants before Phase 14 begins"
|
||||
- "go build ./... (without -tags catalog) succeeds — no undefined symbol linker errors"
|
||||
- "go test ./... passes — no regressions"
|
||||
artifacts:
|
||||
- path: "backend/internal/web/ui/catalog/catalog.templ"
|
||||
provides: "Single-page catalog layout with sidebar nav and component sections"
|
||||
contains: "Component Catalog"
|
||||
- path: "backend/internal/web/ui/catalog/examples.go"
|
||||
provides: "Typed Example structs for each component variant"
|
||||
contains: "buttonExamples"
|
||||
- path: "backend/internal/web/catalog_route_catalog.go"
|
||||
provides: "Build-tagged catalog route registration"
|
||||
contains: "//go:build catalog"
|
||||
- path: "backend/internal/web/catalog_route_stub.go"
|
||||
provides: "No-op stub for production builds"
|
||||
contains: "//go:build !catalog"
|
||||
key_links:
|
||||
- from: "backend/internal/web/router.go"
|
||||
to: "RegisterCatalogRoute"
|
||||
via: "unconditional call in NewRouter"
|
||||
pattern: "RegisterCatalogRoute"
|
||||
- from: "backend/internal/web/catalog_route_catalog.go"
|
||||
to: "backend/internal/web/ui/catalog/catalog.templ"
|
||||
via: "catalogPageHandler() templ.Handler render"
|
||||
pattern: "catalog"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** developer, **I want to** run `just catalog` and view all 11 component types rendered with every variant on a single page, **so that** I can visually sign off on token values and component shapes before Phase 14 begins applying the design system to real app views.
|
||||
|
||||
<objective>
|
||||
Create the build-tag-gated /ui-catalog route with a single-page component catalog.
|
||||
The catalog shows all component variants with their templ call annotations.
|
||||
A human checkpoint at the end gates progression to Phase 14.
|
||||
|
||||
Purpose: Visual sign-off is the final gate before Phases 14–17 apply the design system
|
||||
to real app views. The catalog makes component shapes and token choices visible before
|
||||
any per-view work begins.
|
||||
|
||||
Output:
|
||||
- backend/internal/web/ui/catalog/catalog.templ (layout + section rendering)
|
||||
- backend/internal/web/ui/catalog/examples.go (typed examples per component)
|
||||
- backend/internal/web/catalog_route_catalog.go (//go:build catalog)
|
||||
- backend/internal/web/catalog_route_stub.go (//go:build !catalog)
|
||||
- router.go wired with unconditional RegisterCatalogRoute call
|
||||
- justfile catalog target added
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/13-design-system-foundation/13-CONTEXT.md
|
||||
@.planning/phases/13-design-system-foundation/13-RESEARCH.md
|
||||
@.planning/phases/13-design-system-foundation/13-PATTERNS.md
|
||||
@.planning/phases/13-design-system-foundation/13-UI-SPEC.md
|
||||
@.planning/phases/13-design-system-foundation/13-03-SUMMARY.md
|
||||
@.planning/phases/13-design-system-foundation/13-04-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
From backend/internal/web/router.go:
|
||||
func NewRouter(pinger Pinger, staticFS fs.FS, deps AuthDeps, tabloDeps TablosDeps,
|
||||
taskDeps TasksDeps, etapeDeps EtapesDeps, eventDeps EventsDeps,
|
||||
discussionDeps DiscussionDeps, planningDeps PlanningDeps, fileDeps FilesDeps,
|
||||
csrfKey []byte, env string, trustedOrigins ...string) (http.Handler, error)
|
||||
// Add RegisterCatalogRoute(r) call inside this function, after all protected routes.
|
||||
|
||||
Build tag file pair pattern:
|
||||
catalog_route_catalog.go — //go:build catalog — defines RegisterCatalogRoute(r chi.Router) that mounts handler
|
||||
catalog_route_stub.go — //go:build !catalog — defines RegisterCatalogRoute(r chi.Router) {} no-op
|
||||
|
||||
Catalog layout (from 13-UI-SPEC.md):
|
||||
Sidebar 240px fixed, right: main content fluid
|
||||
Section order: alphabetical — badge, button, card, empty-state, form-field, icon-button, input, modal, select, table, textarea
|
||||
Each section heading: "{ComponentName} — {DS-req}" (e.g. "Button — DS-02")
|
||||
Modal section renders panel-only (no backdrop wrapper — Pitfall 7)
|
||||
|
||||
Catalog copywriting (from 13-UI-SPEC.md):
|
||||
Page title: "Component Catalog"
|
||||
Nav heading: "Components"
|
||||
Empty state demo title: "Nothing here yet"
|
||||
Empty state demo body: "Add your first item to get started."
|
||||
Modal demo title: "Confirm action"
|
||||
Form error demo: "This field is required."
|
||||
Form hint demo: "Enter a value between 1 and 100."
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create catalog package (catalog.templ + examples.go + route files + router wiring)</name>
|
||||
<files>
|
||||
backend/internal/web/ui/catalog/catalog.templ,
|
||||
backend/internal/web/ui/catalog/examples.go,
|
||||
backend/internal/web/catalog_route_catalog.go,
|
||||
backend/internal/web/catalog_route_stub.go,
|
||||
backend/internal/web/router.go
|
||||
</files>
|
||||
<read_first>
|
||||
- go-backend/internal/web/ui/catalog/catalog.templ (structure reference — go-backend uses static file generation but the templ layout structure is the model)
|
||||
- go-backend/internal/web/ui/catalog/examples.go (Example struct pattern and per-component example functions)
|
||||
- backend/internal/web/router.go (find where to insert RegisterCatalogRoute call — after all protected routes, before healthz handler if any)
|
||||
- 13-PATTERNS.md (catalog.templ, examples.go, catalog_route_catalog.go + stub sections)
|
||||
- 13-RESEARCH.md (Catalog Route — Build Tag Pattern section, Pitfall 4, Pitfall 7)
|
||||
- 13-UI-SPEC.md (Catalog Page Contract section — layout, section order, copywriting)
|
||||
</read_first>
|
||||
<action>
|
||||
Step 1 — Create backend/internal/web/ui/catalog/ directory and catalog.templ:
|
||||
Package declaration: "package catalog".
|
||||
The catalog page uses a fake shell with Tailwind utilities (flex, min-h-screen, w-60, etc.)
|
||||
— no import of real app.css (per D-A04).
|
||||
Layout: two-column flex with sidebar (w-60, fixed, border-r) and main content area (flex-1, p-8).
|
||||
Sidebar nav: heading "Components" + anchor links in alphabetical order:
|
||||
#badge, #button, #card, #empty-state, #form-field, #icon-button, #input, #modal, #select, #table, #textarea.
|
||||
Each section in main content: <section id="{slug}"> with <h2>{ComponentName} — {DS-req}</h2>
|
||||
followed by the component examples from the examples list.
|
||||
Each example: rendered component + <pre><code> block showing the Snippet string.
|
||||
Modal section: render the ui-modal-panel div directly without the ui-modal-backdrop wrapper
|
||||
(Pitfall 7 — backdrop position:fixed would overlay the page).
|
||||
Page title: <title>Component Catalog</title>.
|
||||
Import path for ui components: "backend/internal/web/ui".
|
||||
|
||||
Step 2 — Create backend/internal/web/ui/catalog/examples.go:
|
||||
Package declaration: "package catalog".
|
||||
Define type Example struct { Title string; Preview templ.Component; Snippet string }.
|
||||
Define example functions for each component:
|
||||
- badgeExamples() — returns examples for info, warning, success, danger, primary variants
|
||||
- buttonExamples() — returns examples for solid/default, solid/danger, soft/neutral, ghost variants (all size MD)
|
||||
- cardExamples() — returns one example with body content using textBody helper
|
||||
- emptyStateExamples() — returns one example with title "Nothing here yet", description "Add your first item to get started."
|
||||
- formFieldExamples() — returns example with Label, Hint, Error using "This field is required." and "Enter a value between 1 and 100."
|
||||
- iconButtonExamples() — returns examples for ghost/neutral (plus icon) and solid/danger (trash icon)
|
||||
- inputExamples() — returns examples for text type, email type, with placeholder
|
||||
- modalExamples() — returns one example rendering ModalProps{Title: "Confirm action"} BUT wraps it to render only the ui-modal-panel (not the backdrop)
|
||||
- selectExamples() — returns one example with a few options
|
||||
- tableExamples() — returns one example with a simple head/body
|
||||
- textareaExamples() — returns one example with placeholder and rows
|
||||
|
||||
Add a componentFunc helper (adapted from go-backend examples.go):
|
||||
func componentFunc(f func(ctx context.Context, w io.Writer) error) templ.Component.
|
||||
Imports: "context", "io", "github.com/a-h/templ", "backend/internal/web/ui".
|
||||
|
||||
Step 3 — Create backend/internal/web/catalog_route_catalog.go:
|
||||
First line: //go:build catalog
|
||||
Package declaration: package web
|
||||
Import: "github.com/go-chi/chi/v5"
|
||||
Define RegisterCatalogRoute(r chi.Router) that calls r.Get("/ui-catalog", catalogPageHandler()).
|
||||
Define catalogPageHandler() http.HandlerFunc that renders catalog.templ using templ.Handler.
|
||||
Import path for catalog: "backend/internal/web/ui/catalog".
|
||||
|
||||
Step 4 — Create backend/internal/web/catalog_route_stub.go:
|
||||
First line: //go:build !catalog
|
||||
Package declaration: package web
|
||||
Import: "github.com/go-chi/chi/v5"
|
||||
Define RegisterCatalogRoute(r chi.Router) {} // no-op in production builds.
|
||||
|
||||
Step 5 — Update backend/internal/web/router.go:
|
||||
Add a single unconditional call RegisterCatalogRoute(r) inside NewRouter, after all authenticated
|
||||
routes are registered and before the function returns. In the non-catalog build, the stub's
|
||||
no-op satisfies the symbol — no linker error.
|
||||
|
||||
Run: cd backend && go build ./... — must succeed (without -tags catalog — stub satisfies symbol).
|
||||
Run: cd backend && go build -tags catalog ./... — must succeed (catalog build).
|
||||
Run: cd backend && just generate && go test ./... -count=1 — must be green.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd backend && go build ./... && go build -tags catalog ./... && just generate && go test ./... -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- go build ./... (without catalog tag) succeeds — no linker errors
|
||||
- go build -tags catalog ./... succeeds
|
||||
- just generate && go test ./... -count=1 is green
|
||||
- backend/internal/web/catalog_route_catalog.go starts with "//go:build catalog"
|
||||
- backend/internal/web/catalog_route_stub.go starts with "//go:build !catalog"
|
||||
- router.go contains "RegisterCatalogRoute(r)"
|
||||
- catalog.templ contains "Component Catalog"
|
||||
- catalog.templ contains each section anchor: "#badge", "#button", "#card", "#input"
|
||||
- examples.go contains "func buttonExamples()"
|
||||
- examples.go contains "func modalExamples()"
|
||||
- The modal example does NOT use "ui-modal-backdrop" as a wrapper (renders panel-only)
|
||||
</acceptance_criteria>
|
||||
<done>Catalog package created; route files registered; router wired; build succeeds with and without catalog tag; tests green</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 2: Visual sign-off — run catalog and verify all 11 component sections</name>
|
||||
<what-built>
|
||||
A dev-only catalog route at /ui-catalog that renders all 11 component types with all variants.
|
||||
The catalog is gated by //go:build catalog so it never ships in production.
|
||||
The justfile has a "catalog" target that runs: just generate && go run -tags catalog ./cmd/web
|
||||
Developer visits http://localhost:8080/ui-catalog to review all components.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Run the catalog server:
|
||||
cd backend && just generate && go run -tags catalog ./cmd/web
|
||||
(or: just catalog if the target is wired)
|
||||
|
||||
2. Visit http://localhost:8080/ui-catalog in a browser.
|
||||
|
||||
3. Verify the sidebar shows all 11 section links in alphabetical order:
|
||||
badge, button, card, empty-state, form-field, icon-button, input, modal, select, table, textarea
|
||||
|
||||
4. For each section, verify:
|
||||
- Section heading includes the DS requirement number (e.g. "Button — DS-02")
|
||||
- All declared variants are rendered (e.g. button: solid/default, solid/danger, soft/neutral, ghost)
|
||||
- Each variant shows a templ call annotation below the rendered component
|
||||
|
||||
5. Verify specific components:
|
||||
- Button: ghost variant renders with transparent background (no colored fill)
|
||||
- Badge: primary variant renders with purple-tinted background (different from info blue)
|
||||
- Card: header and body regions render as separate bordered sections
|
||||
- Modal: modal panel is visible (no full-page backdrop overlay)
|
||||
- EmptyState: icon circle + title + description render correctly
|
||||
- Select: dropdown control renders with chevron indicator
|
||||
- IconButton: ghost variant has no background, solid variant has filled background
|
||||
|
||||
6. Check browser console — no JavaScript errors.
|
||||
|
||||
7. Confirm token values look correct:
|
||||
- Brand purple (#804eec) on primary buttons and ghost button text
|
||||
- Pill-shaped badges with subtle colored borders
|
||||
- Cards with rounded corners and shadow
|
||||
|
||||
8. Type "approved" if all 11 sections look correct, or describe any issues to address.
|
||||
</how-to-verify>
|
||||
<resume-signal>
|
||||
Type "approved" to proceed to Phase 14, or describe specific visual issues (component,
|
||||
variant, observed vs expected appearance) so they can be fixed before Phase 14 begins.
|
||||
</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Catalog route → production binary | //go:build catalog tag ensures the route is never compiled into production; production binary built without -tags catalog |
|
||||
| Catalog page → browser | All catalog content is developer-authored static data (no user input rendered on catalog page) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-13-05-01 | Information Disclosure | /ui-catalog route in production | mitigate | //go:build catalog + //go:build !catalog stub ensures the route symbol is a no-op in default builds. Production deployment uses go build ./... (no catalog tag). Verify with: go build ./... followed by strings ./cmd/web/main-binary | grep ui-catalog — must return nothing. |
|
||||
| T-13-05-02 | Elevation of Privilege | catalog route in dev | accept | Catalog is dev-only; no auth required is intentional for a local development tool; serves static component previews only, no user data |
|
||||
| T-13-05-03 | Information Disclosure | examples.go static strings | accept | Demo strings ("Nothing here yet", "Confirm action", etc.) are hard-coded display text; no secrets, no user data |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After this plan completes (pre-checkpoint):
|
||||
- cd backend && go build ./... — must succeed (no catalog tag, stub satisfies symbol)
|
||||
- cd backend && go build -tags catalog ./... — must succeed
|
||||
- cd backend && go test ./... -count=1 — must be green
|
||||
- grep 'RegisterCatalogRoute' backend/internal/web/router.go — must match
|
||||
- grep '^//go:build catalog$' backend/internal/web/catalog_route_catalog.go — must match
|
||||
- grep '^//go:build !catalog$' backend/internal/web/catalog_route_stub.go — must match
|
||||
|
||||
After checkpoint approval:
|
||||
- Developer has visually confirmed all 11 component sections on /ui-catalog
|
||||
- Token values (brand purple, badge tones, card shadow) match expectations
|
||||
- Phase 14 can begin
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. go build ./... and go build -tags catalog ./... both succeed
|
||||
2. go test ./... is green with no regressions
|
||||
3. /ui-catalog serves all 11 component sections with variants and templ annotations
|
||||
4. Production build (no -tags catalog) excludes catalog route — RegisterCatalogRoute is a no-op
|
||||
5. Developer has approved the visual output via the checkpoint — all 11 sections confirmed correct
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/13-design-system-foundation/13-05-SUMMARY.md`
|
||||
</output>
|
||||
Loading…
Reference in a new issue