diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 5e418f4..5dece7b 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -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
diff --git a/.planning/phases/13-design-system-foundation/13-01-PLAN.md b/.planning/phases/13-design-system-foundation/13-01-PLAN.md
new file mode 100644
index 0000000..f40b8d8
--- /dev/null
+++ b/.planning/phases/13-design-system-foundation/13-01-PLAN.md
@@ -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.
+
+
+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)
+
+
+
+@/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
+
+
+
+@.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
+
+
+
+
+
+ Task 1: Extract auth.css and replace base.css
+
+ backend/internal/web/ui/auth.css,
+ backend/internal/web/ui/base.css,
+ backend/tailwind.input.css
+
+
+ - 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)
+
+
+ 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.
+
+
+ grep -c 'color-brand-primary' backend/internal/web/ui/base.css
+ 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.
+
+
+ - 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
+
+ base.css replaced with 223-line token vocabulary; auth.css created with extracted provider styles; tailwind.input.css updated with auth.css import
+
+
+
+ Task 2: Extend variants.go with new enums and helpers.go with new helper functions
+
+ backend/internal/web/ui/variants.go,
+ backend/internal/web/ui/helpers.go,
+ backend/internal/web/ui/ui_test.go
+
+
+ - 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)
+
+
+ - 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"
+
+
+ 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).
+
+
+ cd backend && go test ./internal/web/ui/... -run "TestButtonVariantGhost|TestBadgeVariantPrimary|TestIconButtonClass|TestSpaceXClass|TestSpaceYClass" -v
+
+
+ - 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
+
+ variants.go extended with all new enums and class functions; helpers.go extended with component helper functions; all tests green
+
+
+
+
+
+## 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. |
+
+
+
+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
+
+
+
+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
+
+
+
diff --git a/.planning/phases/13-design-system-foundation/13-02-PLAN.md b/.planning/phases/13-design-system-foundation/13-02-PLAN.md
new file mode 100644
index 0000000..8de45b9
--- /dev/null
+++ b/.planning/phases/13-design-system-foundation/13-02-PLAN.md
@@ -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.
+
+
+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)
+
+
+
+@/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
+
+
+
+@.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
+
+
+
+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"
+
+
+
+
+
+ Task 1: Update ButtonClass() multi-class output + button.templ + update ui_test.go button assertions
+
+ backend/internal/web/ui/variants.go,
+ backend/internal/web/ui/button.templ,
+ backend/internal/web/ui/ui_test.go
+
+
+ - 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)
+
+
+ - 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")
+
+
+ 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 != "" { — 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.
+
+
+ cd backend && just generate && go test ./internal/web/ui/... -run "TestButton" -v
+
+
+ - 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)
+
+ ButtonClass() emits multi-class output; button.templ updated with Icon field and buttonType helper; all button tests green
+
+
+
+ Task 2: Replace button.css + badge.css + card.css; migrate card.templ; update template hardcodes; extend ui_test.go
+
+ 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
+
+
+ - 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)
+
+
+ - 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"
+
+
+ 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.
+
+
+ cd backend && just generate && go test ./... -count=1
+
+
+ - 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)
+
+ button.css/badge.css/card.css replaced; card.templ migrated to typed API; all template compound class strings updated; full test suite green
+
+
+
+
+
+## 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 |
+
+
+
+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
+
+
+
+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
+
+
+
diff --git a/.planning/phases/13-design-system-foundation/13-03-PLAN.md b/.planning/phases/13-design-system-foundation/13-03-PLAN.md
new file mode 100644
index 0000000..54d71f7
--- /dev/null
+++ b/.planning/phases/13-design-system-foundation/13-03-PLAN.md
@@ -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.
+
+
+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
+
+
+
+@/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
+
+
+
+@.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
+
+
+
+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
+
+
+
+
+
+ Task 1: Port input.templ, input.css, textarea.templ, textarea.css + tests
+
+ 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
+
+
+ - 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)
+
+
+ - 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"'
+
+
+ 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 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
+
+ cd backend && just generate && go test ./internal/web/ui/... -run "TestInput|TestTextarea" -v
+
+
+ - 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
+
+ input.css/input.templ created; textarea.css/textarea.templ created; all input/textarea tests passing
+
+
+
+ Task 2: Port select.templ + select_helpers.go + select.css + form_field.templ + form-field.css + tests
+
+ 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
+
+
+ - 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)
+
+
+ - Test: Select(SelectProps{Name: "status", Options: []SelectOption{{Value: "a", Label: "Alpha"}}}) HTML contains "ui-select-control"
+ - Test: Select renders a