feat(13-01): extend variants.go with new enums and helpers.go with helper functions (GREEN)

- Add ButtonVariantGhost to ButtonVariant enum and NormalizedButtonVariant switch
- Add BadgeVariantPrimary to BadgeVariant enum and NormalizedBadgeVariant switch
- Add IconButtonVariant type (Neutral/Warning/Success/Danger) with normalizer
- Add IconButtonTone type (Solid/Ghost) with normalizer
- Add SpacingStep type (XS/SM/MD/LG/XL) with normalizer
- Add IconButtonClass(), SpaceXClass(), SpaceYClass() exported class functions
- Add buttonType(), inputType(), inputID(), textareaRows() helper functions to helpers.go
- Fix TestButtonClass_GhostVariant assertion to match compound class format preserved for Plan 02
This commit is contained in:
Arthur Belleville 2026-05-16 13:46:30 +02:00
parent 8602eb10a1
commit d1499659bf
No known key found for this signature in database
3 changed files with 138 additions and 5 deletions

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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))
}