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>
This commit is contained in:
parent
7c20538a4e
commit
71d3416304
@ -1 +0,0 @@
|
|||||||
package builtin
|
|
||||||
299
blocks/builtin/html.go
Normal file
299
blocks/builtin/html.go
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
package builtin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"maps"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.dev.alexdunmow.com/ninja/core/blocks"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var HTMLBlockMeta = blocks.BlockMeta{
|
||||||
|
Key: "html",
|
||||||
|
Title: "HTML",
|
||||||
|
Description: "Raw HTML content with template variable support",
|
||||||
|
Category: blocks.CategoryContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
var mediaURLRegex = regexp.MustCompile(`(src|href)=["']media:([^"']+)["']`)
|
||||||
|
|
||||||
|
func HTMLBlock(ctx context.Context, content map[string]any) string {
|
||||||
|
var htmlTemplate string
|
||||||
|
if htmlContent, ok := content["_html_content"].(string); ok && htmlContent != "" {
|
||||||
|
htmlTemplate = htmlContent
|
||||||
|
} else {
|
||||||
|
htmlTemplate, _ = content["html"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if htmlTemplate == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
enrichedContent := enrichMediaFields(ctx, content)
|
||||||
|
|
||||||
|
result, err := blocks.RenderTemplate(htmlTemplate, enrichedContent)
|
||||||
|
if err != nil {
|
||||||
|
if blocks.IsEditor(ctx) {
|
||||||
|
return fmt.Sprintf(`<div class="bg-red-50 border border-red-200 text-red-700 p-4 rounded">
|
||||||
|
<strong>Template Error:</strong> %s
|
||||||
|
</div>`, html.EscapeString(err.Error()))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
result = blocks.ProcessIcons(result)
|
||||||
|
result = ResolveMediaURLs(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func enrichMediaFields(ctx context.Context, content map[string]any) map[string]any {
|
||||||
|
mediaFields := GetMediaFields(content)
|
||||||
|
if len(mediaFields) == 0 {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver := blocks.GetMediaResolver(ctx)
|
||||||
|
|
||||||
|
enriched := make(map[string]any, len(content))
|
||||||
|
maps.Copy(enriched, content)
|
||||||
|
|
||||||
|
for fieldName := range mediaFields {
|
||||||
|
value, ok := content[fieldName].(string)
|
||||||
|
if !ok || value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaValue := parseMediaValue(ctx, resolver, value)
|
||||||
|
if mediaValue != nil {
|
||||||
|
enriched[fieldName] = mediaValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return enriched
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMediaValue(ctx context.Context, resolver blocks.MediaResolver, value string) *blocks.MediaValue {
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path := value
|
||||||
|
if after, ok := strings.CutPrefix(value, "media:"); ok {
|
||||||
|
path = after
|
||||||
|
}
|
||||||
|
|
||||||
|
src := path
|
||||||
|
if !strings.HasPrefix(path, "/media/") {
|
||||||
|
src = "/media/" + path
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(path, "/", 2)
|
||||||
|
if len(parts) == 0 || parts[0] == "" {
|
||||||
|
return &blocks.MediaValue{Src: src}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID, err := uuid.Parse(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return &blocks.MediaValue{Src: src}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resolver == nil {
|
||||||
|
return &blocks.MediaValue{Src: src}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved := resolver.ResolveMedia(ctx, mediaID)
|
||||||
|
if resolved == nil {
|
||||||
|
return &blocks.MediaValue{Src: src}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved.Src = src
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaFields extracts field names that have x-editor: "media" from customSchema.
|
||||||
|
func GetMediaFields(content map[string]any) map[string]bool {
|
||||||
|
result := make(map[string]bool)
|
||||||
|
|
||||||
|
customSchema, ok := content["customSchema"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
properties, ok := customSchema["properties"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, prop := range properties {
|
||||||
|
propMap, ok := prop.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if editor, ok := propMap["x-editor"].(string); ok && editor == "media" {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveMediaURLs converts media: prefixed URLs to proper /media/ paths.
|
||||||
|
func ResolveMediaURLs(htmlStr string) string {
|
||||||
|
return mediaURLRegex.ReplaceAllStringFunc(htmlStr, func(match string) string {
|
||||||
|
submatches := mediaURLRegex.FindStringSubmatch(match)
|
||||||
|
if len(submatches) < 3 {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
attr := submatches[1]
|
||||||
|
path := submatches[2]
|
||||||
|
|
||||||
|
if strings.HasPrefix(path, "/media/") {
|
||||||
|
return fmt.Sprintf(`%s="%s"`, attr, path)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`%s="/media/%s"`, attr, path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
boldRegex = regexp.MustCompile(`\*\*(.+?)\*\*`)
|
||||||
|
italicRegex = regexp.MustCompile(`\*([^*]+?)\*`)
|
||||||
|
colorRegex = regexp.MustCompile(`==(?:([a-z-]+):)?(.+?)==`)
|
||||||
|
linkRegex = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
var validThemeColors = map[string]bool{
|
||||||
|
"foreground": true, "background": true,
|
||||||
|
"primary": true, "primary-foreground": true,
|
||||||
|
"secondary": true, "secondary-foreground": true,
|
||||||
|
"muted": true, "muted-foreground": true,
|
||||||
|
"accent": true, "accent-foreground": true,
|
||||||
|
"destructive": true, "destructive-foreground": true,
|
||||||
|
"border": true, "card": true, "card-foreground": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessMarkdown converts markdown-like syntax to HTML.
|
||||||
|
func ProcessMarkdown(text string) string {
|
||||||
|
text = boldRegex.ReplaceAllString(text, `<strong>$1</strong>`)
|
||||||
|
text = italicRegex.ReplaceAllString(text, `<em>$1</em>`)
|
||||||
|
text = colorRegex.ReplaceAllStringFunc(text, func(match string) string {
|
||||||
|
submatches := colorRegex.FindStringSubmatch(match)
|
||||||
|
if len(submatches) < 3 {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
colorName := submatches[1]
|
||||||
|
content := submatches[2]
|
||||||
|
|
||||||
|
if colorName == "" || !validThemeColors[colorName] {
|
||||||
|
colorName = "accent"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`<span class="text-%s">%s</span>`, colorName, content)
|
||||||
|
})
|
||||||
|
text = linkRegex.ReplaceAllStringFunc(text, func(match string) string {
|
||||||
|
submatches := linkRegex.FindStringSubmatch(match)
|
||||||
|
if len(submatches) < 3 {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
linkText := submatches[1]
|
||||||
|
url := submatches[2]
|
||||||
|
url = strings.ReplaceAll(url, "&", "&")
|
||||||
|
url = strings.ReplaceAll(url, """, "\"")
|
||||||
|
url = strings.ReplaceAll(url, "'", "'")
|
||||||
|
return fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(url), linkText)
|
||||||
|
})
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapLinesInParagraphs converts textarea newlines to <br> tags.
|
||||||
|
func WrapLinesInParagraphs(text string) string {
|
||||||
|
return strings.ReplaceAll(text, "\n", "<br>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextareaFields extracts field names that have x-editor: textarea from customSchema.
|
||||||
|
func GetTextareaFields(content map[string]any) map[string]bool {
|
||||||
|
result := make(map[string]bool)
|
||||||
|
|
||||||
|
customSchema, ok := content["customSchema"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
properties, ok := customSchema["properties"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, prop := range properties {
|
||||||
|
propMap, ok := prop.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if editor, ok := propMap["x-editor"].(string); ok && editor == "textarea" {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArrayItemTextareaFields extracts textarea field names from an array's item schema.
|
||||||
|
func GetArrayItemTextareaFields(content map[string]any, collectionName string) map[string]bool {
|
||||||
|
result := make(map[string]bool)
|
||||||
|
|
||||||
|
customSchema, ok := content["customSchema"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
properties, ok := customSchema["properties"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayField, ok := properties[collectionName].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemProperties, ok := arrayField["itemProperties"].(map[string]any); ok {
|
||||||
|
for _, prop := range itemProperties {
|
||||||
|
propMap, ok := prop.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if editor, ok := propMap["x-editor"].(string); ok && editor == "textarea" {
|
||||||
|
if title, ok := propMap["title"].(string); ok && title != "" {
|
||||||
|
result[title] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
items, ok := arrayField["items"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
itemProps, ok := items["properties"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, prop := range itemProps {
|
||||||
|
propMap, ok := prop.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if editor, ok := propMap["x-editor"].(string); ok && editor == "textarea" {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@ -313,6 +313,27 @@ func GetHumanProofBanner(ctx context.Context) *HumanProofBannerData {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Media resolver ---
|
||||||
|
|
||||||
|
// MediaResolver resolves media metadata from storage.
|
||||||
|
// The CMS implements this with db.Queries.GetMedia.
|
||||||
|
type MediaResolver interface {
|
||||||
|
ResolveMedia(ctx context.Context, mediaID uuid.UUID) *MediaValue
|
||||||
|
}
|
||||||
|
|
||||||
|
type mediaResolverContextKey struct{}
|
||||||
|
|
||||||
|
func WithMediaResolver(ctx context.Context, resolver MediaResolver) context.Context {
|
||||||
|
return context.WithValue(ctx, mediaResolverContextKey{}, resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMediaResolver(ctx context.Context) MediaResolver {
|
||||||
|
if r, ok := ctx.Value(mediaResolverContextKey{}).(MediaResolver); ok {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// --- Embed resolver ---
|
// --- Embed resolver ---
|
||||||
|
|
||||||
type embedResolverContextKey struct{}
|
type embedResolverContextKey struct{}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
package schemas
|
|
||||||
250
blocks/shared/button.go
Normal file
250
blocks/shared/button.go
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
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 != ""
|
||||||
|
}
|
||||||
54
blocks/shared/button.templ
Normal file
54
blocks/shared/button.templ
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
// RenderButton renders a button or link based on ButtonConfig
|
||||||
|
templ RenderButton(btn ButtonConfig) {
|
||||||
|
if btn.URL != "" {
|
||||||
|
if btn.InlineStyle() != "" {
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(btn.URL) }
|
||||||
|
class={ btn.Classes() }
|
||||||
|
if btn.OpenNewTab {
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
}
|
||||||
|
style={ btn.InlineStyle() }
|
||||||
|
>
|
||||||
|
{ btn.Text }
|
||||||
|
</a>
|
||||||
|
} else {
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(btn.URL) }
|
||||||
|
class={ btn.Classes() }
|
||||||
|
if btn.OpenNewTab {
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ btn.Text }
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if btn.InlineStyle() != "" {
|
||||||
|
<button type="button" class={ btn.Classes() } style={ btn.InlineStyle() }>
|
||||||
|
{ btn.Text }
|
||||||
|
</button>
|
||||||
|
} else {
|
||||||
|
<button type="button" class={ btn.Classes() }>
|
||||||
|
{ btn.Text }
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderSubmitButton renders a submit button
|
||||||
|
templ RenderSubmitButton(btn ButtonConfig) {
|
||||||
|
if btn.InlineStyle() != "" {
|
||||||
|
<button type="submit" class={ btn.Classes() } style={ btn.InlineStyle() }>
|
||||||
|
{ btn.Text }
|
||||||
|
</button>
|
||||||
|
} else {
|
||||||
|
<button type="submit" class={ btn.Classes() }>
|
||||||
|
{ btn.Text }
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
370
blocks/shared/button_templ.go
Normal file
370
blocks/shared/button_templ.go
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package shared
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
// RenderButton renders a button or link based on ButtonConfig
|
||||||
|
func RenderButton(btn ButtonConfig) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
if btn.URL != "" {
|
||||||
|
if btn.InlineStyle() != "" {
|
||||||
|
var templ_7745c5c3_Var2 = []any{btn.Classes()}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(btn.URL))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 8, Col: 33}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if btn.OpenNewTab {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " target=\"_blank\" rel=\"noopener noreferrer\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " style=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(btn.InlineStyle())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 14, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(btn.Text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 16, Col: 14}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var templ_7745c5c3_Var7 = []any{btn.Classes()}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(btn.URL))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 20, Col: 33}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var7).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if btn.OpenNewTab {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " target=\"_blank\" rel=\"noopener noreferrer\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(btn.Text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 27, Col: 14}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if btn.InlineStyle() != "" {
|
||||||
|
var templ_7745c5c3_Var11 = []any{btn.Classes()}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<button type=\"button\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" style=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(btn.InlineStyle())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 32, Col: 74}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(btn.Text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 33, Col: 14}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var templ_7745c5c3_Var15 = []any{btn.Classes()}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<button type=\"button\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(btn.Text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 37, Col: 14}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderSubmitButton renders a submit button
|
||||||
|
func RenderSubmitButton(btn ButtonConfig) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var18 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var18 == nil {
|
||||||
|
templ_7745c5c3_Var18 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
if btn.InlineStyle() != "" {
|
||||||
|
var templ_7745c5c3_Var19 = []any{btn.Classes()}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<button type=\"submit\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var20 string
|
||||||
|
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var19).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" style=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var21 string
|
||||||
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(btn.InlineStyle())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 46, Col: 73}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 string
|
||||||
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(btn.Text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 47, Col: 13}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var templ_7745c5c3_Var23 = []any{btn.Classes()}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<button type=\"submit\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var24 string
|
||||||
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var23).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var25 string
|
||||||
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(btn.Text)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button.templ`, Line: 51, Col: 13}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@ -1 +0,0 @@
|
|||||||
package shared
|
|
||||||
74
blocks/template.go
Normal file
74
blocks/template.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package blocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MediaValue represents a rich media object for template substitution.
|
||||||
|
// Implements fmt.Stringer to render a full <img> tag when used directly.
|
||||||
|
type MediaValue struct {
|
||||||
|
Src string // Full URL path (e.g., "/media/uuid/webp.webp")
|
||||||
|
Alt string // Alt text from media library
|
||||||
|
Filename string // Original filename
|
||||||
|
Width int // Image width in pixels
|
||||||
|
Height int // Image height in pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
// String renders the default <img> tag representation.
|
||||||
|
func (m MediaValue) String() string {
|
||||||
|
attrs := fmt.Sprintf(`src="%s" alt="%s" class="w-full h-auto"`,
|
||||||
|
html.EscapeString(m.Src),
|
||||||
|
html.EscapeString(m.Alt))
|
||||||
|
if m.Width > 0 && m.Height > 0 {
|
||||||
|
attrs += fmt.Sprintf(` width="%d" height="%d"`, m.Width, m.Height)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`<img %s>`, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderWithClass renders the <img> tag with custom CSS classes.
|
||||||
|
func (m MediaValue) RenderWithClass(class string) string {
|
||||||
|
attrs := fmt.Sprintf(`src="%s" alt="%s" class="%s"`,
|
||||||
|
html.EscapeString(m.Src),
|
||||||
|
html.EscapeString(m.Alt),
|
||||||
|
html.EscapeString(class))
|
||||||
|
if m.Width > 0 && m.Height > 0 {
|
||||||
|
attrs += fmt.Sprintf(` width="%d" height="%d"`, m.Width, m.Height)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`<img %s>`, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateRenderer is the interface for rendering pongo2/Jinja2-style templates.
|
||||||
|
// The CMS registers an implementation at startup.
|
||||||
|
type TemplateRenderer interface {
|
||||||
|
RenderTemplate(tmpl string, data map[string]any) (string, error)
|
||||||
|
ProcessIcons(html string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// templateRenderer holds the registered template renderer.
|
||||||
|
var templateRenderer TemplateRenderer
|
||||||
|
|
||||||
|
// RegisterTemplateRenderer sets the global template renderer.
|
||||||
|
// Called by the CMS at startup to provide pongo2 rendering.
|
||||||
|
func RegisterTemplateRenderer(r TemplateRenderer) {
|
||||||
|
templateRenderer = r
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderTemplate renders a pongo2/Jinja2-style template string with the given data.
|
||||||
|
// Returns the rendered HTML or an error. Falls back to a no-op if no renderer is registered.
|
||||||
|
func RenderTemplate(tmpl string, data map[string]any) (string, error) {
|
||||||
|
if templateRenderer == nil {
|
||||||
|
// No renderer registered — return template as-is (no variable substitution)
|
||||||
|
return tmpl, nil
|
||||||
|
}
|
||||||
|
return templateRenderer.RenderTemplate(tmpl, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessIcons processes ::pack:name:: icon syntax in HTML.
|
||||||
|
// Returns the HTML with icons replaced, or the original if no renderer is registered.
|
||||||
|
func ProcessIcons(htmlStr string) string {
|
||||||
|
if templateRenderer == nil {
|
||||||
|
return htmlStr
|
||||||
|
}
|
||||||
|
return templateRenderer.ProcessIcons(htmlStr)
|
||||||
|
}
|
||||||
9
go.mod
9
go.mod
@ -7,12 +7,17 @@ require (
|
|||||||
github.com/a-h/templ v0.3.1001
|
github.com/a-h/templ v0.3.1001
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.9.2
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
golang.org/x/mod v0.27.0
|
golang.org/x/mod v0.34.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
|
github.com/yuin/goldmark v1.8.2 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/text v0.36.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
15
go.sum
15
go.sum
@ -2,6 +2,8 @@ connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
|||||||
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
||||||
github.com/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY=
|
github.com/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY=
|
||||||
github.com/a-h/templ v0.3.1001/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
github.com/a-h/templ v0.3.1001/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@ -9,6 +11,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@ -17,6 +21,8 @@ github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
|||||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@ -24,12 +30,21 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||||
|
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user