---
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>
