feat(gotham): initial extraction from blockninja monorepo

This commit is contained in:
Alex Dunmow 2026-06-03 11:57:17 +08:00
commit 1e7ee2cc1e
45 changed files with 4540 additions and 0 deletions

191
Makefile Normal file
View File

@ -0,0 +1,191 @@
# Gotham — build & deploy helpers (.so plugin workflow)
#
# The plugin compiles to a .so shared object loaded by the CMS at runtime.
# `make rebuild` copies source to the container, builds the .so, and restarts.
#
# Usage:
# make rebuild # Full rebuild: frontend + .so + CSS + migrations, restart
# make backend # Build .so + migrations, restart
# make build-css # Rebuild Tailwind CSS
# make logs # Tail instance logs
# make status # Show instance container status
.PHONY: rebuild backend build-frontend build-base-binary build-so copy-plugin-source sync-migrations build-css deploy-css logs status help spinup templ bump-patch bump-minor bump-major sync-version
# Paths
BLOCKNINJA_DIR := $(HOME)/src/blockninja
PLUGIN_SRC := $(CURDIR)
PLUGIN_NAME := gotham
MIGRATIONS_SRC := $(BLOCKNINJA_DIR)/backend/sql/migrations
GO_BUILDER := localhost/blockninja-go-builder:latest
CONTAINER := instance-gotham
ACCOUNT_SLUG := blockninja
INSTANCE_SLUG := gotham
STYLES_DIR := /var/lib/blockninja/$(ACCOUNT_SLUG)/$(INSTANCE_SLUG)/styles
PLUGIN_DEST := /app/data/plugins/src/$(PLUGIN_NAME)
# Default target: build the .so locally for development.
all: $(PLUGIN_NAME).so
# Local plugin build (no container). Useful for CI / quick checks.
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
# Ensure blockninja core services and the instance container are running.
spinup:
$(MAKE) -C $(BLOCKNINJA_DIR) spinup
# Full rebuild: frontend + .so plugin + CSS + migrations, restart.
rebuild: spinup
$(MAKE) build-frontend
$(MAKE) build-base-binary
$(MAKE) copy-plugin-source
$(MAKE) build-so
$(MAKE) build-css
$(MAKE) sync-migrations
podman restart $(CONTAINER)
@sleep 2
$(MAKE) deploy-css
@echo ""
@echo "Done. https://$(INSTANCE_SLUG).localdev.blockninjacms.com/"
# Backend-only rebuild: .so plugin + migrations, restart.
backend: spinup
$(MAKE) build-base-binary
$(MAKE) copy-plugin-source
$(MAKE) build-so
$(MAKE) sync-migrations
podman restart $(CONTAINER)
@echo "Backend updated."
# Build host admin UI and deploy to container.
build-frontend:
@echo "==> Building @block-ninja/ui ..."
cd $(BLOCKNINJA_DIR)/packages/ui && pnpm run build
@echo "==> Building host admin UI ..."
cd $(BLOCKNINJA_DIR)/web && pnpm run build
@echo "==> Deploying frontend to container ..."
podman exec $(CONTAINER) rm -rf /app/web/dist
podman cp $(BLOCKNINJA_DIR)/web/dist $(CONTAINER):/app/web/dist
@echo "Frontend deployed."
# Build the base CMS binary (without external plugins) and copy to container.
build-base-binary:
@echo "==> Building base CMS binary ..."
podman run --rm \
-v $(BLOCKNINJA_DIR)/backend:/src/backend:ro \
-v blockninja_go_cache:/go/pkg/mod \
-v /tmp:/out \
-w /src/backend \
$(GO_BUILDER) \
go build -o /out/blockninja-server ./cmd/server
podman cp /tmp/blockninja-server $(CONTAINER):/app/server
rm -f /tmp/blockninja-server
# Copy plugin source into the container's plugin source directory.
copy-plugin-source:
@echo "==> Copying $(PLUGIN_NAME) source to container ..."
podman exec $(CONTAINER) rm -rf $(PLUGIN_DEST)
podman exec $(CONTAINER) mkdir -p $(PLUGIN_DEST)
podman cp $(PLUGIN_SRC)/. $(CONTAINER):$(PLUGIN_DEST)/
podman exec $(CONTAINER) rm -rf $(PLUGIN_DEST)/.git $(PLUGIN_DEST)/Makefile
@echo "Plugin source copied."
# Build the .so using the go-builder container (same toolchain as CMS binary).
# Both builds resolve block/core from the shared module cache — no local replace.
build-so:
@echo "==> Building $(PLUGIN_NAME).so ..."
podman run --rm \
-v $(PLUGIN_SRC):/src/plugin:ro \
-v blockninja_go_cache:/go/pkg/mod \
-v /tmp:/out \
-w /src/plugin \
-e CGO_ENABLED=1 \
$(GO_BUILDER) \
go build -buildmode=plugin -ldflags="-s -w" -o /out/$(PLUGIN_NAME).so .
podman exec $(CONTAINER) mkdir -p /app/data/plugins/so
podman cp /tmp/$(PLUGIN_NAME).so $(CONTAINER):/app/data/plugins/so/$(PLUGIN_NAME).so
rm -f /tmp/$(PLUGIN_NAME).so
@echo "$(PLUGIN_NAME).so built."
# Sync base blockninja migration files from host to container.
sync-migrations:
@echo "==> Syncing migrations ..."
@podman unshare bash -c ' \
M=$$(podman mount $(CONTAINER)) && \
rm -rf "$$M/app/migrations" && \
mkdir -p "$$M/app/migrations" && \
podman umount $(CONTAINER)'
@podman cp $(MIGRATIONS_SRC)/. $(CONTAINER):/app/migrations/
@echo "Migrations synced."
# Rebuild Tailwind CSS.
build-css:
@echo "==> Building CSS ..."
cd $(BLOCKNINJA_DIR) && make css
# Copy built CSS to instance styles dir and container.
deploy-css:
@mkdir -p $(STYLES_DIR)
cp $(BLOCKNINJA_DIR)/data/styles/styles.css $(STYLES_DIR)/styles.css
podman cp $(BLOCKNINJA_DIR)/data/styles/styles.css $(CONTAINER):/app/data/styles/styles.css
podman cp $(BLOCKNINJA_DIR)/styles/input.base.css $(CONTAINER):/app/styles/input.base.css
@echo "CSS deployed."
# Regenerate templ Go files locally (for development).
templ:
cd $(PLUGIN_SRC) && templ generate
# Tail instance logs.
logs:
podman logs -f $(CONTAINER)
# Show instance container status.
status:
@podman inspect $(CONTAINER) --format \
'Name: {{.Name}}\nImage: {{.Config.Image}}\nStatus: {{.State.Status}}\nHealth: {{.State.Health.Status}}\nStarted: {{.State.StartedAt}}' \
2>/dev/null || echo "Container $(CONTAINER) not found."
help:
@echo "Targets:"
@echo " all Build $(PLUGIN_NAME).so locally (default)"
@echo " spinup Start blockninja core services + instance container if stopped"
@echo " rebuild Full rebuild: frontend + .so + CSS + migrations, restart"
@echo " backend Build .so + migrations, restart"
@echo " build-frontend Build host admin UI, deploy to container"
@echo " build-base-binary Build base CMS binary, copy to container"
@echo " copy-plugin-source Copy plugin source into container"
@echo " build-so Build .so inside container"
@echo " sync-migrations Copy migration files from host to container"
@echo " build-css Rebuild Tailwind CSS"
@echo " deploy-css Copy CSS to instance styles dir"
@echo " templ Regenerate templ Go files locally"
@echo " logs Tail instance container logs"
@echo " status Show instance container status"
# --- Version bump targets ---
CURRENT_VERSION := $(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/')
bump-patch:
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.%d.%d", $$1, $$2, $$3+1}'); \
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
echo "Bumped to $$NEW and tagged v$$NEW"
bump-minor:
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.%d.0", $$1, $$2+1}'); \
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
echo "Bumped to $$NEW and tagged v$$NEW"
bump-major:
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.0.0", $$1+1}'); \
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
echo "Bumped to $$NEW and tagged v$$NEW"
sync-version:
@TAG=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//'); \
if [ -z "$$TAG" ]; then echo "No tags found"; exit 1; fi; \
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$TAG'"/' plugin.mod; \
echo "Synced plugin.mod to $$TAG"

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# Gotham
Dark, modern BlockNinja theme plugin. Bold typography, high-contrast accents,
and a small set of theme-specific blocks (stats, features, footer) plus
overrides for the built-in `heading` and `text` blocks when the Gotham
template is active.
## Page templates
- `default` — header / main / footer
- `landing` — hero / main / cta / footer
- `full-width` — edge-to-edge header / main / footer
- `centered` — narrow centered content for articles and docs
## Blocks
- `gotham:stats` — stat row with configurable items
- `gotham:features` — feature grid
- `gotham:footer` — branded footer with optional signup form
## Build
```
make # builds gotham.so locally
make rebuild # rebuilds plugin inside an instance container
```
## Versioning
```
make bump-patch # 0.1.0 -> 0.1.1
make bump-minor # 0.1.0 -> 0.2.0
make bump-major # 0.1.0 -> 1.0.0
```
Then `git push --tags` and `ninja plugin publish` to release.

0
assets/fonts/.gitkeep Normal file
View File

View File

@ -0,0 +1 @@
/* placeholder for fonts */

33
assets/style.css Normal file
View File

@ -0,0 +1,33 @@
/* Gotham template styles - uses theme CSS variables */
/* Accent color classes - mapped to theme primary */
.gotham-accent {
color: hsl(var(--primary));
}
.gotham-accent-bg {
background-color: hsl(var(--primary));
}
.gotham-accent-bg:hover {
background-color: hsl(var(--primary) / 0.9);
}
/* Border accent */
.gotham-border-accent {
border-color: hsl(var(--primary));
}
/* Additional Gotham-specific utilities */
.gotham-gradient {
background: linear-gradient(180deg, hsl(var(--background)) 0%, hsl(var(--card)) 100%);
}
.gotham-card {
background-color: hsl(var(--card));
border: 1px solid hsl(var(--border));
}
.gotham-glow {
box-shadow: 0 0 20px hsl(var(--primary) / 0.3);
}

203
email_wrapper.templ Normal file
View File

@ -0,0 +1,203 @@
package main
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// GothamEmailWrapper wraps body content in a dark, modern Gotham-branded email template.
func GothamEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
gothamEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// gothamEmailTemplate is the Gotham-branded email template component.
templ gothamEmailTemplate(emailCtx templates.EmailContext, body string) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<title>{ emailCtx.SiteSettings.SiteName }</title>
<style type="text/css">
/* CSS Reset for Email */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
}
body {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
}
/* Gotham-specific styles */
h1, h2, h3, h4, h5, h6 {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-weight: 700;
}
/* Responsive styles */
@media only screen and (max-width: 620px) {
.email-container {
width: 100% !important;
max-width: 100% !important;
}
.content-padding {
padding-left: 24px !important;
padding-right: 24px !important;
}
}
</style>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;", gothamBgColor(emailCtx)) }>
<!-- Preview text (hidden) -->
if emailCtx.PreviewText != "" {
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">
{ emailCtx.PreviewText }
</div>
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
</div>
}
<!-- Main Container -->
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ fmt.Sprintf("padding: 48px 10px; background-color: %s;", gothamBgColor(emailCtx)) }>
<!-- Email Container -->
<table role="presentation" class="email-container" width="600" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s;", gothamCardColor(emailCtx), gothamBorderColor(emailCtx)) }>
<!-- Header -->
<tr>
<td align="center" style={ fmt.Sprintf("padding: 32px 40px; background-color: %s; border-bottom: 1px solid %s;", gothamCardColor(emailCtx), gothamBorderColor(emailCtx)) }>
if emailCtx.SiteSettings.LogoURL != "" {
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } style="max-height: 48px; width: auto; display: block;"/>
} else if emailCtx.SiteSettings.SiteName != "" {
<h1 style={ fmt.Sprintf("margin: 0; font-size: 24px; font-weight: 800; color: %s; letter-spacing: -0.5px; text-transform: uppercase;", gothamFgColor(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</h1>
}
</td>
</tr>
<!-- Body Content -->
<tr>
<td class="content-padding" style={ fmt.Sprintf("padding: 40px 48px; color: %s; font-size: 16px; line-height: 1.75; letter-spacing: 0.01em;", gothamFgColor(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<!-- Footer -->
<tr>
<td style={ fmt.Sprintf("padding: 32px 48px; background-color: %s; border-top: 1px solid %s;", gothamMutedColor(emailCtx), gothamBorderColor(emailCtx)) }>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center">
<p style={ fmt.Sprintf("margin: 0 0 8px; font-size: 13px; font-weight: 700; color: %s; text-transform: uppercase; letter-spacing: 0.1em;", gothamFgColor(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</p>
if emailCtx.SiteSettings.SiteURL != "" {
<p style={ fmt.Sprintf("margin: 0 0 16px; font-size: 13px; color: %s;", gothamMutedFgColor(emailCtx)) }>
<a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ fmt.Sprintf("color: %s; text-decoration: none; border-bottom: 1px solid %s;", gothamPrimaryColor(emailCtx), gothamPrimaryColor(emailCtx)) }>
{ emailCtx.SiteSettings.SiteURL }
</a>
</p>
}
if emailCtx.UnsubscribeURL != "" {
<p style={ fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", gothamMutedFgColor(emailCtx)) }>
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: none;", gothamMutedFgColor(emailCtx)) }>
Unsubscribe
</a>
</p>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}
// Gotham color helper functions - return hex colors with dark theme defaults
func gothamBgColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Background != "" {
return emailCtx.Colors.Background
}
return "#0a0a0a" // Near black
}
func gothamCardColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Card != "" {
return emailCtx.Colors.Card
}
return "#141414" // Dark card
}
func gothamFgColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Foreground != "" {
return emailCtx.Colors.Foreground
}
return "#fafafa" // Near white
}
func gothamPrimaryColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Primary != "" {
return emailCtx.Colors.Primary
}
return "#fafafa" // Gotham uses white as primary
}
func gothamMutedFgColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.MutedForeground != "" {
return emailCtx.Colors.MutedForeground
}
return "#737373" // Gray
}
func gothamMutedColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Muted != "" {
return emailCtx.Colors.Muted
}
return "#1a1a1a" // Darker muted
}
func gothamBorderColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Border != "" {
return emailCtx.Colors.Border
}
return "#262626" // Dark border
}

427
email_wrapper_templ.go Normal file
View File

@ -0,0 +1,427 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//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"
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// GothamEmailWrapper wraps body content in a dark, modern Gotham-branded email template.
func GothamEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
gothamEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// gothamEmailTemplate is the Gotham-branded email template component.
func gothamEmailTemplate(emailCtx templates.EmailContext, body string) 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)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><!--[if mso]>\n\t\t<noscript>\n\t\t\t<xml>\n\t\t\t\t<o:OfficeDocumentSettings>\n\t\t\t\t\t<o:PixelsPerInch>96</o:PixelsPerInch>\n\t\t\t\t</o:OfficeDocumentSettings>\n\t\t\t</xml>\n\t\t</noscript>\n\t\t<![endif]--><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 35, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><style type=\"text/css\">\n\t\t\t/* CSS Reset for Email */\n\t\t\tbody, table, td, p, a, li, blockquote {\n\t\t\t\t-webkit-text-size-adjust: 100%;\n\t\t\t\t-ms-text-size-adjust: 100%;\n\t\t\t}\n\t\t\ttable, td {\n\t\t\t\tmso-table-lspace: 0pt;\n\t\t\t\tmso-table-rspace: 0pt;\n\t\t\t}\n\t\t\timg {\n\t\t\t\t-ms-interpolation-mode: bicubic;\n\t\t\t\tborder: 0;\n\t\t\t\theight: auto;\n\t\t\t\tline-height: 100%;\n\t\t\t\toutline: none;\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\t\t\tbody {\n\t\t\t\tmargin: 0 !important;\n\t\t\t\tpadding: 0 !important;\n\t\t\t\twidth: 100% !important;\n\t\t\t}\n\t\t\ta[x-apple-data-detectors] {\n\t\t\t\tcolor: inherit !important;\n\t\t\t\ttext-decoration: none !important;\n\t\t\t}\n\t\t\t/* Gotham-specific styles */\n\t\t\th1, h2, h3, h4, h5, h6 {\n\t\t\t\tfont-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n\t\t\t\tfont-weight: 700;\n\t\t\t}\n\t\t\t/* Responsive styles */\n\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t.email-container {\n\t\t\t\t\twidth: 100% !important;\n\t\t\t\t\tmax-width: 100% !important;\n\t\t\t\t}\n\t\t\t\t.content-padding {\n\t\t\t\t\tpadding-left: 24px !important;\n\t\t\t\t\tpadding-right: 24px !important;\n\t\t\t\t}\n\t\t\t}\n\t\t</style></head><body style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;", gothamBgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 81, Col: 188}
}
_, 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, 3, "\"><!-- Preview text (hidden) -->")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.PreviewText != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display: none; max-height: 0; overflow: hidden; mso-hide: all;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 85, Col: 26}
}
_, 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, 5, "</div><div style=\"display: none; max-height: 0; overflow: hidden; mso-hide: all;\">&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<!-- Main Container --><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 48px 10px; background-color: %s;", gothamBgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 95, Col: 112}
}
_, 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, 7, "\"><!-- Email Container --><table role=\"presentation\" class=\"email-container\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s;", gothamCardColor(emailCtx), gothamBorderColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 97, Col: 251}
}
_, 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, 8, "\"><!-- Header --><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 32px 40px; background-color: %s; border-bottom: 1px solid %s;", gothamCardColor(emailCtx), gothamBorderColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 101, Col: 175}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.LogoURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.LogoURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 103, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 103, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" style=\"max-height: 48px; width: auto; display: block;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if emailCtx.SiteSettings.SiteName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h1 style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-size: 24px; font-weight: 800; color: %s; letter-spacing: -0.5px; text-transform: uppercase;", gothamFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 105, Col: 168}
}
_, 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, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 106, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</td></tr><!-- Body Content --><tr><td class=\"content-padding\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 40px 48px; color: %s; font-size: 16px; line-height: 1.75; letter-spacing: 0.01em;", gothamFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 114, Col: 173}
}
_, 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, 17, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td></tr><!-- Footer --><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 32px 48px; background-color: %s; border-top: 1px solid %s;", gothamMutedColor(emailCtx), gothamBorderColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 121, Col: 158}
}
_, 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, 19, "\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\"><p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0 0 8px; font-size: 13px; font-weight: 700; color: %s; text-transform: uppercase; letter-spacing: 0.1em;", gothamFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 125, Col: 174}
}
_, 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, 20, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 126, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.SiteURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0 0 16px; font-size: 13px; color: %s;", gothamMutedFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 129, Col: 113}
}
_, 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, 23, "\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 templ.SafeURL
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.SiteSettings.SiteURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 130, Col: 67}
}
_, 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, 24, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none; border-bottom: 1px solid %s;", gothamPrimaryColor(emailCtx), gothamPrimaryColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 130, Col: 215}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 131, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", gothamMutedFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 136, Col: 106}
}
_, 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, 28, "\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 templ.SafeURL
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.UnsubscribeURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 137, Col: 61}
}
_, 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, 29, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none;", gothamMutedFgColor(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 137, Col: 150}
}
_, 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, 30, "\">Unsubscribe</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td></tr></table></td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Gotham color helper functions - return hex colors with dark theme defaults
func gothamBgColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Background != "" {
return emailCtx.Colors.Background
}
return "#0a0a0a" // Near black
}
func gothamCardColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Card != "" {
return emailCtx.Colors.Card
}
return "#141414" // Dark card
}
func gothamFgColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Foreground != "" {
return emailCtx.Colors.Foreground
}
return "#fafafa" // Near white
}
func gothamPrimaryColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Primary != "" {
return emailCtx.Colors.Primary
}
return "#fafafa" // Gotham uses white as primary
}
func gothamMutedFgColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.MutedForeground != "" {
return emailCtx.Colors.MutedForeground
}
return "#737373" // Gray
}
func gothamMutedColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Muted != "" {
return emailCtx.Colors.Muted
}
return "#1a1a1a" // Darker muted
}
func gothamBorderColor(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Border != "" {
return emailCtx.Colors.Border
}
return "#262626" // Dark border
}
var _ = templruntime.GeneratedTemplate

