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>
251 lines
6.3 KiB
Go
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 != ""
|
|
}
|