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 + + + +After completion, create `.planning/phases/13-design-system-foundation/13-01-SUMMARY.md` + 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 + + + +After completion, create `.planning/phases/13-design-system-foundation/13-02-SUMMARY.md` + 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