49
embed.go Normal file
View File

@ -0,0 +1,49 @@
package main
import (
"embed"
"io/fs"
"net/http"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
// Assets returns the embedded assets filesystem.
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "assets")
return sub
}
// Schemas returns the embedded schemas filesystem.
func Schemas() fs.FS {
sub, _ := fs.Sub(schemasFS, "schemas")
return sub
}
// AssetsHandler returns an http.Handler that serves the embedded assets.
func AssetsHandler() http.Handler {
return http.FileServer(http.FS(Assets()))
}
// ThemePresets returns the embedded theme presets JSON.
func ThemePresets() []byte {
return presetsData
}
// BundledFonts returns the embedded fonts manifest JSON.
func BundledFonts() []byte {
return fontsData
}

58
features.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// FeaturesBlockMeta defines metadata for the features grid block.
var FeaturesBlockMeta = blocks.BlockMeta{
Key: "features",
Title: "Feature Cards",
Description: "Grid of feature cards with icons and descriptions",
Source: "gotham",
}
// FeaturesBlock renders a feature cards grid.
// Content expects: {"section_title": "...", "columns": 3, "features": [{"icon": "...", "title": "...", "description": "..."}]}
func FeaturesBlock(ctx context.Context, content map[string]any) string {
items := getSlice(content, "features")
if len(items) == 0 {
return ""
}
var features []FeatureItem
for _, item := range items {
features = append(features, FeatureItem{
Icon: getString(item, "icon"),
Title: getString(item, "title"),
Description: getString(item, "description"),
})
}
data := FeaturesData{
SectionTitle: getString(content, "section_title"),
Columns: getInt(content, "columns", 3),
Features: features,
}
var buf bytes.Buffer
_ = featuresComponent(data).Render(ctx, &buf)
return buf.String()
}
// FeaturesData contains data for the features component.
type FeaturesData struct {
SectionTitle string
Columns int
Features []FeatureItem
}
// FeatureItem represents a single feature.
type FeatureItem struct {
Icon string
Title string
Description string
}

