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.