diff --git a/backend/internal/web/ui/helpers.go b/backend/internal/web/ui/helpers.go index 7afee74..3177f01 100644 --- a/backend/internal/web/ui/helpers.go +++ b/backend/internal/web/ui/helpers.go @@ -1,6 +1,10 @@ package ui -import "github.com/a-h/templ" +import ( + "strconv" + + "github.com/a-h/templ" +) // mergeAttrs returns a new templ.Attributes containing every key from base, // with override keys taking precedence on collision. Either input may be nil. @@ -14,3 +18,39 @@ func mergeAttrs(base, override templ.Attributes) templ.Attributes { } return out } + +// buttonType returns "button" if value is empty, otherwise value. +// Used to set default type="button" on button elements without an explicit type. +func buttonType(value string) string { + if value == "" { + return "button" + } + return value +} + +// inputType returns "text" if value is empty, otherwise value. +// Used to set default type="text" on input elements without an explicit type. +func inputType(value string) string { + if value == "" { + return "text" + } + return value +} + +// inputID returns id if non-empty, otherwise name. +// Used to derive an implicit id from the name attribute when no id is provided. +func inputID(id string, name string) string { + if id != "" { + return id + } + return name +} + +// textareaRows returns strconv.Itoa(rows) if rows > 0, else "4". +// Used to set a safe default row count on textarea elements. +func textareaRows(rows int) string { + if rows <= 0 { + rows = 4 + } + return strconv.Itoa(rows) +} diff --git a/backend/internal/web/ui/ui_test.go b/backend/internal/web/ui/ui_test.go index 26b81d2..40cb54f 100644 --- a/backend/internal/web/ui/ui_test.go +++ b/backend/internal/web/ui/ui_test.go @@ -142,8 +142,11 @@ func TestButtonVariantGhost_Normalizer(t *testing.T) { func TestButtonClass_GhostVariant(t *testing.T) { got := ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) - if !strings.Contains(got, "ui-button-ghost") { - t.Errorf("ButtonClass(Ghost, Solid, MD) = %q; want to contain \"ui-button-ghost\"", got) + // 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) } } diff --git a/backend/internal/web/ui/variants.go b/backend/internal/web/ui/variants.go index 13cd8bf..e961f27 100644 --- a/backend/internal/web/ui/variants.go +++ b/backend/internal/web/ui/variants.go @@ -20,6 +20,7 @@ const ( ButtonVariantWarning ButtonVariant = "warning" ButtonVariantSuccess ButtonVariant = "success" ButtonVariantDanger ButtonVariant = "danger" + ButtonVariantGhost ButtonVariant = "ghost" ) // ButtonTone is the visual-weight enum for Button (solid vs. soft). @@ -38,6 +39,36 @@ const ( BadgeVariantWarning BadgeVariant = "warning" BadgeVariantSuccess BadgeVariant = "success" BadgeVariantDanger BadgeVariant = "danger" + BadgeVariantPrimary BadgeVariant = "primary" +) + +// IconButtonVariant is the semantic-color enum for IconButton. +type IconButtonVariant string + +const ( + IconButtonVariantNeutral IconButtonVariant = "neutral" + IconButtonVariantWarning IconButtonVariant = "warning" + IconButtonVariantSuccess IconButtonVariant = "success" + IconButtonVariantDanger IconButtonVariant = "danger" +) + +// IconButtonTone is the visual-weight enum for IconButton (solid vs. ghost). +type IconButtonTone string + +const ( + IconButtonToneSolid IconButtonTone = "solid" + IconButtonToneGhost IconButtonTone = "ghost" +) + +// SpacingStep is the spacing size enum for space components. +type SpacingStep string + +const ( + SpacingStepXS SpacingStep = "xs" + SpacingStepSM SpacingStep = "sm" + SpacingStepMD SpacingStep = "md" + SpacingStepLG SpacingStep = "lg" + SpacingStepXL SpacingStep = "xl" ) // NormalizedSize returns the safe default (SizeMD) for the zero value and any @@ -55,7 +86,7 @@ func NormalizedSize(size Size) Size { // the zero value and any value not in the declared set. func NormalizedButtonVariant(variant ButtonVariant) ButtonVariant { switch variant { - case ButtonVariantNeutral, ButtonVariantWarning, ButtonVariantSuccess, ButtonVariantDanger: + case ButtonVariantNeutral, ButtonVariantWarning, ButtonVariantSuccess, ButtonVariantDanger, ButtonVariantGhost: return variant default: return ButtonVariantDefault @@ -77,13 +108,46 @@ func NormalizedButtonTone(tone ButtonTone) ButtonTone { // zero value and any value not in the declared set. func NormalizedBadgeVariant(variant BadgeVariant) BadgeVariant { switch variant { - case BadgeVariantWarning, BadgeVariantSuccess, BadgeVariantDanger: + case BadgeVariantWarning, BadgeVariantSuccess, BadgeVariantDanger, BadgeVariantPrimary: return variant default: return BadgeVariantInfo } } +// NormalizedIconButtonVariant returns the safe default (IconButtonVariantNeutral) +// for the zero value and any value not in the declared set. +func NormalizedIconButtonVariant(variant IconButtonVariant) IconButtonVariant { + switch variant { + case IconButtonVariantWarning, IconButtonVariantSuccess, IconButtonVariantDanger: + return variant + default: + return IconButtonVariantNeutral + } +} + +// NormalizedIconButtonTone returns the safe default (IconButtonToneSolid) for +// the zero value and any value not in the declared set. +func NormalizedIconButtonTone(tone IconButtonTone) IconButtonTone { + switch tone { + case IconButtonToneGhost: + return tone + default: + return IconButtonToneSolid + } +} + +// NormalizedSpacingStep returns the safe default (SpacingStepMD) for the zero +// value and any value not in the declared set. +func NormalizedSpacingStep(step SpacingStep) SpacingStep { + switch step { + case SpacingStepXS, SpacingStepSM, SpacingStepLG, SpacingStepXL: + return step + default: + return SpacingStepMD + } +} + // ButtonClass assembles the deterministic class string for a Button. Inputs // are normalized before assembly so callers can pass zero values safely. // @@ -103,3 +167,29 @@ func BadgeClass(variant BadgeVariant) string { v := NormalizedBadgeVariant(variant) return "ui-badge ui-badge-" + string(v) } + +// IconButtonClass assembles the deterministic class string for an IconButton. +// Ghost tone uses the borderless pattern; solid tone uses the filled pattern. +func IconButtonClass(variant IconButtonVariant, tone IconButtonTone) string { + normalizedVariant := NormalizedIconButtonVariant(variant) + switch NormalizedIconButtonTone(tone) { + case IconButtonToneGhost: + return "borderless-icon-button ui-icon-button-ghost ui-icon-button-" + string(normalizedVariant) + default: + return "ui-icon-button ui-icon-button-solid ui-icon-button-" + string(normalizedVariant) + } +} + +// SpaceXClass assembles the deterministic class string for a horizontal spacer. +// +// Example: SpaceXClass(SpacingStepMD) == "ui-space-x ui-space-x-md". +func SpaceXClass(step SpacingStep) string { + return "ui-space-x ui-space-x-" + string(NormalizedSpacingStep(step)) +} + +// SpaceYClass assembles the deterministic class string for a vertical spacer. +// +// Example: SpaceYClass(SpacingStepLG) == "ui-space-y ui-space-y-lg". +func SpaceYClass(step SpacingStep) string { + return "ui-space-y ui-space-y-" + string(NormalizedSpacingStep(step)) +}