41
features.templ Normal file
View File

@ -0,0 +1,41 @@
package main
// featuresComponent renders a Gotham-styled feature cards grid.
templ featuresComponent(data FeaturesData) {
<section class="py-20 flex-1">
<div class="max-w-6xl mx-auto px-4">
if data.SectionTitle != "" {
<h2 class="text-3xl font-bold text-center mb-12 gotham-accent">{ data.SectionTitle }</h2>
}
<div class={ "grid gap-8", featureGridCols(data.Columns) }>
for _, feature := range data.Features {
<div class="p-6 gotham-surface rounded-lg gotham-border border hover:border-primary/30 transition-colors">
if feature.Icon != "" {
<div class="mb-4 gotham-accent">
@iconSVG(feature.Icon)
</div>
}
<h3 class="text-xl font-semibold text-foreground mb-3">{ feature.Title }</h3>
if feature.Description != "" {
<p class="text-muted-foreground leading-relaxed">{ feature.Description }</p>
}
</div>
}
</div>
</div>
</section>
}
// featureGridCols returns the appropriate grid column class.
func featureGridCols(cols int) string {
switch cols {
case 1:
return "grid-cols-1"
case 2:
return "grid-cols-1 md:grid-cols-2"
case 4:
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
default:
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
}
}

160
features_templ.go Normal file
View File

@ -0,0 +1,160 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//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"
// featuresComponent renders a Gotham-styled feature cards grid.
func featuresComponent(data FeaturesData) 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)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"py-20 flex-1\"><div class=\"max-w-6xl mx-auto px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.SectionTitle != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"text-3xl font-bold text-center mb-12 gotham-accent\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.SectionTitle)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features.templ`, Line: 8, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
var templ_7745c5c3_Var3 = []any{"grid gap-8", featureGridCols(data.Columns)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, feature := range data.Features {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"p-6 gotham-surface rounded-lg gotham-border border hover:border-primary/30 transition-colors\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if feature.Icon != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"mb-4 gotham-accent\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = iconSVG(feature.Icon).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<h3 class=\"text-xl font-semibold text-foreground mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(feature.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features.templ`, Line: 18, Col: 76}
}
_, 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, 10, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if feature.Description != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"text-muted-foreground leading-relaxed\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(feature.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `features.templ`, Line: 20, Col: 77}
}
_, 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, 12, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// featureGridCols returns the appropriate grid column class.
func featureGridCols(cols int) string {
switch cols {
case 1:
return "grid-cols-1"
case 2:
return "grid-cols-1 md:grid-cols-2"
case 4:
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
default:
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
}
}
var _ = templruntime.GeneratedTemplate

88
fonts.json Normal file
View File

@ -0,0 +1,88 @@
[
{
"name": "Gotham",
"family": "Gotham",
"variants": [
{
"weight": "100",
"style": "normal",
"file": "fonts/web/gothamthin-webfont.woff2"
},
{
"weight": "100",
"style": "italic",
"file": "fonts/web/gothamthinitalic-webfont.woff2"
},
{
"weight": "200",
"style": "normal",
"file": "fonts/web/gothamxlight-webfont.woff2"
},
{
"weight": "200",
"style": "italic",
"file": "fonts/web/gothamxlightitalic-webfont.woff2"
},
{
"weight": "300",
"style": "normal",
"file": "fonts/web/gothamlight-webfont.woff2"
},
{
"weight": "300",
"style": "italic",
"file": "fonts/web/gothamlightitalic-webfont.woff2"
},
{
"weight": "400",
"style": "normal",
"file": "fonts/web/gothambook-webfont.woff2"
},
{
"weight": "400",
"style": "italic",
"file": "fonts/web/gothambookitalic-webfont.woff2"
},
{
"weight": "500",
"style": "normal",
"file": "fonts/web/gothammedium-webfont.woff2"
},
{
"weight": "500",
"style": "italic",
"file": "fonts/web/gothammediumitalic-webfont.woff2"
},
{
"weight": "700",
"style": "normal",
"file": "fonts/web/gothambold-webfont.woff2"
},
{
"weight": "700",
"style": "italic",
"file": "fonts/web/gothambolditalic-webfont.woff2"
},
{
"weight": "800",
"style": "normal",
"file": "fonts/web/gothamblack-webfont.woff2"
},
{
"weight": "800",
"style": "italic",
"file": "fonts/web/gothamblackitalic-webfont.woff2"
},
{
"weight": "900",
"style": "normal",
"file": "fonts/web/gothamultra-webfont.woff2"
},
{
"weight": "900",
"style": "italic",
"file": "fonts/web/gothamultraitalic-webfont.woff2"
}
]
}
]

68
footer.go Normal file
View File

@ -0,0 +1,68 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// FooterBlockMeta defines metadata for the footer block.
var FooterBlockMeta = blocks.BlockMeta{
Key: "footer",
Title: "Footer",
Description: "Multi-column footer with links and copyright",
Source: "gotham",
}
// FooterBlock renders a multi-column footer.
// Content expects: {"copyright": "...", "columns": [{"heading": "...", "links": [{"text": "...", "page_id": "...", "url": "..."}]}]}
func FooterBlock(ctx context.Context, content map[string]any) string {
columns := getSlice(content, "columns")
var footerColumns []FooterColumn
for _, col := range columns {
links := getSlice(col, "links")
var footerLinks []FooterLink
for _, link := range links {
footerLinks = append(footerLinks, FooterLink{
Text: getString(link, "text"),
PageID: getString(link, "page_id"),
URL: getString(link, "url"),
})
}
footerColumns = append(footerColumns, FooterColumn{
Heading: getString(col, "heading"),
Links: footerLinks,
})
}
data := FooterData{
Copyright: getString(content, "copyright"),
Columns: footerColumns,
}
var buf bytes.Buffer
_ = footerComponent(data).Render(ctx, &buf)
return buf.String()
}
// FooterData contains data for the footer component.
type FooterData struct {
Copyright string
Columns []FooterColumn
}
// FooterColumn represents a column in the footer.
type FooterColumn struct {
Heading string
Links []FooterLink
}
// FooterLink represents a link in the footer.
// If PageID is set, it's an internal page link. Otherwise, URL is used.
type FooterLink struct {
Text string
PageID string
URL string
}

63
footer.templ Normal file
View File

