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 != "" }