core/blocks/shared/button.go
Alex Dunmow 71d3416304 feat: WO-PS-010 SDK HTMLBlock with MediaResolver interface
HTMLBlock uses MediaResolver interface instead of db.Queries for
media metadata enrichment. Includes shared ButtonConfig, MediaValue,
TemplateRenderer interface, and block helper utilities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-01 09:17:50 +08:00

251 lines
6.3 KiB
Go

package shared
import (
"strconv"
"strings"
)
// ButtonConfig represents a unified button configuration
type ButtonConfig struct {
// Content
Text string
URL string
// Type determines theme defaults
// Built-in: "primary" | "secondary" | "outline" | "ghost" | "link" | "destructive"
// Custom: any user-defined type name from theme.buttons.custom
Type string
// Override Mode
UseThemeDefaults bool // When true, uses CSS variables from theme
// Custom Overrides (only used when UseThemeDefaults = false)
TextColor string // Tailwind class like "text-white" or "text-[#ff5500]"
TextOpacity int // 0-100
BgColor string // Tailwind class like "bg-primary" or "bg-[#ff5500]"
BgOpacity int // 0-100
// Effects (can override theme)
Pill bool // Fully rounded corners
Elevated bool // Shadow
ThreeDee bool // 3D push effect
Size string // "sm" | "md" | "lg"
// Behavior
OpenNewTab bool
}
// Classes returns CSS classes for the button
func (b ButtonConfig) Classes() string {
var classes []string
classes = append(classes, "btn")
// Size class
switch b.Size {
case "sm":
classes = append(classes, "btn-sm")
case "lg":
classes = append(classes, "btn-lg")
default:
classes = append(classes, "btn-md")
}
if b.UseThemeDefaults {
// Use theme CSS variable classes
btnType := b.Type
if btnType == "" {
btnType = "primary"
}
classes = append(classes, "btn-"+btnType)
} else {
// Use custom Tailwind classes
if b.BgColor != "" {
classes = append(classes, b.BgColor)
} else {
// Default based on type variant
switch b.Type {
case "outline":
classes = append(classes, "border-2", "border-primary", "bg-transparent")
case "ghost":
classes = append(classes, "bg-transparent", "hover:bg-accent")
case "link":
classes = append(classes, "bg-transparent", "underline", "underline-offset-4")
case "destructive":
classes = append(classes, "bg-destructive", "hover:bg-destructive/90")
default: // primary, secondary, solid
classes = append(classes, "bg-primary", "hover:bg-primary/90")
}
}
if b.TextColor != "" {
classes = append(classes, b.TextColor)
} else {
switch b.Type {
case "outline":
classes = append(classes, "text-primary")
case "ghost":
classes = append(classes, "text-foreground", "hover:text-accent-foreground")
case "link":
classes = append(classes, "text-primary")
case "destructive":
classes = append(classes, "text-destructive-foreground")
default:
classes = append(classes, "text-primary-foreground")
}
}
}
// Effects that can override theme
if b.Pill {
classes = append(classes, "rounded-full")
}
if b.Elevated && b.Type != "ghost" && b.Type != "link" {
classes = append(classes, "shadow-lg", "hover:shadow-xl")
}
if b.ThreeDee && (b.Type == "" || b.Type == "primary" || b.Type == "solid") {
classes = append(classes, "border-b-4", "border-primary/70", "active:border-b-2", "active:mt-0.5")
}
return strings.Join(classes, " ")
}
// InlineStyle returns inline CSS styles for opacity if needed
func (b ButtonConfig) InlineStyle() string {
var styles []string
if !b.UseThemeDefaults {
if b.TextOpacity > 0 && b.TextOpacity < 100 {
opacity := float64(b.TextOpacity) / 100.0
styles = append(styles, "color-opacity: "+strconv.FormatFloat(opacity, 'f', 2, 64))
}
if b.BgOpacity > 0 && b.BgOpacity < 100 {
opacity := float64(b.BgOpacity) / 100.0
styles = append(styles, "--tw-bg-opacity: "+strconv.FormatFloat(opacity, 'f', 2, 64))
}
}
if len(styles) == 0 {
return ""
}
return strings.Join(styles, "; ")
}
// ParseButton extracts ButtonConfig from JSON content
func ParseButton(value any, defaultType string) ButtonConfig {
btn := ButtonConfig{
Type: defaultType,
UseThemeDefaults: true, // Default to theme styling
TextOpacity: 100,
BgOpacity: 100,
Size: "md",
}
if value == nil {
return btn
}
obj, ok := value.(map[string]any)
if !ok {
return btn
}
// Content
if text, ok := obj["text"].(string); ok {
btn.Text = text
}
if url, ok := obj["url"].(string); ok {
btn.URL = url
}
// Type - check explicit type field first
typeExplicitlySet := false
if t, ok := obj["type"].(string); ok && t != "" {
btn.Type = t
typeExplicitlySet = true
}
// Use theme defaults toggle - explicit setting takes precedence
if useTheme, ok := obj["useThemeDefaults"].(bool); ok {
btn.UseThemeDefaults = useTheme
} else {
// Legacy: if useThemeDefaults not set but custom colors exist, assume not using theme defaults
if textColor, ok := obj["textColor"].(string); ok && textColor != "" {
btn.UseThemeDefaults = false
} else if bgColor, ok := obj["bgColor"].(string); ok && bgColor != "" {
btn.UseThemeDefaults = false
}
}
// Custom overrides
if textColor, ok := obj["textColor"].(string); ok {
btn.TextColor = textColor
}
if textOpacity, ok := obj["textOpacity"].(float64); ok {
btn.TextOpacity = int(textOpacity)
}
if bgColor, ok := obj["bgColor"].(string); ok {
btn.BgColor = bgColor
}
if bgOpacity, ok := obj["bgOpacity"].(float64); ok {
btn.BgOpacity = int(bgOpacity)
}
// Effects
if pill, ok := obj["pill"].(bool); ok {
btn.Pill = pill
}
if elevated, ok := obj["elevated"].(bool); ok {
btn.Elevated = elevated
}
if threeDee, ok := obj["threeDee"].(bool); ok {
btn.ThreeDee = threeDee
}
if size, ok := obj["size"].(string); ok && size != "" {
btn.Size = size
}
// Behavior
if openNewTab, ok := obj["openNewTab"].(bool); ok {
btn.OpenNewTab = openNewTab
}
// Legacy support: "style" field from old format
if style, ok := obj["style"].(string); ok && style != "" && btn.Type == defaultType {
// Map legacy styles to types
switch style {
case "secondary":
btn.Type = "secondary"
case "outline":
btn.Type = "outline"
case "ghost":
btn.Type = "ghost"
case "link":
btn.Type = "link"
case "destructive":
btn.Type = "destructive"
default:
btn.Type = "primary"
}
}
// Legacy: "variant" field - only use if type wasn't explicitly set
if !typeExplicitlySet {
if variant, ok := obj["variant"].(string); ok && variant != "" {
if variant == "solid" {
btn.Type = "primary"
} else {
btn.Type = variant
}
}
}
return btn
}
// HasContent returns true if the button has text and URL
func (b ButtonConfig) HasContent() bool {
return b.Text != "" && b.URL != ""
}