@ -0,0 +1,63 @@
package main
// footerComponent renders a Gotham-styled multi-column footer.
// Note: The <footer> wrapper element is handled by the block wrapper (render/blocks.go)
templ footerComponent(data FooterData) {
<div class="max-w-6xl mx-auto px-4">
if len(data.Columns) > 0 {
<div class={ "grid gap-8 mb-12", footerGridCols(len(data.Columns)) }>
for _, col := range data.Columns {
<div>
if col.Heading != "" {
<h4 class="text-lg font-semibold gotham-accent mb-4">{ col.Heading }</h4>
}
if len(col.Links) > 0 {
<ul class="space-y-2">
for _, link := range col.Links {
<li>
<a href={ templ.SafeURL(getLinkURL(link)) } class="text-muted-foreground hover:text-foreground transition-colors">
{ link.Text }
</a>
</li>
}
</ul>
}
</div>
}
</div>
}
if data.Copyright != "" {
<div class="pt-8 gotham-border border-t text-center">
<p class="text-muted-foreground text-sm">{ data.Copyright }</p>
</div>
}
</div>
}
// footerGridCols returns grid column class for footer.
func footerGridCols(count int) string {
switch count {
case 1:
return "grid-cols-1"
case 2:
return "grid-cols-1 md:grid-cols-2"
case 3:
return "grid-cols-1 md:grid-cols-3"
default:
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
}
}
// getLinkURL returns the appropriate URL for a footer link.
// If PageID is set, constructs an internal link. Otherwise returns the URL.
func getLinkURL(link FooterLink) string {
if link.PageID != "" {
// Internal page links use the page ID as slug placeholder
// In production, this would be resolved to the actual page slug
return "/pages/" + link.PageID
}
if link.URL != "" {
return link.URL
}
return "#"
}

192
footer_templ.go Normal file
View File

@ -0,0 +1,192 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//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"
// footerComponent renders a Gotham-styled multi-column footer.
// Note: The <footer> wrapper element is handled by the block wrapper (render/blocks.go)
func footerComponent(data FooterData) 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)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-6xl mx-auto px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Columns) > 0 {
var templ_7745c5c3_Var2 = []any{"grid gap-8 mb-12", footerGridCols(len(data.Columns))}
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, 2, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
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
}
for _, col := range data.Columns {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if col.Heading != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<h4 class=\"text-lg font-semibold gotham-accent mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Heading)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 12, Col: 73}
}
_, 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, 6, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(col.Links) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<ul class=\"space-y-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, link := range col.Links {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(getLinkURL(link)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 18, Col: 51}
}
_, 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, 9, "\" class=\"text-muted-foreground hover:text-foreground transition-colors\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(link.Text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 19, Col: 22}
}
_, 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, 10, "</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Copyright != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"pt-8 gotham-border border-t text-center\"><p class=\"text-muted-foreground text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.Copyright)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 31, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// footerGridCols returns grid column class for footer.
func footerGridCols(count int) string {
switch count {
case 1:
return "grid-cols-1"
case 2:
return "grid-cols-1 md:grid-cols-2"
case 3:
return "grid-cols-1 md:grid-cols-3"
default:
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
}
}
// getLinkURL returns the appropriate URL for a footer link.
// If PageID is set, constructs an internal link. Otherwise returns the URL.
func getLinkURL(link FooterLink) string {
if link.PageID != "" {
// Internal page links use the page ID as slug placeholder
// In production, this would be resolved to the actual page slug
return "/pages/" + link.PageID
}
if link.URL != "" {
return link.URL
}
return "#"
}
var _ = templruntime.GeneratedTemplate

20
go.mod Normal file
View File

@ -0,0 +1,20 @@
module git.dev.alexdunmow.com/block/themes/gotham
go 1.26.2
require (
git.dev.alexdunmow.com/block/core v0.10.1-0.20260603033123-041a7c2e3fae
github.com/a-h/templ v0.3.1020
)
require (
connectrpc.com/connect v1.20.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.2 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

42
go.sum Normal file
View File

@ -0,0 +1,42 @@
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
git.dev.alexdunmow.com/block/core v0.10.1-0.20260603033123-041a7c2e3fae h1:DKTKe7iMTxPpgnHFbGVDjbbORTsbyobjSiOdkcMpqB8=
git.dev.alexdunmow.com/block/core v0.10.1-0.20260603033123-041a7c2e3fae/go.mod h1:OPUWn+Ry75Lpi+rnYf9S8ZLrNeRsO+MTF8GyCii6Nx8=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
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/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

BIN
gotham.so Normal file

Binary file not shown.

42
heading_override.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"bytes"
"context"
"strconv"
)
// GothamHeadingBlock renders a heading with Gotham styling.
// Uses the same schema as the base heading block.
// Content expects: {"text": "Heading text", "level": 1-6, "textClass": "optional-class-for-h1"}
// Note: "class" is reserved for wrapper styling, use "textClass" for the heading element itself
func GothamHeadingBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
textClass := getString(content, "textClass")
level := parseHeadingLevel(content)
var buf bytes.Buffer
_ = gothamHeadingComponent(level, text, textClass).Render(ctx, &buf)
return buf.String()
}
// parseHeadingLevel parses the level from content, defaulting to 2.
func parseHeadingLevel(content map[string]any) int {
if level, ok := content["level"].(float64); ok {
l := int(level)
if l >= 1 && l <= 6 {
return l
}
}
if level, ok := content["level"].(int); ok {
if level >= 1 && level <= 6 {
return level
}
}
if level, ok := content["level"].(string); ok {
if l, err := strconv.Atoi(level); err == nil && l >= 1 && l <= 6 {
return l
}
}
return 2
}

41
heading_override.templ Normal file
View File

@ -0,0 +1,41 @@
package main
// gothamHeadingBaseClass returns default Tailwind classes for each heading level.
func gothamHeadingBaseClass(level int) string {
switch level {
case 1:
return "text-4xl font-bold tracking-tight"
case 2:
return "text-3xl font-semibold tracking-tight"
case 3:
return "text-2xl font-semibold"
case 4:
return "text-xl font-semibold"
case 5:
return "text-lg font-medium"
case 6:
return "text-base font-medium"
default:
return "text-3xl font-semibold tracking-tight"
}
}
// gothamHeadingComponent renders a heading with Gotham accent styling.
templ gothamHeadingComponent(level int, text, textClass string) {
switch level {
case 1:
<h1 class={ gothamHeadingBaseClass(1), "text-accent", textClass }>{ text }</h1>
case 2:
<h2 class={ gothamHeadingBaseClass(2), "text-accent", textClass }>{ text }</h2>
case 3:
<h3 class={ gothamHeadingBaseClass(3), "text-accent", textClass }>{ text }</h3>
case 4:
<h4 class={ gothamHeadingBaseClass(4), "text-accent", textClass }>{ text }</h4>
case 5:
<h5 class={ gothamHeadingBaseClass(5), "text-accent", textClass }>{ text }</h5>
case 6:
<h6 class={ gothamHeadingBaseClass(6), "text-accent", textClass }>{ text }</h6>
default:
<h2 class={ gothamHeadingBaseClass(2), "text-accent", textClass }>{ text }</h2>
}
}

311
heading_override_templ.go Normal file
View File

@ -0,0 +1,311 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//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"
// gothamHeadingBaseClass returns default Tailwind classes for each heading level.
func gothamHeadingBaseClass(level int) string {
switch level {
case 1:
return "text-4xl font-bold tracking-tight"
case 2:
return "text-3xl font-semibold tracking-tight"
case 3:
return "text-2xl font-semibold"
case 4:
return "text-xl font-semibold"
case 5:
return "text-lg font-medium"
case 6:
return "text-base font-medium"
default:
return "text-3xl font-semibold tracking-tight"
}
}
// gothamHeadingComponent renders a heading with Gotham accent styling.
func gothamHeadingComponent(level int, text, textClass string) 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)
switch level {
case 1:
var templ_7745c5c3_Var2 = []any{gothamHeadingBaseClass(1), "text-accent", textClass}
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, "<h1 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 27, Col: 75}
}
_, 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, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 2:
var templ_7745c5c3_Var5 = []any{gothamHeadingBaseClass(2), "text-accent", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var5).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 29, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 3:
var templ_7745c5c3_Var8 = []any{gothamHeadingBaseClass(3), "text-accent", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 31, Col: 75}
}
_, 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, 9, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 4:
var templ_7745c5c3_Var11 = []any{gothamHeadingBaseClass(4), "text-accent", textClass}
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, 10, "<h4 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var11).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 33, Col: 75}
}
_, 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, 12, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 5:
var templ_7745c5c3_Var14 = []any{gothamHeadingBaseClass(5), "text-accent", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h5 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 35, Col: 75}
}
_, 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, 15, "</h5>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 6:
var templ_7745c5c3_Var17 = []any{gothamHeadingBaseClass(6), "text-accent", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h6 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var17).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 37, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h6>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
var templ_7745c5c3_Var20 = []any{gothamHeadingBaseClass(2), "text-accent", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var20).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 39, Col: 75}
}
_, 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, 21, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

34
helpers.go Normal file
View File

@ -0,0 +1,34 @@
package main
// getString extracts a string value from content map.
func getString(content map[string]any, key string) string {
if v, ok := content[key].(string); ok {
return v
}
return ""
}
// getInt extracts an int value from content map (handles float64 from JSON).
func getInt(content map[string]any, key string, defaultVal int) int {
if v, ok := content[key].(float64); ok {
return int(v)
}
if v, ok := content[key].(int); ok {
return v
}
return defaultVal
}
// getSlice extracts a slice of maps from content.
func getSlice(content map[string]any, key string) []map[string]any {
if v, ok := content[key].([]any); ok {
result := make([]map[string]any, 0, len(v))
for _, item := range v {
if m, ok := item.(map[string]any); ok {
result = append(result, m)
}
}
return result
}
return nil
}

