From 66f23bba7707effbb94c117fd23399062e756f57 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 16 May 2026 13:52:01 +0200 Subject: [PATCH] feat(13-02): multi-class ButtonClass() + Icon field + buttonType helper in button.templ - ButtonClass() now emits "ui-button ui-button-solid ui-button-default ui-button-md" - Ghost variant special case: "ui-button ui-button-ghost ui-button-md" (tone omitted) - button.templ: added Icon string field to ButtonProps, replaced inline btnType with buttonType() helper - ui_test.go: updated TestButton_DefaultSolidMD to multi-class slice assertions - ui_test.go: updated TestButtonClass_String and TestButtonClass_GhostVariant expectations - All 18 ui package tests pass --- backend/internal/web/ui/button.templ | 14 +++++++------- backend/internal/web/ui/ui_test.go | 24 +++++++++++++++--------- backend/internal/web/ui/variants.go | 12 +++++++++--- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/backend/internal/web/ui/button.templ b/backend/internal/web/ui/button.templ index df79976..1c517af 100644 --- a/backend/internal/web/ui/button.templ +++ b/backend/internal/web/ui/button.templ @@ -3,21 +3,21 @@ package ui // ButtonProps is the input to the Button templ component. // // Type defaults to "button" when empty. Attrs is a pass-through for arbitrary -// attributes (notably hx-* HTMX attributes). +// attributes (notably hx-* HTMX attributes). Icon is a named icon key that +// will be rendered via UIIcon when Plan 04 wires up icon_button.templ. type ButtonProps struct { Label string Variant ButtonVariant Tone ButtonTone Size Size Type string + Icon string Attrs templ.Attributes } templ Button(props ButtonProps) { - {{ btnType := props.Type }} - if btnType == "" { - {{ btnType = "button" }} - } - {{ class := ButtonClass(props.Variant, props.Tone, props.Size) }} - + } diff --git a/backend/internal/web/ui/ui_test.go b/backend/internal/web/ui/ui_test.go index 40cb54f..fa4d1e0 100644 --- a/backend/internal/web/ui/ui_test.go +++ b/backend/internal/web/ui/ui_test.go @@ -23,9 +23,16 @@ func render(t *testing.T, ctx context.Context, c templ.Component) string { func TestButton_DefaultSolidMD(t *testing.T) { out := render(t, context.Background(), Button(ButtonProps{Label: "Fetch server time"})) - wantClass := `class="ui-button ui-button-solid-default-md"` - if !strings.Contains(out, wantClass) { - t.Errorf("output missing class %q\ngot: %s", wantClass, out) + // Plan 02: multi-class assertions (one check per class token) + for _, wantClass := range []string{ + `ui-button`, + `ui-button-solid`, + `ui-button-default`, + `ui-button-md`, + } { + if !strings.Contains(out, wantClass) { + t.Errorf("output missing class %q\ngot: %s", wantClass, out) + } } if !strings.Contains(out, "Fetch server time") { t.Errorf("output missing label literal; got: %s", out) @@ -102,7 +109,7 @@ func TestBadge_ZeroValueDefaultsToInfo(t *testing.T) { func TestButtonClass_String(t *testing.T) { got := ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD) - want := "ui-button ui-button-solid-default-md" + want := "ui-button ui-button-solid ui-button-default ui-button-md" if got != want { t.Errorf("ButtonClass = %q; want %q", got, want) } @@ -142,11 +149,10 @@ func TestButtonVariantGhost_Normalizer(t *testing.T) { func TestButtonClass_GhostVariant(t *testing.T) { got := ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) - // ButtonClass uses the compound format (ui-button-{tone}-{variant}-{size}) preserved - // from Phase 1 — Plan 02 migrates it to multi-class. Ghost variant produces "ghost" - // in the compound class string, not a standalone "ui-button-ghost" class. - if !strings.Contains(got, "ghost") { - t.Errorf("ButtonClass(Ghost, Solid, MD) = %q; want to contain \"ghost\"", got) + // Plan 02: ghost variant omits the tone class and emits "ui-button-ghost" standalone. + want := "ui-button ui-button-ghost ui-button-md" + if got != want { + t.Errorf("ButtonClass(Ghost, Solid, MD) = %q; want %q", got, want) } } diff --git a/backend/internal/web/ui/variants.go b/backend/internal/web/ui/variants.go index e961f27..46a0b99 100644 --- a/backend/internal/web/ui/variants.go +++ b/backend/internal/web/ui/variants.go @@ -152,12 +152,18 @@ func NormalizedSpacingStep(step SpacingStep) SpacingStep { // are normalized before assembly so callers can pass zero values safely. // // Example: ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD) == -// "ui-button ui-button-solid-default-md". +// "ui-button ui-button-solid ui-button-default ui-button-md". +// +// Ghost variant is a special case: the tone class is omitted and "ui-button-ghost" +// is emitted as a standalone modifier. func ButtonClass(variant ButtonVariant, tone ButtonTone, size Size) string { v := NormalizedButtonVariant(variant) - t := NormalizedButtonTone(tone) s := NormalizedSize(size) - return "ui-button ui-button-" + string(t) + "-" + string(v) + "-" + string(s) + if v == ButtonVariantGhost { + return "ui-button ui-button-ghost ui-button-" + string(s) + } + t := NormalizedButtonTone(tone) + return "ui-button ui-button-" + string(t) + " ui-button-" + string(v) + " ui-button-" + string(s) } // BadgeClass assembles the deterministic class string for a Badge.