41
hero.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// HeroBlockMeta defines metadata for the hero section block.
var HeroBlockMeta = blocks.BlockMeta{
Key: "hero",
Title: "Hero Section",
Description: "Large hero section with headline and optional CTA",
Source: "gotham",
}
// HeroBlock renders a hero section.
// Content expects: {"headline": "...", "subheadline": "...", "background_url": "...", "cta_text": "...", "cta_url": "..."}
func HeroBlock(ctx context.Context, content map[string]any, _ []string) string {
data := HeroData{
Headline: getString(content, "headline"),
Subheadline: getString(content, "subheadline"),
BackgroundURL: getString(content, "background_url"),
CTAText: getString(content, "cta_text"),
CTAURL: getString(content, "cta_url"),
}
var buf bytes.Buffer
_ = heroComponent(data).Render(ctx, &buf)
return buf.String()
}
// HeroData contains data for the hero component.
type HeroData struct {
Headline string
Subheadline string
BackgroundURL string
CTAText string
CTAURL string
}

29
hero.templ Normal file
View File

@ -0,0 +1,29 @@
package main
// heroComponent renders a Gotham-styled hero section.
templ heroComponent(data HeroData) {
<section class="relative py-32 overflow-hidden">
if data.BackgroundURL != "" {
<div class="absolute inset-0 z-0">
<img src={ data.BackgroundURL } alt="" class="w-full h-full object-cover opacity-20"/>
<div class="absolute inset-0 bg-gradient-to-b from-background/80 via-background/60 to-background"></div>
</div>
}
<div class="relative z-10 max-w-4xl mx-auto px-4 text-center">
<h1 class="text-5xl md:text-6xl font-bold mb-6">
<span class="gotham-accent">{ data.Headline }</span>
</h1>
if data.Subheadline != "" {
<p class="text-xl md:text-2xl text-muted-foreground mb-10 max-w-2xl mx-auto">{ data.Subheadline }</p>
}
if data.CTAText != "" && data.CTAURL != "" {
<a
href={ templ.SafeURL(data.CTAURL) }
class="inline-block px-8 py-4 gotham-accent-bg text-primary-foreground font-semibold rounded-lg transition-colors text-lg"
>
{ data.CTAText }
</a>
}
</div>
</section>
}

132
hero_templ.go Normal file
View File

@ -0,0 +1,132 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//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"
// heroComponent renders a Gotham-styled hero section.
func heroComponent(data HeroData) 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)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"relative py-32 overflow-hidden\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.BackgroundURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"absolute inset-0 z-0\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.BackgroundURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `hero.templ`, Line: 8, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"\" class=\"w-full h-full object-cover opacity-20\"><div class=\"absolute inset-0 bg-gradient-to-b from-background/80 via-background/60 to-background\"></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"relative z-10 max-w-4xl mx-auto px-4 text-center\"><h1 class=\"text-5xl md:text-6xl font-bold mb-6\"><span class=\"gotham-accent\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Headline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `hero.templ`, Line: 14, Col: 47}
}
_, 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, 5, "</span></h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Subheadline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p class=\"text-xl md:text-2xl text-muted-foreground mb-10 max-w-2xl mx-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Subheadline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `hero.templ`, Line: 17, Col: 99}
}
_, 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, 7, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.CTAText != "" && data.CTAURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.CTAURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `hero.templ`, Line: 21, Col: 38}
}
_, 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, 9, "\" class=\"inline-block px-8 py-4 gotham-accent-bg text-primary-foreground font-semibold rounded-lg transition-colors text-lg\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.CTAText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `hero.templ`, Line: 24, Col: 19}
}
_, 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, 10, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

8
plugin.mod Normal file
View File

@ -0,0 +1,8 @@
[plugin]
name = "gotham"
display_name = "Gotham"
scope = "@themes"
version = "0.1.0"
description = "Dark, modern BlockNinja theme with bold typography and high-contrast accents."
kind = "plugin"
categories = ["templates"]

502
presets.json Normal file
View File

@ -0,0 +1,502 @@
[
{
"id": "dark-knight",
"name": "The Dark Knight",
"description": "Classic Gotham - dark with amber accents",
"theme": {
"lightColors": {
"background": "0 0% 100%",
"foreground": "0 0% 3.9%",
"card": "0 0% 100%",
"cardForeground": "0 0% 3.9%",
"popover": "0 0% 100%",
"popoverForeground": "0 0% 3.9%",
"primary": "45 93% 47%",
"primaryForeground": "0 0% 0%",
"secondary": "0 0% 96%",
"secondaryForeground": "0 0% 9%",
"muted": "0 0% 96%",
"mutedForeground": "0 0% 45%",
"accent": "45 93% 47%",
"accentForeground": "0 0% 0%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "0 0% 90%",
"input": "0 0% 90%",
"ring": "45 93% 47%"
},
"darkColors": {
"background": "0 0% 3.9%",
"foreground": "0 0% 98%",
"card": "0 0% 6%",
"cardForeground": "0 0% 98%",
"popover": "0 0% 6%",
"popoverForeground": "0 0% 98%",
"primary": "45 93% 47%",
"primaryForeground": "0 0% 0%",
"secondary": "0 0% 10%",
"secondaryForeground": "0 0% 98%",
"muted": "0 0% 10%",
"mutedForeground": "0 0% 60%",
"accent": "45 93% 47%",
"accentForeground": "0 0% 0%",
"destructive": "0 62% 30%",
"destructiveForeground": "0 0% 98%",
"border": "0 0% 14%",
"input": "0 0% 14%",
"ring": "45 93% 47%"
},
"mode": "dark"
}
},
{
"id": "joker",
"name": "The Joker",
"description": "Chaotic purple and toxic green",
"theme": {
"lightColors": {
"background": "0 0% 100%",
"foreground": "270 50% 15%",
"card": "0 0% 98%",
"cardForeground": "270 50% 15%",
"popover": "0 0% 98%",
"popoverForeground": "270 50% 15%",
"primary": "120 80% 35%",
"primaryForeground": "0 0% 100%",
"secondary": "270 40% 90%",
"secondaryForeground": "270 50% 15%",
"muted": "270 20% 95%",
"mutedForeground": "270 30% 40%",
"accent": "300 100% 70%",
"accentForeground": "0 0% 0%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "270 20% 85%",
"input": "270 20% 85%",
"ring": "120 80% 35%"
},
"darkColors": {
"background": "270 30% 8%",
"foreground": "120 100% 80%",
"card": "270 30% 12%",
"cardForeground": "120 100% 80%",
"popover": "270 30% 12%",
"popoverForeground": "120 100% 80%",
"primary": "120 100% 40%",
"primaryForeground": "270 30% 8%",
"secondary": "270 50% 20%",
"secondaryForeground": "120 100% 80%",
"muted": "270 30% 15%",
"mutedForeground": "270 20% 60%",
"accent": "300 100% 50%",
"accentForeground": "0 0% 0%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "270 30% 20%",
"input": "270 30% 20%",
"ring": "120 100% 40%"
},
"mode": "dark"
}
},
{
"id": "riddler",
"name": "The Riddler",
"description": "Bright neon green with purple mystery",
"theme": {
"lightColors": {
"background": "0 0% 100%",
"foreground": "0 0% 5%",
"card": "0 0% 98%",
"cardForeground": "0 0% 5%",
"popover": "0 0% 98%",
"popoverForeground": "0 0% 5%",
"primary": "120 70% 35%",
"primaryForeground": "0 0% 100%",
"secondary": "280 50% 85%",
"secondaryForeground": "280 80% 20%",
"muted": "0 0% 96%",
"mutedForeground": "120 30% 35%",
"accent": "280 80% 55%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "120 30% 85%",
"input": "0 0% 90%",
"ring": "120 70% 35%"
},
"darkColors": {
"background": "0 0% 4%",
"foreground": "120 100% 50%",
"card": "0 0% 8%",
"cardForeground": "120 100% 50%",
"popover": "0 0% 8%",
"popoverForeground": "120 100% 50%",
"primary": "120 100% 45%",
"primaryForeground": "0 0% 0%",
"secondary": "280 80% 30%",
"secondaryForeground": "120 100% 50%",
"muted": "0 0% 12%",
"mutedForeground": "120 50% 40%",
"accent": "280 100% 60%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "120 100% 20%",
"input": "0 0% 15%",
"ring": "120 100% 45%"
},
"mode": "dark"
}
},
{
"id": "harley-quinn",
"name": "Harley Quinn",
"description": "Bold red, black, and playful pink",
"theme": {
"lightColors": {
"background": "0 0% 100%",
"foreground": "0 0% 5%",
"card": "350 50% 98%",
"cardForeground": "0 0% 5%",
"popover": "350 50% 98%",
"popoverForeground": "0 0% 5%",
"primary": "350 85% 50%",
"primaryForeground": "0 0% 100%",
"secondary": "210 80% 90%",
"secondaryForeground": "210 100% 20%",
"muted": "0 0% 96%",
"mutedForeground": "350 20% 40%",
"accent": "330 100% 75%",
"accentForeground": "0 0% 0%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "350 30% 88%",
"input": "0 0% 90%",
"ring": "350 85% 50%"
},
"darkColors": {
"background": "0 0% 5%",
"foreground": "0 0% 95%",
"card": "350 80% 10%",
"cardForeground": "0 0% 95%",
"popover": "350 80% 10%",
"popoverForeground": "0 0% 95%",
"primary": "350 100% 55%",
"primaryForeground": "0 0% 100%",
"secondary": "210 100% 50%",
"secondaryForeground": "0 0% 100%",
"muted": "0 0% 12%",
"mutedForeground": "350 30% 60%",
"accent": "330 100% 65%",
"accentForeground": "0 0% 0%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "350 50% 20%",
"input": "0 0% 15%",
"ring": "350 100% 55%"
},
"mode": "dark"
}
},
{
"id": "mr-freeze",
"name": "Mr. Freeze",
"description": "Cold ice blue and frozen white",
"theme": {
"lightColors": {
"background": "0 0% 100%",
"foreground": "200 60% 10%",
"card": "200 40% 98%",
"cardForeground": "200 60% 10%",
"popover": "200 40% 98%",
"popoverForeground": "200 60% 10%",
"primary": "195 90% 45%",
"primaryForeground": "0 0% 100%",
"secondary": "200 40% 92%",
"secondaryForeground": "200 60% 10%",
"muted": "200 30% 95%",
"mutedForeground": "200 30% 40%",
"accent": "180 80% 60%",
"accentForeground": "200 60% 10%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "200 30% 88%",
"input": "200 30% 85%",
"ring": "195 90% 45%"
},
"darkColors": {
"background": "200 50% 8%",
"foreground": "200 100% 95%",
"card": "200 50% 12%",
"cardForeground": "200 100% 95%",
"popover": "200 50% 12%",
"popoverForeground": "200 100% 95%",
"primary": "195 100% 50%",
"primaryForeground": "200 50% 8%",
"secondary": "200 60% 20%",
"secondaryForeground": "200 100% 95%",
"muted": "200 40% 15%",
"mutedForeground": "200 40% 60%",
"accent": "180 100% 70%",
"accentForeground": "200 50% 8%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "200 40% 25%",
"input": "200 40% 20%",
"ring": "195 100% 50%"
},
"mode": "dark"
}
},
{
"id": "poison-ivy",
"name": "Poison Ivy",
"description": "Deep forest green with seductive red",
"theme": {
"lightColors": {
"background": "0 0% 100%",
"foreground": "140 50% 12%",
"card": "140 30% 98%",
"cardForeground": "140 50% 12%",
"popover": "140 30% 98%",
"popoverForeground": "140 50% 12%",
"primary": "140 60% 35%",
"primaryForeground": "0 0% 100%",
"secondary": "0 50% 92%",
"secondaryForeground": "0 70% 25%",
"muted": "140 20% 95%",
"mutedForeground": "140 25% 40%",
"accent": "0 70% 55%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "140 25% 88%",
"input": "140 20% 85%",
"ring": "140 60% 35%"
},
"darkColors": {
"background": "140 40% 6%",
"foreground": "140 60% 90%",
"card": "140 40% 10%",
"cardForeground": "140 60% 90%",
"popover": "140 40% 10%",
"popoverForeground": "140 60% 90%",
"primary": "140 70% 40%",
"primaryForeground": "0 0% 100%",
"secondary": "0 70% 40%",
"secondaryForeground": "0 0% 100%",
"muted": "140 30% 15%",
"mutedForeground": "140 30% 55%",
"accent": "0 80% 50%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "140 30% 20%",
"input": "140 30% 15%",
"ring": "140 70% 40%"
},
"mode": "dark"
}
},
{
"id": "two-face",
"name": "Two-Face",
"description": "Split personality - half dark, half light",
"theme": {
"lightColors": {
"background": "0 0% 100%",
"foreground": "0 0% 5%",
"card": "0 0% 98%",
"cardForeground": "0 0% 5%",
"popover": "0 0% 98%",
"popoverForeground": "0 0% 5%",
"primary": "0 0% 10%",
"primaryForeground": "0 0% 100%",
"secondary": "0 0% 92%",
"secondaryForeground": "0 0% 10%",
"muted": "0 0% 96%",
"mutedForeground": "0 0% 45%",
"accent": "45 100% 50%",
"accentForeground": "0 0% 0%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "0 0% 88%",
"input": "0 0% 85%",
"ring": "0 0% 10%"
},
"darkColors": {
"background": "0 0% 5%",
"foreground": "0 0% 95%",
"card": "0 0% 10%",
"cardForeground": "0 0% 95%",
"popover": "0 0% 10%",
"popoverForeground": "0 0% 95%",
"primary": "0 0% 95%",
"primaryForeground": "0 0% 5%",
"secondary": "0 0% 20%",
"secondaryForeground": "0 0% 95%",
"muted": "0 0% 15%",
"mutedForeground": "0 0% 55%",
"accent": "45 100% 50%",
"accentForeground": "0 0% 0%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "0 0% 25%",
"input": "0 0% 20%",
"ring": "0 0% 95%"
},
"mode": "dark"
}
},
{
"id": "catwoman",
"name": "Catwoman",
"description": "Sleek purple and black elegance",
"theme": {
"lightColors": {
"background": "0 0% 100%",
"foreground": "280 30% 15%",
"card": "280 20% 98%",
"cardForeground": "280 30% 15%",
"popover": "280 20% 98%",
"popoverForeground": "280 30% 15%",
"primary": "280 50% 45%",
"primaryForeground": "0 0% 100%",
"secondary": "280 25% 92%",
"secondaryForeground": "280 30% 15%",
"muted": "280 15% 96%",
"mutedForeground": "280 15% 40%",
"accent": "320 70% 60%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "280 20% 88%",
"input": "280 15% 85%",
"ring": "280 50% 45%"
},
"darkColors": {
"background": "280 20% 6%",
"foreground": "280 10% 90%",
"card": "280 20% 10%",
"cardForeground": "280 10% 90%",
"popover": "280 20% 10%",
"popoverForeground": "280 10% 90%",
"primary": "280 60% 55%",
"primaryForeground": "0 0% 100%",
"secondary": "280 30% 20%",
"secondaryForeground": "280 10% 90%",
"muted": "280 15% 15%",
"mutedForeground": "280 10% 55%",
"accent": "320 80% 60%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "280 20% 22%",
"input": "280 20% 18%",
"ring": "280 60% 55%"
},
"mode": "dark"
}
},
{
"id": "scarecrow",
"name": "Scarecrow",
"description": "Fear-inducing orange and decayed brown",
"theme": {
"lightColors": {
"background": "0 0% 100%",
"foreground": "30 40% 15%",
"card": "35 30% 98%",
"cardForeground": "30 40% 15%",
"popover": "35 30% 98%",
"popoverForeground": "30 40% 15%",
"primary": "25 85% 45%",
"primaryForeground": "0 0% 100%",
"secondary": "30 30% 92%",
"secondaryForeground": "30 40% 15%",
"muted": "30 20% 95%",
"mutedForeground": "30 25% 40%",
"accent": "15 90% 50%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "30 25% 88%",
"input": "30 20% 85%",
"ring": "25 85% 45%"
},
"darkColors": {
"background": "30 30% 6%",
"foreground": "35 60% 85%",
"card": "30 30% 10%",
"cardForeground": "35 60% 85%",
"popover": "30 30% 10%",
"popoverForeground": "35 60% 85%",
"primary": "25 90% 50%",
"primaryForeground": "0 0% 0%",
"secondary": "30 40% 20%",
"secondaryForeground": "35 60% 85%",
"muted": "30 25% 15%",
"mutedForeground": "30 20% 50%",
"accent": "15 100% 45%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "30 30% 20%",
"input": "30 30% 15%",
"ring": "25 90% 50%"
},
"mode": "dark"
}
},
{
"id": "bane",
"name": "Bane",
"description": "Military tactical green and black",
"theme": {
"lightColors": {
"background": "0 0% 100%",
"foreground": "100 30% 15%",
"card": "100 20% 98%",
"cardForeground": "100 30% 15%",
"popover": "100 20% 98%",
"popoverForeground": "100 30% 15%",
"primary": "100 45% 30%",
"primaryForeground": "0 0% 100%",
"secondary": "100 20% 92%",
"secondaryForeground": "100 30% 15%",
"muted": "100 15% 95%",
"mutedForeground": "100 20% 40%",
"accent": "45 75% 50%",
"accentForeground": "0 0% 0%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "100 20% 88%",
"input": "100 15% 85%",
"ring": "100 45% 30%"
},
"darkColors": {
"background": "100 20% 5%",
"foreground": "100 10% 85%",
"card": "100 20% 9%",
"cardForeground": "100 10% 85%",
"popover": "100 20% 9%",
"popoverForeground": "100 10% 85%",
"primary": "100 50% 35%",
"primaryForeground": "0 0% 100%",
"secondary": "100 25% 18%",
"secondaryForeground": "100 10% 85%",
"muted": "100 15% 14%",
"mutedForeground": "100 10% 50%",
"accent": "45 80% 45%",
"accentForeground": "0 0% 0%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 98%",
"border": "100 20% 18%",
"input": "100 20% 14%",
"ring": "100 50% 35%"
},
"mode": "dark"
}
}
]

116
register.go Normal file
View File

@ -0,0 +1,116 @@
package main
import (
"context"
"github.com/a-h/templ"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// wrap adapts a templ-returning render function to templates.TemplateFunc.
// templ.Component already implements templates.HTMLComponent via Render.
func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc {
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
return f(ctx, doc)
}
}
// Register is the plugin entry point that registers the Gotham template and blocks.
// Returns an error if template registration fails (e.g., validation failure).
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
// Register the Gotham system template with metadata
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
Key: "gotham",
Title: "Gotham",
Description: "Dark, modern theme with bold typography",
})
// Register page templates for the Gotham system template
if err := tr.RegisterPageTemplate("gotham", templates.PageTemplateMeta{
Key: "default",
Title: "Default",
Description: "Standard dark page layout with header, main content, and footer",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderGotham)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("gotham", templates.PageTemplateMeta{
Key: "landing",
Title: "Landing Page",
Description: "Full-width hero sections, ideal for marketing pages",
Slots: []string{"hero", "main", "cta", "footer"},
}, wrap(RenderGothamLanding)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("gotham", templates.PageTemplateMeta{
Key: "full-width",
Title: "Full Width",
Description: "Edge-to-edge content without max-width constraints",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderGothamFullWidth)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("gotham", templates.PageTemplateMeta{
Key: "centered",
Title: "Centered Content",
Description: "Narrow, centered content for articles and documentation",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderGothamCentered)); err != nil {
return err
}
// Register Gotham-specific blocks
br.Register(StatsBlockMeta, StatsBlock)
br.Register(FeaturesBlockMeta, FeaturesBlock)
br.Register(FooterBlockMeta, FooterBlock)
// Register overrides for built-in blocks when using Gotham template
br.RegisterTemplateOverride("gotham", "heading", GothamHeadingBlock)
br.RegisterTemplateOverride("gotham", "text", GothamTextBlock)
// Register email wrapper for branded communications
tr.RegisterEmailWrapper("gotham", GothamEmailWrapper)
return nil
}
// DefaultMasterPages returns the default master pages that Gotham provides.
// These are created automatically when the plugin is first loaded.
func DefaultMasterPages() []plugin.MasterPageDefinition {
return []plugin.MasterPageDefinition{
{
Key: "gotham:default-master",
Title: "Gotham Default Master",
PageTemplates: []string{"default", "centered"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "navbar",
Title: "Main Navigation",
Content: map[string]any{"menuName": "main"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Main Content Slot",
Content: map[string]any{"slotName": "main", "placeholder": "Page content will appear here"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "gotham:footer",
Title: "Site Footer",
Content: map[string]any{"showSignup": true},
Slot: "footer",
SortOrder: 0,
},
},
},
}
}

24
registration.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/templates"
"git.dev.alexdunmow.com/block/core/plugin"
)
// Registration is the compile-time plugin registration for the Gotham template.
var Registration = plugin.PluginRegistration{
Name: "gotham",
Version: plugin.ParseModVersion(pluginModBytes),
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
return Register(tr, br)
},
Assets: func() http.Handler { return AssetsHandler() },
Schemas: func() fs.FS { return Schemas() },
ThemePresets: func() []byte { return ThemePresets() },
BundledFonts: func() []byte { return BundledFonts() },
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
}

View File

@ -0,0 +1,55 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Features Block",
"description": "Grid of feature cards with icons and descriptions",
"type": "object",
"properties": {
"section_title": {
"type": "string",
"title": "Section Title",
"description": "Optional title above the feature cards",
"x-editor": "text"
},
"columns": {
"type": "integer",
"title": "Columns",
"description": "Number of columns (1-4)",
"x-editor": "number",
"minimum": 1,
"maximum": 4,
"default": 3
},
"features": {
"type": "array",
"title": "Features",
"description": "List of feature cards",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"icon": {
"type": "string",
"title": "Icon",
"description": "Icon name (chart, users, clock, star)",
"x-editor": "select",
"enum": ["", "chart", "users", "clock", "star"]
},
"title": {
"type": "string",
"title": "Title",
"description": "Feature title",
"x-editor": "text"
},
"description": {
"type": "string",
"title": "Description",
"description": "Feature description",
"x-editor": "textarea"
}
},
"required": ["title"]
}
}
}
}

View File

@ -0,0 +1,63 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Footer Block",
"description": "Multi-column footer with links and copyright",
"type": "object",
"properties": {
"copyright": {
"type": "string",
"title": "Copyright Text",
"description": "Copyright notice displayed at the bottom",
"default": "© 2025 Your Company. All rights reserved.",
"x-editor": "text"
},
"columns": {
"type": "array",
"title": "Footer Columns",
"description": "Columns with headings and links",
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Column Heading",
"description": "Title for this column",
"x-editor": "text"
},
"links": {
"type": "array",
"title": "Links",
"description": "Links in this column",
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"title": "Link Text",
"description": "Display text for the link",
"x-editor": "text"
},
"page_id": {
"type": "string",
"title": "Page",
"description": "Select an internal page (overrides URL)",
"x-editor": "page-select"
},
"url": {
"type": "string",
"title": "External URL",
"description": "External URL (used if no page selected)",
"x-editor": "url"
}
},
"required": ["text"]
}
}
},
"required": ["heading"]
}
}
}
}

39
schemas/hero.schema.json Normal file
View File

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Hero Block",
"description": "Large hero section with headline and optional CTA",
"type": "object",
"properties": {
"headline": {
"type": "string",
"title": "Headline",
"description": "Main headline text",
"x-editor": "text"
},
"subheadline": {
"type": "string",
"title": "Subheadline",
"description": "Supporting text below the headline",
"x-editor": "textarea"
},
"background_url": {
"type": "string",
"title": "Background Image URL",
"description": "URL for the background image",
"x-editor": "url"
},
"cta_text": {
"type": "string",
"title": "CTA Button Text",
"description": "Text for the call-to-action button",
"x-editor": "text"
},
"cta_url": {
"type": "string",
"title": "CTA Button URL",
"description": "Link for the call-to-action button",
"x-editor": "url"
}
},
"required": ["headline"]
}

View File

@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Stat Item",
"description": "A single statistic with value, label, and optional icon",
"type": "object",
"properties": {
"value": {
"type": "string",
"title": "Value",
"description": "The statistic value (e.g., '100+', '99%')",
"default": "0",
"x-editor": "text"
},
"label": {
"type": "string",
"title": "Label",
"description": "Description of the statistic",
"default": "Stat Label",
"x-editor": "text"
},
"icon": {
"type": "string",
"title": "Icon",
"description": "Icon name (chart, users, clock, star)",
"x-editor": "select",
"enum": ["", "chart", "users", "clock", "star"]
}
}
}

40
schemas/stats.schema.json Normal file
View File

@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Stats Block",
"description": "Display statistics with labels and values",
"type": "object",
"properties": {
"items": {
"type": "array",
"title": "Statistics",
"description": "List of statistic items to display",
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string",
"title": "Value",
"description": "The statistic value (e.g., '100+', '99%')",
"x-editor": "text"
},
"label": {
"type": "string",
"title": "Label",
"description": "Description of the statistic",
"x-editor": "text"
},
"icon": {
"type": "string",
"title": "Icon",
"description": "Icon name (chart, users, clock, star)",
"x-editor": "select",
"enum": ["", "chart", "users", "clock", "star"]
}
},
"required": ["value", "label"]
}
}
},
"required": []
}

35
stat_item.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// StatItemBlockMeta defines metadata for a single stat item.
var StatItemBlockMeta = blocks.BlockMeta{
Key: "stat_item",
Title: "Stat Item",
Description: "A single statistic with value, label, and optional icon",
Source: "gotham",
Hidden: true, // Only available as child of stats block
}
// StatItemBlock renders a single stat item.
// Content expects: {"value": "100+", "label": "Projects", "icon": "chart"}
func StatItemBlock(ctx context.Context, content map[string]any) string {
item := StatItem{
Value: getString(content, "value"),
Label: getString(content, "label"),
Icon: getString(content, "icon"),
}
if item.Value == "" && item.Label == "" {
return ""
}
var buf bytes.Buffer
_ = statItemComponent(item).Render(ctx, &buf)
return buf.String()
}

14
stat_item.templ Normal file
View File

@ -0,0 +1,14 @@
package main
// statItemComponent renders a single Gotham-styled stat item.
templ statItemComponent(stat StatItem) {
<div class="text-center p-6 rounded-lg gotham-border border">
if stat.Icon != "" {
<div class="mb-3 gotham-accent">
@iconSVG(stat.Icon)
</div>
}
<div class="text-4xl font-bold gotham-accent mb-2">{ stat.Value }</div>
<div class="text-muted-foreground uppercase text-sm tracking-wider">{ stat.Label }</div>
</div>
}

85
stat_item_templ.go Normal file
View File

@ -0,0 +1,85 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//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"
// statItemComponent renders a single Gotham-styled stat item.
func statItemComponent(stat StatItem) 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)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"text-center p-6 rounded-lg gotham-border border\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if stat.Icon != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mb-3 gotham-accent\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = iconSVG(stat.Icon).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"text-4xl font-bold gotham-accent mb-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stat.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `stat_item.templ`, Line: 11, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"text-muted-foreground uppercase text-sm tracking-wider\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stat.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `stat_item.templ`, Line: 12, Col: 82}
}
_, 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, 6, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

48
stats.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// StatsBlockMeta defines metadata for the stats counter block.
// Stats uses a schema-defined items array, not child blocks.
var StatsBlockMeta = blocks.BlockMeta{
Key: "stats",
Title: "Stats Counter",
Description: "Display statistics with labels and values",
Source: "gotham",
}
// StatsBlock renders a stats counter grid from content.items array.
func StatsBlock(ctx context.Context, content map[string]any) string {
items := getSlice(content, "items")
if len(items) == 0 {
// Empty state - show placeholder
var buf bytes.Buffer
_ = statsEmptyComponent().Render(ctx, &buf)
return buf.String()
}
var stats []StatItem
for _, item := range items {
stats = append(stats, StatItem{
Value: getString(item, "value"),
Label: getString(item, "label"),
Icon: getString(item, "icon"),
})
}
var buf bytes.Buffer
_ = statsComponent(stats).Render(ctx, &buf)
return buf.String()
}
// StatItem represents a single statistic.
type StatItem struct {
Value string
Label string
Icon string
}

84
stats.templ Normal file
View File

@ -0,0 +1,84 @@
package main
// statsContainerComponent renders the stats grid container with pre-rendered children.
templ statsContainerComponent(childCount int, childrenHTML string) {
<section class="py-16 gotham-surface flex-1">
<div class="max-w-6xl mx-auto px-4">
<div class={ "grid gap-8", gridCols(childCount) }>
@templ.Raw(childrenHTML)
</div>
</div>
</section>
}
// statsEmptyComponent renders an empty placeholder for stats without items.
templ statsEmptyComponent() {
<section class="py-16 gotham-surface flex-1">
<div class="max-w-6xl mx-auto px-4">
<div class="text-center p-8 border border-dashed border-border rounded-lg">
<p class="text-muted-foreground">Click + to add stat items</p>
</div>
</div>
</section>
}
// statsComponent renders a Gotham-styled stats counter grid (legacy content-based).
templ statsComponent(items []StatItem) {
<section class="py-16 gotham-surface flex-1">
<div class="max-w-6xl mx-auto px-4">
<div class={ "grid gap-8", gridCols(len(items)) }>
for _, stat := range items {
<div class="text-center p-6 rounded-lg gotham-border border">
if stat.Icon != "" {
<div class="mb-3 gotham-accent">
@iconSVG(stat.Icon)
</div>
}
<div class="text-4xl font-bold gotham-accent mb-2">{ stat.Value }</div>
<div class="text-muted-foreground uppercase text-sm tracking-wider">{ stat.Label }</div>
</div>
}
</div>
</div>
</section>
}
// gridCols returns the appropriate grid column class based on item count.
func gridCols(count int) string {
switch count {
case 1:
return "grid-cols-1"
case 2:
return "grid-cols-1 md:grid-cols-2"
case 3:
return "grid-cols-1 md:grid-cols-3"
default:
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
}
}
// iconSVG renders a simple icon based on name.
templ iconSVG(name string) {
switch name {
case "chart":
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
case "users":
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
case "clock":
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
case "star":
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
</svg>
default:
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
}
}

274
stats_templ.go Normal file
View File

@ -0,0 +1,274 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//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"
// statsContainerComponent renders the stats grid container with pre-rendered children.
func statsContainerComponent(childCount int, childrenHTML string) 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)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"py-16 gotham-surface flex-1\"><div class=\"max-w-6xl mx-auto px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 = []any{"grid gap-8", gridCols(childCount)}
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, 2, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `stats.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
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
}
templ_7745c5c3_Err = templ.Raw(childrenHTML).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// statsEmptyComponent renders an empty placeholder for stats without items.
func statsEmptyComponent() 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_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<section class=\"py-16 gotham-surface flex-1\"><div class=\"max-w-6xl mx-auto px-4\"><div class=\"text-center p-8 border border-dashed border-border rounded-lg\"><p class=\"text-muted-foreground\">Click + to add stat items</p></div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// statsComponent renders a Gotham-styled stats counter grid (legacy content-based).
func statsComponent(items []StatItem) 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_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<section class=\"py-16 gotham-surface flex-1\"><div class=\"max-w-6xl mx-auto px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 = []any{"grid gap-8", gridCols(len(items))}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var6).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `stats.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, stat := range items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"text-center p-6 rounded-lg gotham-border border\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if stat.Icon != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"mb-3 gotham-accent\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = iconSVG(stat.Icon).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"text-4xl font-bold gotham-accent mb-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(stat.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `stats.templ`, Line: 37, Col: 69}
}
_, 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, 13, "</div><div class=\"text-muted-foreground uppercase text-sm tracking-wider\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(stat.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `stats.templ`, Line: 38, Col: 86}
}
_, 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, 14, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// gridCols returns the appropriate grid column class based on item count.
func gridCols(count int) string {
switch count {
case 1:
return "grid-cols-1"
case 2:
return "grid-cols-1 md:grid-cols-2"
case 3:
return "grid-cols-1 md:grid-cols-3"
default:
return "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
}
}
// iconSVG renders a simple icon based on name.
func iconSVG(name string) 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_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch name {
case "chart":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"w-10 h-10 mx-auto\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "users":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"w-10 h-10 mx-auto\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "clock":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"w-10 h-10 mx-auto\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "star":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"w-10 h-10 mx-auto\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"w-10 h-10 mx-auto\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 10V3L4 14h7v7l9-11h-7z\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

239
template.templ Normal file
View File

@ -0,0 +1,239 @@
package main
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
type PageData struct {
Title string
Slots map[string]string
ThemeMode string
ThemeCSS string
SiteSettings bn.SiteSettingsData
PageMeta bn.PageMeta
StructuredData string
CSSHash string
PageviewNonce string
EngagementConfig bn.EngagementConfig
}
func parseGothamPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{Title: title, Slots: slots, ThemeMode: themeMode, ThemeCSS: themeCSS, SiteSettings: siteSettings, PageMeta: pageMeta, StructuredData: structuredData, CSSHash: cssHash, PageviewNonce: pageviewNonce, EngagementConfig: engagementConfig}
}
// Default Gotham page template
templ Gotham(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/gotham/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow max-w-4xl mx-auto w-full px-4 py-8">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center">
<p class="text-muted-foreground">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Landing page template - full-width hero sections for marketing pages
templ GothamLanding(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/gotham/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
// Hero section - full width
<section class="w-full">
@templ.Raw(data.Slots["hero"])
</section>
// Main content
<main class="flex-grow">
if main, ok := data.Slots["main"]; ok && main != "" {
<div class="max-w-6xl mx-auto px-4 py-16">
@templ.Raw(main)
</div>
}
</main>
// Call to action section - full width
<section class="w-full">
@templ.Raw(data.Slots["cta"])
</section>
// Footer
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Full-width page template - edge-to-edge content
templ GothamFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/gotham/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="max-w-4xl mx-auto py-20 px-4 text-center">
<p class="text-muted-foreground">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Centered content page template - narrow, centered layout for articles
templ GothamCentered(data PageData) {
<!DOCTYPE html>
<html lang="en" class="dark">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/gotham/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full border-b border-border">
<div class="max-w-2xl mx-auto px-4">
@templ.Raw(data.Slots["header"])
</div>
</header>
<main class="flex-grow max-w-2xl mx-auto w-full px-4 py-12">
if main, ok := data.Slots["main"]; ok && main != "" {
<article class="prose prose-invert prose-lg max-w-none">
@templ.Raw(main)
</article>
} else {
<div class="py-20 text-center">
<p class="text-muted-foreground">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
func RenderGotham(ctx context.Context, doc map[string]any) templ.Component {
return Gotham(parseGothamPageData(doc))
}
func RenderGothamLanding(ctx context.Context, doc map[string]any) templ.Component {
return GothamLanding(parseGothamPageData(doc))
}
func RenderGothamFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return GothamFullWidth(parseGothamPageData(doc))
}
func RenderGothamCentered(ctx context.Context, doc map[string]any) templ.Component {
return GothamCentered(parseGothamPageData(doc))
}

491
template_templ.go Normal file
View File

@ -0,0 +1,491 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//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"
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
type PageData struct {
Title string
Slots map[string]string
ThemeMode string
ThemeCSS string
SiteSettings bn.SiteSettingsData
PageMeta bn.PageMeta
StructuredData string
CSSHash string
PageviewNonce string
EngagementConfig bn.EngagementConfig
}
func parseGothamPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{Title: title, Slots: slots, ThemeMode: themeMode, ThemeCSS: themeCSS, SiteSettings: siteSettings, PageMeta: pageMeta, StructuredData: structuredData, CSSHash: cssHash, PageviewNonce: pageviewNonce, EngagementConfig: engagementConfig}
}
// Default Gotham page template
func Gotham(data PageData) 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)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"dark\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/gotham/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</header><main class=\"flex-grow max-w-4xl mx-auto w-full px-4 py-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"py-20 text-center\"><p class=\"text-muted-foreground\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</main><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Landing page template - full-width hero sections for marketing pages
func GothamLanding(data PageData) 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_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<!doctype html><html lang=\"en\" class=\"dark\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/gotham/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<section class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["hero"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</section><main class=\"flex-grow\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"max-w-6xl mx-auto px-4 py-16\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</main><section class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["cta"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</section><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Full-width page template - edge-to-edge content
func GothamFullWidth(data PageData) 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_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<!doctype html><html lang=\"en\" class=\"dark\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/gotham/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</header><main class=\"flex-grow w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"max-w-4xl mx-auto py-20 px-4 text-center\"><p class=\"text-muted-foreground\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</main><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Centered content page template - narrow, centered layout for articles
func GothamCentered(data PageData) 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_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<!doctype html><html lang=\"en\" class=\"dark\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/gotham/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<header class=\"w-full border-b border-border\"><div class=\"max-w-2xl mx-auto px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></header><main class=\"flex-grow max-w-2xl mx-auto w-full px-4 py-12\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<article class=\"prose prose-invert prose-lg max-w-none\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div class=\"py-20 text-center\"><p class=\"text-muted-foreground\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</main><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func RenderGotham(ctx context.Context, doc map[string]any) templ.Component {
return Gotham(parseGothamPageData(doc))
}
func RenderGothamLanding(ctx context.Context, doc map[string]any) templ.Component {
return GothamLanding(parseGothamPageData(doc))
}
func RenderGothamFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return GothamFullWidth(parseGothamPageData(doc))
}
func RenderGothamCentered(ctx context.Context, doc map[string]any) templ.Component {
return GothamCentered(parseGothamPageData(doc))
}
var _ = templruntime.GeneratedTemplate

17
text_override.go Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"bytes"
"context"
)
// GothamTextBlock renders text with Gotham styling.
// Uses the same schema as the base text block.
func GothamTextBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
class := getString(content, "class")
var buf bytes.Buffer
_ = gothamTextComponent(text, class).Render(ctx, &buf)
return buf.String()
}

8
text_override.templ Normal file
View File

@ -0,0 +1,8 @@
package main
// gothamTextComponent renders text with Gotham's dark theme styling.
templ gothamTextComponent(text, class string) {
<div class={ "prose prose-invert prose-amber max-w-none flex-1", class }>
@templ.Raw(text)
</div>
}

67
text_override_templ.go Normal file
View File

@ -0,0 +1,67 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//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"
// gothamTextComponent renders text with Gotham's dark theme styling.
func gothamTextComponent(text, class string) 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)
var templ_7745c5c3_Var2 = []any{"prose prose-invert prose-amber max-w-none flex-1", class}
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, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `text_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(text).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate