Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63e396e7d0 | ||
|
|
6a636e1798 | ||
|
|
8d863aa09e | ||
|
|
3c42d1348f | ||
|
|
22b62b0ffc | ||
|
|
539b4a5302 | ||
|
|
f3328a9ae9 | ||
|
|
ff2c30103b | ||
|
|
e242c44743 | ||
|
|
f6a21c1295 | ||
|
|
16844f6a94 | ||
|
|
f5af05fd5f | ||
|
|
db13a889f5 | ||
|
|
543b99531b | ||
|
|
3f9b3e564a | ||
|
|
6b8367377b | ||
|
|
486d3a283e | ||
|
|
c341fb9645 | ||
|
|
0911a76f2e | ||
|
|
8da60d7d5a | ||
|
|
9955f5f002 | ||
|
|
9adbbb9199 | ||
|
|
550855e5a6 | ||
|
|
28256490c1 | ||
|
|
b4781d3416 | ||
|
|
dfe3bd780d | ||
|
|
38cd309b40 | ||
|
|
316a888b3f | ||
|
|
0db2a2114e |
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.so
|
||||||
|
*.bnp
|
||||||
218
Makefile
@ -1,191 +1,55 @@
|
|||||||
# Gotham — build & deploy helpers (.so plugin workflow)
|
# gotham — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
|
||||||
#
|
#
|
||||||
# The plugin compiles to a .so shared object loaded by the CMS at runtime.
|
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
|
||||||
# `make rebuild` copies source to the container, builds the .so, and restarts.
|
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
|
||||||
|
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
|
||||||
|
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
|
||||||
|
# classifies the repo as codeless (no Go at the root) and packs the artifact
|
||||||
|
# with manifest.pb (codeless: true) and NO plugin.wasm.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# make rebuild # Full rebuild: frontend + .so + CSS + migrations, restart
|
# make build # pack gotham-<version>.bnp (codeless — no wasm)
|
||||||
# make backend # Build .so + migrations, restart
|
# make verify # validate the built .bnp (loader-identical checks)
|
||||||
# make build-css # Rebuild Tailwind CSS
|
# make archive-check # prove a clean `git archive HEAD` still builds codeless
|
||||||
# make logs # Tail instance logs
|
# make publish # ninja plugin publish --bnp (to the active registry)
|
||||||
# make status # Show instance container status
|
# make clean # remove build artefacts
|
||||||
|
|
||||||
.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
|
.PHONY: build verify archive-check publish clean help
|
||||||
|
|
||||||
# Paths
|
|
||||||
BLOCKNINJA_DIR := $(HOME)/src/blockninja
|
|
||||||
PLUGIN_SRC := $(CURDIR)
|
|
||||||
PLUGIN_NAME := gotham
|
PLUGIN_NAME := gotham
|
||||||
MIGRATIONS_SRC := $(BLOCKNINJA_DIR)/backend/sql/migrations
|
NINJA := ninja
|
||||||
GO_BUILDER := localhost/blockninja-go-builder:latest
|
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
|
||||||
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.
|
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
|
||||||
all: $(PLUGIN_NAME).so
|
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
|
||||||
|
build:
|
||||||
|
$(NINJA) plugin build --dir .
|
||||||
|
|
||||||
# Local plugin build (no container). Useful for CI / quick checks.
|
# Validate the built .bnp exactly as the registry/loader will.
|
||||||
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
|
verify:
|
||||||
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
|
$(NINJA) plugin verify $(BNP)
|
||||||
|
|
||||||
# Ensure blockninja core services and the instance container are running.
|
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
|
||||||
spinup:
|
archive-check:
|
||||||
$(MAKE) -C $(BLOCKNINJA_DIR) spinup
|
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
|
||||||
|
trap 'rm -rf "$$T"' EXIT && \
|
||||||
|
git archive HEAD | tar -x -C "$$T" && \
|
||||||
|
cd "$$T" && $(NINJA) plugin build --dir . && \
|
||||||
|
echo "archive-check OK"
|
||||||
|
|
||||||
# Full rebuild: frontend + .so plugin + CSS + migrations, restart.
|
# Publish the prebuilt .bnp to the active registry.
|
||||||
rebuild: spinup
|
# Pass --host https://my.blockninja.dev for the dev orchestrator.
|
||||||
$(MAKE) build-frontend
|
publish: build
|
||||||
$(MAKE) build-base-binary
|
$(NINJA) plugin publish --bnp $(BNP)
|
||||||
$(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.
|
# Remove build artefacts.
|
||||||
backend: spinup
|
clean:
|
||||||
$(MAKE) build-base-binary
|
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
|
||||||
$(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:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " all Build $(PLUGIN_NAME).so locally (default)"
|
@echo " build Pack the codeless .bnp (no wasm)"
|
||||||
@echo " spinup Start blockninja core services + instance container if stopped"
|
@echo " verify Validate the built .bnp (loader-identical checks)"
|
||||||
@echo " rebuild Full rebuild: frontend + .so + CSS + migrations, restart"
|
@echo " archive-check Prove a clean git archive HEAD packs codeless"
|
||||||
@echo " backend Build .so + migrations, restart"
|
@echo " publish Publish the .bnp to the active registry"
|
||||||
@echo " build-frontend Build host admin UI, deploy to container"
|
@echo " clean Remove build artefacts"
|
||||||
@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"
|
|
||||||
|
|||||||
42
README.md
@ -1,36 +1,24 @@
|
|||||||
# Gotham
|
# Gotham
|
||||||
|
|
||||||
Dark, modern BlockNinja theme plugin. Bold typography, high-contrast accents,
|
An after-dark theme in near-black with brass hairline accents. Tall condensed headlines sit over letterboxed hero bars, so pages carry the mood of a private members' club after midnight. It reads exclusive and filmic. The look is cinematic luxe, the kind of page that feels like an invitation.
|
||||||
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
|
## Good for
|
||||||
|
|
||||||
- `default` — header / main / footer
|
We built Gotham for nightlife and hospitality. Treat these as starting points, not limits. If the mood fits your brand, use it:
|
||||||
- `landing` — hero / main / cta / footer
|
|
||||||
- `full-width` — edge-to-edge header / main / footer
|
|
||||||
- `centered` — narrow centered content for articles and docs
|
|
||||||
|
|
||||||
## Blocks
|
- Nightclubs and late-night lounges
|
||||||
|
- Private members' clubs
|
||||||
|
- Cocktail bars and speakeasies
|
||||||
|
- Event spaces and ticketed shows
|
||||||
|
|
||||||
- `gotham:stats` — stat row with configurable items
|
## What you get
|
||||||
- `gotham:features` — feature grid
|
|
||||||
- `gotham:footer` — branded footer with optional signup form
|
|
||||||
|
|
||||||
## Build
|
- Five color presets, each with a tuned light mode and a tuned dark mode: Velvet Black, Midnight Brass, Noir Rose, Smoke & Silver, and Bourbon.
|
||||||
|
- Four bundled fonts, ready to assign: Antonio for condensed display headings, Cormorant Garamond for elegant serif text, Inter for interface copy, and IBM Plex Mono. Fonts stay admin-controlled, so you can swap them from the typography settings.
|
||||||
|
- Four persona blocks: a door strip for guest-list and entry details, an events marquee, a membership-tiers section, and a bottle-service menu.
|
||||||
|
- A demo site that installs when you activate the theme: a home page, an about page, a short blog with three posts, and a contact page whose form emails your admins.
|
||||||
|
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
|
||||||
|
|
||||||
```
|
## Install
|
||||||
make # builds gotham.so locally
|
|
||||||
make rebuild # rebuilds plugin inside an instance container
|
|
||||||
```
|
|
||||||
|
|
||||||
## Versioning
|
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully dressed starting point.
|
||||||
|
|
||||||
```
|
|
||||||
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.
|
|
||||||
|
|||||||
29
RECOMMENDED_FONTS.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Recommended fonts — Gotham
|
||||||
|
|
||||||
|
Gotham is admin-font-controlled. Every family is consumed through
|
||||||
|
`var(--font-heading|body|mono, <fallback>)`, so nothing breaks if you assign a
|
||||||
|
different family. These are the intended stacks.
|
||||||
|
|
||||||
|
Four OFL families ship bundled in `assets/fonts/` (see `LICENSES.md`) and appear
|
||||||
|
in the admin font picker as `source=template` rows. Assign them in
|
||||||
|
Admin -> Appearance -> Typography.
|
||||||
|
|
||||||
|
| Role | Variable | Assign | Bundled | Fallback |
|
||||||
|
|------|----------|--------|---------|----------|
|
||||||
|
| Headings | `--font-heading` | **Antonio** (or Oswald / Bebas Neue) | yes | `'Antonio', 'Oswald', 'Bebas Neue', sans-serif` |
|
||||||
|
| Body | `--font-body` | **Cormorant Garamond** | yes | `'Cormorant Garamond', 'Cormorant', Georgia, serif` |
|
||||||
|
| Meta / labels | `--font-body` accents | **Inter** | yes | `Inter, system-ui, sans-serif` |
|
||||||
|
| Mono | `--font-mono` | **IBM Plex Mono** | yes | `'IBM Plex Mono', ui-monospace, monospace` |
|
||||||
|
|
||||||
|
## Why these
|
||||||
|
|
||||||
|
- **Antonio** is a tall condensed sans. It carries the letterboxed marquee
|
||||||
|
headlines. Oswald and Bebas Neue are the intended substitutes if you prefer to
|
||||||
|
assign from the Google Fonts tab.
|
||||||
|
- **Cormorant Garamond** is the elegant serif body voice — the invitation card,
|
||||||
|
not the neon sign.
|
||||||
|
- **Inter** handles small meta: labels, eyebrows, form fields.
|
||||||
|
- **IBM Plex Mono** covers any monospace surface (times, codes, run sheets).
|
||||||
|
|
||||||
|
Keep the condensed display and the serif body paired. That contrast is the
|
||||||
|
theme's typographic signature.
|
||||||
21
RECOMMENDED_ICONS.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Recommended icons — Gotham
|
||||||
|
|
||||||
|
Gotham renders icons from the sprite system only, as
|
||||||
|
`<svg><use href="/icons/<pack>.svg#<name>"/></svg>` (or the `::pack:name:size::`
|
||||||
|
template shorthand). It never inlines SVG paths.
|
||||||
|
|
||||||
|
## Required pack
|
||||||
|
|
||||||
|
- **Lucide** (`lucide`) — the theme's default pack. Declared in
|
||||||
|
`plugin.mod` `required_icon_packs = ["lucide"]`.
|
||||||
|
|
||||||
|
Lucide ships bundled with the CMS, so no manual install is normally needed. If a
|
||||||
|
theme icon renders empty, open `/admin/icons`, confirm **Lucide** is installed
|
||||||
|
and enabled, then reactivate the theme.
|
||||||
|
|
||||||
|
## Icons the theme references
|
||||||
|
|
||||||
|
`lucide:diamond`, `lucide:arrow-right`, `lucide:check`, `lucide:chevron-right`
|
||||||
|
appear in seed content and block defaults. All resolve from the bundled Lucide
|
||||||
|
pack. Any icon-picker field defaults to a Lucide glyph so the theme renders
|
||||||
|
without further setup.
|
||||||
16
assets/fonts/LICENSES.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Bundled font licenses
|
||||||
|
|
||||||
|
All bundled woff2 files in this directory are latin-subset builds redistributed
|
||||||
|
under the SIL Open Font License 1.1 (OFL). No modifications were made beyond
|
||||||
|
subsetting to the latin range.
|
||||||
|
|
||||||
|
| Family | Files | License | Source |
|
||||||
|
|--------|-------|---------|--------|
|
||||||
|
| Antonio | antonio-latin.woff2, antonio-latin-ext.woff2 | SIL OFL 1.1 | Vernon Adams / Google Fonts |
|
||||||
|
| Cormorant Garamond | cormorant-garamond-400.woff2, cormorant-garamond-400-italic.woff2, cormorant-garamond-600.woff2, cormorant-garamond-700.woff2 | SIL OFL 1.1 | Christian Thalmann / Google Fonts |
|
||||||
|
| Inter | inter-400.woff2, inter-500.woff2, inter-700.woff2 | SIL OFL 1.1 | Rasmus Andersson / Google Fonts |
|
||||||
|
| IBM Plex Mono | ibm-plex-mono-400.woff2, ibm-plex-mono-500.woff2 | SIL OFL 1.1 | IBM / Google Fonts |
|
||||||
|
|
||||||
|
Full OFL text: https://openfontlicense.org/. Font-family is still admin-controlled;
|
||||||
|
these bundled families appear as `source=template` rows in the fonts picker and
|
||||||
|
are consumed only via `var(--font-heading|body|mono, <fallback>)`.
|
||||||
BIN
assets/fonts/antonio-latin-ext.woff2
Normal file
BIN
assets/fonts/antonio-latin.woff2
Normal file
BIN
assets/fonts/cormorant-garamond-400-italic.woff2
Normal file
BIN
assets/fonts/cormorant-garamond-400.woff2
Normal file
BIN
assets/fonts/cormorant-garamond-600.woff2
Normal file
BIN
assets/fonts/cormorant-garamond-700.woff2
Normal file
BIN
assets/fonts/ibm-plex-mono-400.woff2
Normal file
BIN
assets/fonts/ibm-plex-mono-500.woff2
Normal file
BIN
assets/fonts/inter-400.woff2
Normal file
BIN
assets/fonts/inter-500.woff2
Normal file
BIN
assets/fonts/inter-700.woff2
Normal file
@ -1 +0,0 @@
|
|||||||
/* placeholder for fonts */
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
/* 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);
|
|
||||||
}
|
|
||||||
32
blocks/blocks.yaml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Gotham theme block definitions (codeless .bnp, WO-TF-018).
|
||||||
|
# Only TRUE persona furniture remains as theme blocks. Everything that
|
||||||
|
# duplicated a builtin (hero, stats, feature grid, footer, gallery, divider,
|
||||||
|
# quote, testimonial...) is now a template OVERRIDE of the builtin key under
|
||||||
|
# templates/overrides/gotham/. Keys are fully qualified (gotham:<key>) — the
|
||||||
|
# codeless loader registers definitions verbatim; the qualified keys match the
|
||||||
|
# data-block attributes, master_pages.json and seed references.
|
||||||
|
blocks:
|
||||||
|
- key: gotham:door_strip
|
||||||
|
title: Door Strip
|
||||||
|
description: Sticky near-black door strip with brass rules — reservations line, guest-list CTA and tonight's hours.
|
||||||
|
category: navigation
|
||||||
|
schema: door_strip.schema.json
|
||||||
|
template: door_strip.ninjatpl
|
||||||
|
- key: gotham:events_marquee
|
||||||
|
title: Events Marquee
|
||||||
|
description: Letterboxed line-up of nights and residencies, each with date, room and a booking link.
|
||||||
|
category: content
|
||||||
|
schema: events_marquee.schema.json
|
||||||
|
template: events_marquee.ninjatpl
|
||||||
|
- key: gotham:membership_tiers
|
||||||
|
title: Membership Tiers
|
||||||
|
description: Side-by-side membership cards with a brass-framed feature list and an apply link.
|
||||||
|
category: content
|
||||||
|
schema: membership_tiers.schema.json
|
||||||
|
template: membership_tiers.ninjatpl
|
||||||
|
- key: gotham:bottle_menu
|
||||||
|
title: Bottle Menu
|
||||||
|
description: Cellar and bottle-service list grouped into sections, each pour with a note and price.
|
||||||
|
category: content
|
||||||
|
schema: bottle_menu.schema.json
|
||||||
|
template: bottle_menu.ninjatpl
|
||||||
29
blocks/bottle_menu.ninjatpl
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{# bottle_menu — cellar list. sections[] and section.items[] are arrays of maps. #}
|
||||||
|
<section data-block="gotham:bottle_menu" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-4xl px-4">
|
||||||
|
<div class="text-center reveal">
|
||||||
|
{% if eyebrow %}<div class="mb-4 flex items-center justify-center gap-4"><span class="reel-rule w-10"></span><span class="reel-caps text-xs text-primary">{{ eyebrow }}</span><span class="reel-rule w-10"></span></div>{% endif %}
|
||||||
|
{% if title %}<h2 class="reel-caps text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);color:hsl(var(--foreground));">{{ title }}</h2>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if sections %}
|
||||||
|
<div class="mt-12 space-y-12">
|
||||||
|
{% for section in sections %}
|
||||||
|
<div class="reveal">
|
||||||
|
{% if section.title %}<h3 class="reel-caps reel-underline w-fit text-lg text-primary" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);">{{ section.title }}</h3>{% endif %}
|
||||||
|
<ul class="mt-5 space-y-4">
|
||||||
|
{% for pour in section.items %}
|
||||||
|
<li class="flex items-baseline gap-4">
|
||||||
|
<span class="shrink-0" style="font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);color:hsl(var(--foreground));font-size:1.05rem;">{{ pour.name }}</span>
|
||||||
|
<span class="reel-rule flex-1 translate-y-[-2px]" style="opacity:0.4;"></span>
|
||||||
|
{% if pour.price %}<span class="shrink-0 reel-caps text-sm text-primary">{{ pour.price }}</span>{% endif %}
|
||||||
|
{% if pour.note %}<span class="hidden shrink-0 basis-full text-sm italic sm:block sm:basis-auto" style="color:hsl(var(--muted-foreground));">{{ pour.note }}</span>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if footnote %}<p class="mt-10 text-center text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ footnote }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
36
blocks/bottle_menu.schema.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Bottle Menu",
|
||||||
|
"description": "Cellar and bottle-service list grouped into sections.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"eyebrow": { "type": "string", "title": "Eyebrow", "x-editor": "text", "default": "The cellar" },
|
||||||
|
"title": { "type": "string", "title": "Section title", "x-editor": "text", "default": "Bottle service" },
|
||||||
|
"footnote": { "type": "string", "title": "Footnote", "x-editor": "text", "default": "" },
|
||||||
|
"sections": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Sections",
|
||||||
|
"x-editor": "array",
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": { "type": "string", "title": "Section title", "x-editor": "text" },
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Pours",
|
||||||
|
"x-editor": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string", "title": "Name", "x-editor": "text" },
|
||||||
|
"note": { "type": "string", "title": "Note", "x-editor": "text" },
|
||||||
|
"price": { "type": "string", "title": "Price", "x-editor": "text" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
blocks/door_strip.ninjatpl
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{# door_strip — sticky brass-on-black door strip. Iterates hours[] (array of strings). #}
|
||||||
|
<div data-block="gotham:door_strip" class="sticky top-0 z-30 w-full film-grain reel-strip" style="background-color: hsl(var(--foreground)); color: hsl(var(--background));">
|
||||||
|
<div class="max-w-6xl mx-auto px-4 py-2 flex flex-wrap items-center justify-between gap-3 text-sm">
|
||||||
|
<div class="flex items-center gap-3 reel-caps text-xs" style="font-family: var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif); color: hsl(var(--primary));">
|
||||||
|
<span>{{ label|default:"The Door" }}</span>
|
||||||
|
{% if phone %}<span aria-hidden="true" style="color: hsl(var(--primary) / 0.5);">/</span><a href="tel:{{ phone }}" class="reel-link" style="border-bottom-color: hsl(var(--primary));">{{ phone }}</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if hours %}<div class="hidden sm:flex flex-wrap items-center gap-3" style="color: hsl(var(--background));">{% for h in hours %}{% if not forloop.First %}<span aria-hidden="true" style="color: hsl(var(--primary) / 0.5);">/</span>{% endif %}<span>{{ h.line }}</span>{% endfor %}</div>{% endif %}
|
||||||
|
{% if reserveUrl %}<a href="{{ reserveUrl }}" class="reel-btn" style="background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary));">{{ reserveLabel|default:"Guest list" }}</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
48
blocks/door_strip.schema.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Door Strip",
|
||||||
|
"description": "Sticky near-black door strip with brass rules, reservations line, guest-list CTA and tonight's hours.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Strip label",
|
||||||
|
"description": "Short label at the head of the strip, e.g. 'The Door'.",
|
||||||
|
"x-editor": "text",
|
||||||
|
"default": "The Door"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Reservations phone",
|
||||||
|
"description": "International-format number for the reservations desk.",
|
||||||
|
"x-editor": "text",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"reserveLabel": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Reserve button label",
|
||||||
|
"x-editor": "text",
|
||||||
|
"default": "Guest list"
|
||||||
|
},
|
||||||
|
"reserveUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Reserve / guest-list URL",
|
||||||
|
"description": "Link to the booking or guest-list page.",
|
||||||
|
"x-editor": "link",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"hours": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Door hours",
|
||||||
|
"description": "Free-form lines such as 'Thu-Sat - 9pm till late'.",
|
||||||
|
"x-editor": "array",
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"line": { "type": "string", "title": "Line", "x-editor": "text" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
blocks/events_marquee.ninjatpl
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{# events_marquee — letterboxed line-up. Iterates events[] (array of maps). #}
|
||||||
|
<section data-block="gotham:events_marquee" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-5xl px-4">
|
||||||
|
<div class="text-center reveal">
|
||||||
|
{% if eyebrow %}<div class="mb-4 flex items-center justify-center gap-4"><span class="reel-rule w-10"></span><span class="reel-caps text-xs text-primary">{{ eyebrow }}</span><span class="reel-rule w-10"></span></div>{% endif %}
|
||||||
|
{% if title %}<h2 class="reel-caps text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);color:hsl(var(--foreground));">{{ title }}</h2>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if events %}
|
||||||
|
<ul class="mt-10 divide-y divide-[hsl(var(--border))] border-y border-[hsl(var(--border))]">
|
||||||
|
{% for event in events %}
|
||||||
|
<li class="reveal grid grid-cols-1 gap-1 py-5 sm:grid-cols-[8rem_1fr_auto] sm:items-baseline sm:gap-6">
|
||||||
|
<span class="reel-caps text-sm text-primary" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);">{{ event.date }}</span>
|
||||||
|
<div>
|
||||||
|
<span class="reel-caps text-lg" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);color:hsl(var(--foreground));">{{ event.name }}</span>
|
||||||
|
{% if event.room %}<span class="ml-3 text-sm" style="color:hsl(var(--muted-foreground));">{{ event.room }}</span>{% endif %}
|
||||||
|
{% if event.note %}<p class="mt-1 text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ event.note }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if event.url %}<a href="{{ event.url }}" class="reel-link inline-flex items-center gap-1 justify-self-start text-sm sm:justify-self-end">Book<svg class="inline-block h-4 w-4" width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
36
blocks/events_marquee.schema.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Events Marquee",
|
||||||
|
"description": "Letterboxed line-up of nights and residencies.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"eyebrow": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Eyebrow",
|
||||||
|
"x-editor": "text",
|
||||||
|
"default": "The programme"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Section title",
|
||||||
|
"x-editor": "text",
|
||||||
|
"default": "Tonight and after"
|
||||||
|
},
|
||||||
|
"events": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Events",
|
||||||
|
"x-editor": "array",
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"date": { "type": "string", "title": "Date", "x-editor": "text" },
|
||||||
|
"name": { "type": "string", "title": "Name", "x-editor": "text" },
|
||||||
|
"room": { "type": "string", "title": "Room", "x-editor": "text" },
|
||||||
|
"note": { "type": "string", "title": "Note", "x-editor": "text" },
|
||||||
|
"url": { "type": "string", "title": "Booking URL", "x-editor": "link" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
blocks/membership_tiers.ninjatpl
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{# membership_tiers — brass-framed membership cards. tiers[] and tier.features[] are arrays of maps. #}
|
||||||
|
<section data-block="gotham:membership_tiers" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
<div class="text-center reveal">
|
||||||
|
{% if eyebrow %}<div class="mb-4 flex items-center justify-center gap-4"><span class="reel-rule w-10"></span><span class="reel-caps text-xs text-primary">{{ eyebrow }}</span><span class="reel-rule w-10"></span></div>{% endif %}
|
||||||
|
{% if title %}<h2 class="reel-caps text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);color:hsl(var(--foreground));">{{ title }}</h2>{% endif %}
|
||||||
|
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-lg italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ intro }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if tiers %}
|
||||||
|
<div class="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{% for tier in tiers %}
|
||||||
|
<article class="reveal reel-frame flex flex-col bg-card text-card-foreground {% if tier.featured %}reel-foil{% endif %}">
|
||||||
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
|
<h3 class="reel-caps text-xl" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);color:hsl(var(--foreground));">{{ tier.name }}</h3>
|
||||||
|
{% if tier.featured %}<span class="reel-caps text-[0.6rem] text-primary">House pick</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex items-baseline gap-1">
|
||||||
|
<span class="text-3xl" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);color:hsl(var(--primary));">{{ tier.price }}</span>
|
||||||
|
{% if tier.cadence %}<span class="text-sm" style="color:hsl(var(--muted-foreground));">{{ tier.cadence }}</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if tier.description %}<p class="mt-3 text-sm leading-relaxed" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ tier.description }}</p>{% endif %}
|
||||||
|
{% if tier.features %}
|
||||||
|
<ul class="mt-5 space-y-2 border-t border-[hsl(var(--border))] pt-5 text-sm">
|
||||||
|
{% for feature in tier.features %}
|
||||||
|
<li class="flex items-start gap-2"><span class="mt-0.5 shrink-0 text-primary"><svg class="inline-block h-4 w-4" width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg></span><span style="color:hsl(var(--foreground));">{{ feature.label }}</span></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% if tier.applyUrl %}<a href="{{ tier.applyUrl }}" class="reel-btn mt-6 justify-center">{{ tier.applyLabel|default:"Apply" }}</a>{% endif %}
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
40
blocks/membership_tiers.schema.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Membership Tiers",
|
||||||
|
"description": "Side-by-side membership cards with a brass-framed feature list.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"eyebrow": { "type": "string", "title": "Eyebrow", "x-editor": "text", "default": "Membership" },
|
||||||
|
"title": { "type": "string", "title": "Section title", "x-editor": "text", "default": "Join the house" },
|
||||||
|
"intro": { "type": "string", "title": "Intro", "x-editor": "textarea", "default": "" },
|
||||||
|
"tiers": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Tiers",
|
||||||
|
"x-editor": "array",
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string", "title": "Name", "x-editor": "text" },
|
||||||
|
"price": { "type": "string", "title": "Price", "x-editor": "text" },
|
||||||
|
"cadence": { "type": "string", "title": "Cadence", "x-editor": "text" },
|
||||||
|
"description": { "type": "string", "title": "Description", "x-editor": "textarea" },
|
||||||
|
"featured": { "type": "boolean", "title": "Featured", "x-editor": "boolean", "default": false },
|
||||||
|
"applyLabel": { "type": "string", "title": "Apply label", "x-editor": "text" },
|
||||||
|
"applyUrl": { "type": "string", "title": "Apply URL", "x-editor": "link" },
|
||||||
|
"features": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Features",
|
||||||
|
"x-editor": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": { "type": "string", "title": "Label", "x-editor": "text" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,203 +0,0 @@
|
|||||||
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;">
|
|
||||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
|
||||||
</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
|
|
||||||
}
|
|
||||||
@ -1,427 +0,0 @@
|
|||||||
// 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;\"> ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌</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
@ -1,49 +0,0 @@
|
|||||||
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
@ -1,58 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,160 +0,0 @@
|
|||||||
// 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
|
|
||||||
102
fonts.json
@ -1,88 +1,38 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "Gotham",
|
"name": "Antonio",
|
||||||
"family": "Gotham",
|
"family": "Antonio",
|
||||||
"variants": [
|
"variants": [
|
||||||
{
|
{ "weight": "400", "style": "normal", "file": "fonts/antonio-latin.woff2" },
|
||||||
"weight": "100",
|
{ "weight": "600", "style": "normal", "file": "fonts/antonio-latin.woff2" },
|
||||||
"style": "normal",
|
{ "weight": "700", "style": "normal", "file": "fonts/antonio-latin.woff2" }
|
||||||
"file": "fonts/web/gothamthin-webfont.woff2"
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"weight": "100",
|
"name": "Cormorant Garamond",
|
||||||
"style": "italic",
|
"family": "Cormorant Garamond",
|
||||||
"file": "fonts/web/gothamthinitalic-webfont.woff2"
|
"variants": [
|
||||||
|
{ "weight": "400", "style": "normal", "file": "fonts/cormorant-garamond-400.woff2" },
|
||||||
|
{ "weight": "400", "style": "italic", "file": "fonts/cormorant-garamond-400-italic.woff2" },
|
||||||
|
{ "weight": "600", "style": "normal", "file": "fonts/cormorant-garamond-600.woff2" },
|
||||||
|
{ "weight": "700", "style": "normal", "file": "fonts/cormorant-garamond-700.woff2" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"weight": "200",
|
"name": "Inter",
|
||||||
"style": "normal",
|
"family": "Inter",
|
||||||
"file": "fonts/web/gothamxlight-webfont.woff2"
|
"variants": [
|
||||||
|
{ "weight": "400", "style": "normal", "file": "fonts/inter-400.woff2" },
|
||||||
|
{ "weight": "500", "style": "normal", "file": "fonts/inter-500.woff2" },
|
||||||
|
{ "weight": "700", "style": "normal", "file": "fonts/inter-700.woff2" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"weight": "200",
|
"name": "IBM Plex Mono",
|
||||||
"style": "italic",
|
"family": "IBM Plex Mono",
|
||||||
"file": "fonts/web/gothamxlightitalic-webfont.woff2"
|
"variants": [
|
||||||
},
|
{ "weight": "400", "style": "normal", "file": "fonts/ibm-plex-mono-400.woff2" },
|
||||||
{
|
{ "weight": "500", "style": "normal", "file": "fonts/ibm-plex-mono-500.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
@ -1,68 +0,0 @@
|
|||||||
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
@ -1,63 +0,0 @@
|
|||||||
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
@ -1,192 +0,0 @@
|
|||||||
// 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
@ -1,20 +0,0 @@
|
|||||||
module git.dev.alexdunmow.com/block/themes/gotham
|
|
||||||
|
|
||||||
go 1.26.4
|
|
||||||
|
|
||||||
require (
|
|
||||||
git.dev.alexdunmow.com/block/core v0.14.1
|
|
||||||
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
|
|
||||||
)
|
|
||||||
44
go.sum
@ -1,44 +0,0 @@
|
|||||||
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.14.0 h1:cO6QQQPndhxwh1zbDvDOzebwrFS8Ka3WsxD8JBJLw/Y=
|
|
||||||
git.dev.alexdunmow.com/block/core v0.14.0/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
|
|
||||||
git.dev.alexdunmow.com/block/core v0.14.1 h1:63b25yzWoqrhBd3wdDFhsJzyGNM99wYmdh4WkN35hDk=
|
|
||||||
git.dev.alexdunmow.com/block/core v0.14.1/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
|
|
||||||
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=
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,311 +0,0 @@
|
|||||||
// 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
@ -1,34 +0,0 @@
|
|||||||
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
@ -1,41 +0,0 @@
|
|||||||
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
@ -1,29 +0,0 @@
|
|||||||
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
@ -1,132 +0,0 @@
|
|||||||
// 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
|
|
||||||
252
manifest.yaml
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
# Gotham theme — codeless .bnp manifest (WO-TF-018, Wave A).
|
||||||
|
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
|
||||||
|
#
|
||||||
|
# Nightlife / private-club identity: cinematic near-black luxe, brass hairline
|
||||||
|
# accents, letterboxed heroes, tall condensed display type. Utility CSS lives in
|
||||||
|
# `css.input_css_append` below (host Tailwind pipeline) — colour is always
|
||||||
|
# hsl(var(--token)) and font-family always var(--font-heading|body|mono, ...).
|
||||||
|
theme_presets: presets.json
|
||||||
|
bundled_fonts: fonts.json
|
||||||
|
master_pages: master_pages.json
|
||||||
|
required_icon_packs: [lucide, simple-icons]
|
||||||
|
|
||||||
|
system_templates:
|
||||||
|
- key: gotham
|
||||||
|
title: Gotham
|
||||||
|
description: "After-dark identity for a nightlife venue or private members' club — cinematic near-black luxe, brass accents and condensed display type."
|
||||||
|
|
||||||
|
page_templates:
|
||||||
|
- system: gotham
|
||||||
|
key: default
|
||||||
|
title: Default
|
||||||
|
description: "Centred near-black page with brass rails, header, main and footer."
|
||||||
|
slots: [header, main, footer]
|
||||||
|
- system: gotham
|
||||||
|
key: landing
|
||||||
|
title: Marquee Landing
|
||||||
|
description: "Letterboxed hero, door strip and a closing CTA for the main room."
|
||||||
|
slots: [hero, main, cta, footer]
|
||||||
|
- system: gotham
|
||||||
|
key: article
|
||||||
|
title: Feature
|
||||||
|
description: "Narrow editorial column for press, listings and the house journal."
|
||||||
|
slots: [header, main, aside, footer]
|
||||||
|
- system: gotham
|
||||||
|
key: full-width
|
||||||
|
title: Grand Room
|
||||||
|
description: "Edge-to-edge gallery layout for the room, lookbooks and galleries."
|
||||||
|
slots: [header, main, footer]
|
||||||
|
- system: gotham
|
||||||
|
key: blog-index
|
||||||
|
title: The Journal
|
||||||
|
description: "Blog index with a brass masthead, featured and post-grid slots."
|
||||||
|
slots: [header, featured, main, footer]
|
||||||
|
- system: gotham
|
||||||
|
key: contact
|
||||||
|
title: Reservations
|
||||||
|
description: "Contact layout with an optional aside for hours and the door."
|
||||||
|
slots: [header, main, aside, footer]
|
||||||
|
- system: gotham
|
||||||
|
key: auth
|
||||||
|
title: Members
|
||||||
|
description: "Centred members' shell with wordmark and a spotlight crest."
|
||||||
|
slots: [main, footer]
|
||||||
|
- system: gotham
|
||||||
|
key: centered
|
||||||
|
title: Centered
|
||||||
|
description: "Narrow centred column for documents and single statements."
|
||||||
|
slots: [header, main, footer]
|
||||||
|
|
||||||
|
template_overrides:
|
||||||
|
# Chrome (4)
|
||||||
|
- { template: gotham, block: navbar }
|
||||||
|
- { template: gotham, block: footer }
|
||||||
|
- { template: gotham, block: utility-bar }
|
||||||
|
- { template: gotham, block: announcement-bar }
|
||||||
|
# Marketing (12)
|
||||||
|
- { template: gotham, block: hero }
|
||||||
|
- { template: gotham, block: feature-grid }
|
||||||
|
- { template: gotham, block: rich-section }
|
||||||
|
- { template: gotham, block: stats }
|
||||||
|
- { template: gotham, block: testimonial }
|
||||||
|
- { template: gotham, block: logo-strip }
|
||||||
|
- { template: gotham, block: pricing }
|
||||||
|
- { template: gotham, block: team }
|
||||||
|
- { template: gotham, block: faq }
|
||||||
|
- { template: gotham, block: timeline }
|
||||||
|
- { template: gotham, block: cta }
|
||||||
|
- { template: gotham, block: countdown }
|
||||||
|
# Primitives (11)
|
||||||
|
- { template: gotham, block: heading }
|
||||||
|
- { template: gotham, block: text }
|
||||||
|
- { template: gotham, block: rich-text }
|
||||||
|
- { template: gotham, block: image }
|
||||||
|
- { template: gotham, block: quote }
|
||||||
|
- { template: gotham, block: divider }
|
||||||
|
- { template: gotham, block: button }
|
||||||
|
- { template: gotham, block: card }
|
||||||
|
- { template: gotham, block: gallery }
|
||||||
|
- { template: gotham, block: video-embed }
|
||||||
|
- { template: gotham, block: audio-embed }
|
||||||
|
# Commerce / local (6)
|
||||||
|
- { template: gotham, block: menu-list }
|
||||||
|
- { template: gotham, block: event-list }
|
||||||
|
- { template: gotham, block: contact-form }
|
||||||
|
- { template: gotham, block: contact-card }
|
||||||
|
- { template: gotham, block: hours-location }
|
||||||
|
- { template: gotham, block: social-links }
|
||||||
|
# Blog family (12)
|
||||||
|
- { template: gotham, block: blog-hero }
|
||||||
|
- { template: gotham, block: featured-posts }
|
||||||
|
- { template: gotham, block: related-posts }
|
||||||
|
- { template: gotham, block: popular-posts }
|
||||||
|
- { template: gotham, block: category-list }
|
||||||
|
- { template: gotham, block: post-hero }
|
||||||
|
- { template: gotham, block: post-metadata }
|
||||||
|
- { template: gotham, block: author-bio }
|
||||||
|
- { template: gotham, block: author-bio-hero }
|
||||||
|
- { template: gotham, block: newsletter-signup }
|
||||||
|
- { template: gotham, block: breadcrumbs }
|
||||||
|
- { template: gotham, block: sidebar-nav }
|
||||||
|
# Auth surface (3)
|
||||||
|
- { template: gotham, block: auth-form }
|
||||||
|
- { template: gotham, block: password-reset-form }
|
||||||
|
- { template: gotham, block: auth-status }
|
||||||
|
# System (1)
|
||||||
|
- { template: gotham, block: page-suggestions }
|
||||||
|
|
||||||
|
email_wrappers:
|
||||||
|
- gotham
|
||||||
|
|
||||||
|
css:
|
||||||
|
input_css_append: |
|
||||||
|
/* Gotham theme — cinematic near-black utility CSS.
|
||||||
|
*
|
||||||
|
* Colour is always consumed via hsl(var(--token)); font families always
|
||||||
|
* come from var(--font-heading|body|mono, <fallback>). No literal colours
|
||||||
|
* except black scrims layered over IMAGES (letterbox effect).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Bundled display faces. The host does not yet emit @font-face for
|
||||||
|
* bundled_fonts (LoadedPlugin.BundledFonts is unconsumed), so the theme
|
||||||
|
* declares its own faces against the served artifact paths
|
||||||
|
* (/templates/gotham/fonts/...). Presets set typography.fontHeading/body
|
||||||
|
* to template:gotham:<Family> so --font-heading/--font-body resolve to
|
||||||
|
* these families. */
|
||||||
|
@font-face { font-family: 'Antonio'; src: url('/templates/gotham/fonts/antonio-latin.woff2') format('woff2'); font-weight: 100 700; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'Cormorant Garamond'; src: url('/templates/gotham/fonts/cormorant-garamond-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'Cormorant Garamond'; src: url('/templates/gotham/fonts/cormorant-garamond-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
|
||||||
|
@font-face { font-family: 'Cormorant Garamond'; src: url('/templates/gotham/fonts/cormorant-garamond-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'Cormorant Garamond'; src: url('/templates/gotham/fonts/cormorant-garamond-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'IBM Plex Mono'; src: url('/templates/gotham/fonts/ibm-plex-mono-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
/* reel-rule — brass hairline, 1px ascending to 2px. */
|
||||||
|
.reel-rule { border: none; border-top: 1px solid hsl(var(--primary)); margin: 0; }
|
||||||
|
.reel-rule-thick { border: none; border-top: 2px solid hsl(var(--primary)); margin: 0; }
|
||||||
|
|
||||||
|
/* reel-frame — thin film frame with brass corner ticks. */
|
||||||
|
.reel-frame { position: relative; border: 1px solid hsl(var(--border)); padding: 1.25rem 1.5rem; }
|
||||||
|
.reel-frame::before, .reel-frame::after {
|
||||||
|
content: ""; position: absolute; width: 14px; height: 14px;
|
||||||
|
border-color: hsl(var(--primary)); border-style: solid; pointer-events: none;
|
||||||
|
}
|
||||||
|
.reel-frame::before { top: -1px; left: -1px; border-width: 1px 0 0 1px; }
|
||||||
|
.reel-frame::after { bottom: -1px; right: -1px; border-width: 0 1px 1px 0; }
|
||||||
|
|
||||||
|
/* reel-foil — inset brass glow used on hover / featured states. */
|
||||||
|
.reel-foil {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px hsl(var(--primary)),
|
||||||
|
inset 0 1px 0 hsl(var(--primary) / 0.5),
|
||||||
|
inset 0 -1px 0 hsl(var(--primary) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reel-caps — condensed caps tracking for display headings. */
|
||||||
|
.reel-caps { text-transform: uppercase; letter-spacing: 0.12em; }
|
||||||
|
|
||||||
|
/* reel-underline — brass hairline underline on headings. */
|
||||||
|
.reel-underline { position: relative; padding-bottom: 0.35em; }
|
||||||
|
.reel-underline::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: hsl(var(--primary)); }
|
||||||
|
|
||||||
|
/* reel-btn — the invitation. Brass on near-black, inverts on hover. */
|
||||||
|
.reel-btn {
|
||||||
|
display: inline-flex; align-items: center; gap: 0.5rem;
|
||||||
|
border: 1px solid hsl(var(--primary));
|
||||||
|
background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground));
|
||||||
|
padding: 0.7rem 1.7rem; border-radius: 2px;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.14em; font-size: 0.8rem; line-height: 1;
|
||||||
|
transition: background-color 200ms ease, color 200ms ease, box-shadow 200ms ease;
|
||||||
|
}
|
||||||
|
.reel-btn:hover, .reel-btn:focus-visible {
|
||||||
|
background-color: transparent; color: hsl(var(--primary));
|
||||||
|
box-shadow: inset 0 0 0 1px hsl(var(--primary));
|
||||||
|
}
|
||||||
|
.reel-btn:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 3px; }
|
||||||
|
|
||||||
|
/* reel-link — brass underline link. */
|
||||||
|
.reel-link { color: hsl(var(--primary)); text-decoration: none; border-bottom: 1px solid hsl(var(--primary) / 0.5); transition: color 200ms ease, border-color 200ms ease; }
|
||||||
|
.reel-link:hover, .reel-link:focus-visible { color: hsl(var(--accent)); border-bottom-color: hsl(var(--accent)); }
|
||||||
|
|
||||||
|
/* film-grain — 4% film-grain noise, dark surfaces. */
|
||||||
|
.film-grain { position: relative; isolation: isolate; }
|
||||||
|
.film-grain::after {
|
||||||
|
content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; opacity: 0.04;
|
||||||
|
background-image:
|
||||||
|
repeating-radial-gradient(circle at 17% 23%, hsl(var(--foreground)) 0 1px, transparent 1px 3px),
|
||||||
|
repeating-radial-gradient(circle at 73% 61%, hsl(var(--foreground)) 0 1px, transparent 1px 3px);
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* spotlight — a radial pool of brass light, hero/crest overlay. */
|
||||||
|
.spotlight { background-image: radial-gradient(ellipse 60% 55% at 50% 40%, hsl(var(--primary) / 0.16), transparent 70%); }
|
||||||
|
|
||||||
|
/* reel-strip — brass double-rule band (crest / marquee accent). */
|
||||||
|
.reel-strip {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(hsl(var(--primary)), hsl(var(--primary))),
|
||||||
|
linear-gradient(hsl(var(--primary)), hsl(var(--primary)));
|
||||||
|
background-size: 100% 1px, 100% 1px;
|
||||||
|
background-position: top, bottom;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reel-mat — brass inner-mat around framed media. */
|
||||||
|
.reel-mat { position: relative; padding: 4px; box-shadow: inset 0 0 0 1px hsl(var(--border)); }
|
||||||
|
.reel-mat > * { display: block; width: 100%; height: 100%; }
|
||||||
|
|
||||||
|
/* reel-dropcap — condensed brass dropcap on first paragraph. */
|
||||||
|
.reel-dropcap[data-deco-dropcap]::first-letter,
|
||||||
|
.reel-dropcap[data-reel-dropcap]::first-letter {
|
||||||
|
font-family: var(--font-heading, "Antonio", "Oswald", "Bebas Neue", sans-serif);
|
||||||
|
font-size: 3.2em; line-height: 0.9; float: left; padding: 0.02em 0.14em 0 0; color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reel-onimg-* — fixed light ink for text set OVER hero imagery + its
|
||||||
|
* black scrims (mode-independent, like the scrim itself): light-mode
|
||||||
|
* tokens are near-black and vanish on the photo. */
|
||||||
|
.reel-onimg-strong { color: hsl(40 30% 97%); }
|
||||||
|
.reel-onimg-soft { color: hsl(40 22% 88% / 0.92); }
|
||||||
|
.reel-onimg-dim { color: hsl(40 20% 85% / 0.9); }
|
||||||
|
|
||||||
|
/* letterbox-scrim — black cinematic bars over hero imagery (image overlay). */
|
||||||
|
.letterbox-scrim {
|
||||||
|
background-image: linear-gradient(to bottom,
|
||||||
|
hsl(0 0% 0% / 0.78) 0%, hsl(0 0% 0% / 0.15) 16%,
|
||||||
|
transparent 40%, transparent 60%,
|
||||||
|
hsl(0 0% 0% / 0.15) 84%, hsl(0 0% 0% / 0.78) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reveal — MID-tier scroll reveal. No JS = visible; reduced-motion honoured. */
|
||||||
|
.reveal { opacity: 1; }
|
||||||
|
.gotham-js .reveal { opacity: 0; transform: translateY(12px); transition: opacity .4s ease, transform .4s ease; }
|
||||||
|
.gotham-js .reveal.is-visible { opacity: 1; transform: none; }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.gotham-js .reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Raw CSS (outside @layer, JIT-scan-independent) — layout guarantees the
|
||||||
|
* host Tailwind scan may not reliably emit from theme .ninjatpl files.
|
||||||
|
* Imageless hero: a compact letterboxed text plate, never a 75vh void. */
|
||||||
|
@media (max-width: 640px) { .gotham-hero-plate { min-height: 380px; } }
|
||||||
23
master_pages.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "gotham:default-master",
|
||||||
|
"title": "Gotham Default Master",
|
||||||
|
"page_templates": ["default", "article", "blog-index", "contact", "centered", "full-width"],
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "navbar", "title": "House Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "Gotham House" } }, "slot": "header", "sort_order": 0 },
|
||||||
|
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 },
|
||||||
|
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "centered", "companyName": "Gotham House", "tagline": "A members' club after dark." }, "slot": "footer", "sort_order": 0 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "gotham:marquee-master",
|
||||||
|
"title": "Gotham Marquee Master",
|
||||||
|
"page_templates": ["landing"],
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "navbar", "title": "House Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "Gotham House" } }, "slot": "hero", "sort_order": 0 },
|
||||||
|
{ "block_key": "gotham:door_strip", "title": "Door Strip", "content": { "label": "The Door", "phone": "", "reserveLabel": "Guest list", "reserveUrl": "/contact", "hours": [] }, "slot": "hero", "sort_order": 1 },
|
||||||
|
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
|
||||||
|
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "centered", "companyName": "Gotham House", "tagline": "A members' club after dark." }, "slot": "footer", "sort_order": 0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -2,11 +2,8 @@
|
|||||||
name = "gotham"
|
name = "gotham"
|
||||||
display_name = "Gotham"
|
display_name = "Gotham"
|
||||||
scope = "@themes"
|
scope = "@themes"
|
||||||
version = "0.1.3"
|
version = "1.2.0"
|
||||||
description = "Dark, high-contrast BlockNinja theme with bold typography and modern accent panels."
|
description = "After-dark identity for a nightlife venue or private members' club. Cinematic near-black luxe, brass hairline accents, letterboxed heroes and tall condensed display type."
|
||||||
kind = "theme"
|
kind = "theme"
|
||||||
categories = ["templates"]
|
categories = ["templates"]
|
||||||
tags = ["dark", "high-contrast", "agency", "blog", "professional", "modern", "serif"]
|
tags = ["dark", "nightlife", "luxury", "club", "cinematic", "brass", "condensed", "hospitality", "events"]
|
||||||
|
|
||||||
[compatibility]
|
|
||||||
block_core = ">=0.11.0 <0.12.0"
|
|
||||||
|
|||||||
713
presets.json
@ -1,502 +1,287 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "dark-knight",
|
"id": "velvet-black",
|
||||||
"name": "The Dark Knight",
|
"name": "Velvet Black",
|
||||||
"description": "Classic Gotham - dark with amber accents",
|
"description": "Deep matte black lifted by brass gilt. The house palette. Silver-screen platinum by day, velvet black after dark.",
|
||||||
"theme": {
|
"theme": {
|
||||||
|
"mode": "both",
|
||||||
|
"typography": {
|
||||||
|
"fontHeading": "template:gotham:Antonio",
|
||||||
|
"fontBody": "template:gotham:Cormorant Garamond",
|
||||||
|
"fontMono": "template:gotham:IBM Plex Mono",
|
||||||
|
"fontSizeBase": "17",
|
||||||
|
"lineHeightBase": "1.6"
|
||||||
|
},
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "0 0% 100%",
|
"background": "40 20% 96%",
|
||||||
"foreground": "0 0% 3.9%",
|
"foreground": "28 16% 9%",
|
||||||
"card": "0 0% 100%",
|
"card": "40 24% 99%",
|
||||||
"cardForeground": "0 0% 3.9%",
|
"cardForeground": "28 16% 9%",
|
||||||
"popover": "0 0% 100%",
|
"popover": "40 24% 99%",
|
||||||
"popoverForeground": "0 0% 3.9%",
|
"popoverForeground": "28 16% 9%",
|
||||||
"primary": "45 93% 47%",
|
"primary": "36 62% 38%",
|
||||||
"primaryForeground": "0 0% 0%",
|
"primaryForeground": "40 30% 97%",
|
||||||
"secondary": "0 0% 96%",
|
"secondary": "40 12% 88%",
|
||||||
"secondaryForeground": "0 0% 9%",
|
"secondaryForeground": "28 14% 13%",
|
||||||
"muted": "0 0% 96%",
|
"muted": "40 12% 90%",
|
||||||
"mutedForeground": "0 0% 45%",
|
"mutedForeground": "28 11% 32%",
|
||||||
"accent": "45 93% 47%",
|
"accent": "34 64% 44%",
|
||||||
"accentForeground": "0 0% 0%",
|
"accentForeground": "40 30% 97%",
|
||||||
"destructive": "0 84% 60%",
|
"destructive": "0 68% 42%",
|
||||||
"destructiveForeground": "0 0% 98%",
|
"destructiveForeground": "40 26% 96%",
|
||||||
"border": "0 0% 90%",
|
"border": "38 16% 73%",
|
||||||
"input": "0 0% 90%",
|
"input": "40 16% 80%",
|
||||||
"ring": "45 93% 47%"
|
"ring": "36 62% 40%"
|
||||||
},
|
},
|
||||||
"darkColors": {
|
"darkColors": {
|
||||||
"background": "0 0% 3.9%",
|
"background": "28 13% 4%",
|
||||||
"foreground": "0 0% 98%",
|
"foreground": "40 32% 93%",
|
||||||
"card": "0 0% 6%",
|
"card": "28 12% 7%",
|
||||||
"cardForeground": "0 0% 98%",
|
"cardForeground": "40 32% 93%",
|
||||||
"popover": "0 0% 6%",
|
"popover": "28 12% 7%",
|
||||||
"popoverForeground": "0 0% 98%",
|
"popoverForeground": "40 32% 93%",
|
||||||
"primary": "45 93% 47%",
|
"primary": "40 72% 54%",
|
||||||
"primaryForeground": "0 0% 0%",
|
"primaryForeground": "28 24% 5%",
|
||||||
"secondary": "0 0% 10%",
|
"secondary": "28 9% 13%",
|
||||||
"secondaryForeground": "0 0% 98%",
|
"secondaryForeground": "40 26% 88%",
|
||||||
"muted": "0 0% 10%",
|
"muted": "28 8% 11%",
|
||||||
"mutedForeground": "0 0% 60%",
|
"mutedForeground": "40 16% 67%",
|
||||||
"accent": "45 93% 47%",
|
"accent": "38 78% 62%",
|
||||||
"accentForeground": "0 0% 0%",
|
"accentForeground": "28 24% 5%",
|
||||||
"destructive": "0 62% 30%",
|
"destructive": "0 72% 52%",
|
||||||
"destructiveForeground": "0 0% 98%",
|
"destructiveForeground": "40 26% 95%",
|
||||||
"border": "0 0% 14%",
|
"border": "40 22% 17%",
|
||||||
"input": "0 0% 14%",
|
"input": "40 18% 15%",
|
||||||
"ring": "45 93% 47%"
|
"ring": "40 72% 54%"
|
||||||
},
|
}
|
||||||
"mode": "dark"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "joker",
|
"id": "midnight-brass",
|
||||||
"name": "The Joker",
|
"name": "Midnight Brass",
|
||||||
"description": "Chaotic purple and toxic green",
|
"description": "Cool blue-black steel with bright brass. A colder, later room.",
|
||||||
"theme": {
|
"theme": {
|
||||||
|
"mode": "both",
|
||||||
|
"typography": {
|
||||||
|
"fontHeading": "template:gotham:Antonio",
|
||||||
|
"fontBody": "template:gotham:Cormorant Garamond",
|
||||||
|
"fontMono": "template:gotham:IBM Plex Mono",
|
||||||
|
"fontSizeBase": "17",
|
||||||
|
"lineHeightBase": "1.6"
|
||||||
|
},
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "0 0% 100%",
|
"background": "220 16% 95%",
|
||||||
"foreground": "270 50% 15%",
|
"foreground": "222 22% 10%",
|
||||||
"card": "0 0% 98%",
|
"card": "220 20% 98%",
|
||||||
"cardForeground": "270 50% 15%",
|
"cardForeground": "222 22% 10%",
|
||||||
"popover": "0 0% 98%",
|
"popover": "220 20% 98%",
|
||||||
"popoverForeground": "270 50% 15%",
|
"popoverForeground": "222 22% 10%",
|
||||||
"primary": "120 80% 35%",
|
"primary": "40 60% 44%",
|
||||||
"primaryForeground": "0 0% 100%",
|
"primaryForeground": "220 20% 96%",
|
||||||
"secondary": "270 40% 90%",
|
"secondary": "220 12% 88%",
|
||||||
"secondaryForeground": "270 50% 15%",
|
"secondaryForeground": "222 16% 18%",
|
||||||
"muted": "270 20% 95%",
|
"muted": "220 12% 90%",
|
||||||
"mutedForeground": "270 30% 40%",
|
"mutedForeground": "220 11% 33%",
|
||||||
"accent": "300 100% 70%",
|
"accent": "40 66% 50%",
|
||||||
"accentForeground": "0 0% 0%",
|
"accentForeground": "222 20% 8%",
|
||||||
"destructive": "0 84% 60%",
|
"destructive": "0 68% 46%",
|
||||||
"destructiveForeground": "0 0% 98%",
|
"destructiveForeground": "220 20% 96%",
|
||||||
"border": "270 20% 85%",
|
"border": "220 14% 78%",
|
||||||
"input": "270 20% 85%",
|
"input": "220 12% 82%",
|
||||||
"ring": "120 80% 35%"
|
"ring": "40 60% 46%"
|
||||||
},
|
},
|
||||||
"darkColors": {
|
"darkColors": {
|
||||||
"background": "270 30% 8%",
|
"background": "222 26% 4%",
|
||||||
"foreground": "120 100% 80%",
|
"foreground": "215 26% 93%",
|
||||||
"card": "270 30% 12%",
|
"card": "222 22% 7%",
|
||||||
"cardForeground": "120 100% 80%",
|
"cardForeground": "215 26% 93%",
|
||||||
"popover": "270 30% 12%",
|
"popover": "222 22% 7%",
|
||||||
"popoverForeground": "120 100% 80%",
|
"popoverForeground": "215 26% 93%",
|
||||||
"primary": "120 100% 40%",
|
"primary": "42 72% 58%",
|
||||||
"primaryForeground": "270 30% 8%",
|
"primaryForeground": "222 24% 8%",
|
||||||
"secondary": "270 50% 20%",
|
"secondary": "222 16% 15%",
|
||||||
"secondaryForeground": "120 100% 80%",
|
"secondaryForeground": "215 20% 86%",
|
||||||
"muted": "270 30% 15%",
|
"muted": "222 12% 11%",
|
||||||
"mutedForeground": "270 20% 60%",
|
"mutedForeground": "215 16% 67%",
|
||||||
"accent": "300 100% 50%",
|
"accent": "40 78% 62%",
|
||||||
"accentForeground": "0 0% 0%",
|
"accentForeground": "222 24% 8%",
|
||||||
"destructive": "0 84% 60%",
|
"destructive": "0 72% 52%",
|
||||||
"destructiveForeground": "0 0% 98%",
|
"destructiveForeground": "215 22% 95%",
|
||||||
"border": "270 30% 20%",
|
"border": "220 20% 22%",
|
||||||
"input": "270 30% 20%",
|
"input": "220 16% 18%",
|
||||||
"ring": "120 100% 40%"
|
"ring": "42 72% 58%"
|
||||||
},
|
}
|
||||||
"mode": "dark"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "riddler",
|
"id": "noir-rose",
|
||||||
"name": "The Riddler",
|
"name": "Noir Rose",
|
||||||
"description": "Bright neon green with purple mystery",
|
"description": "Near-black warmed with rose-gold. The velvet-rope room.",
|
||||||
"theme": {
|
"theme": {
|
||||||
|
"mode": "both",
|
||||||
|
"typography": {
|
||||||
|
"fontHeading": "template:gotham:Antonio",
|
||||||
|
"fontBody": "template:gotham:Cormorant Garamond",
|
||||||
|
"fontMono": "template:gotham:IBM Plex Mono",
|
||||||
|
"fontSizeBase": "17",
|
||||||
|
"lineHeightBase": "1.6"
|
||||||
|
},
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "0 0% 100%",
|
"background": "20 20% 95%",
|
||||||
"foreground": "0 0% 5%",
|
"foreground": "18 18% 10%",
|
||||||
"card": "0 0% 98%",
|
"card": "20 24% 98%",
|
||||||
"cardForeground": "0 0% 5%",
|
"cardForeground": "18 18% 10%",
|
||||||
"popover": "0 0% 98%",
|
"popover": "20 24% 98%",
|
||||||
"popoverForeground": "0 0% 5%",
|
"popoverForeground": "18 18% 10%",
|
||||||
"primary": "120 70% 35%",
|
"primary": "16 52% 50%",
|
||||||
"primaryForeground": "0 0% 100%",
|
"primaryForeground": "20 30% 97%",
|
||||||
"secondary": "280 50% 85%",
|
"secondary": "20 14% 88%",
|
||||||
"secondaryForeground": "280 80% 20%",
|
"secondaryForeground": "18 14% 18%",
|
||||||
"muted": "0 0% 96%",
|
"muted": "20 12% 90%",
|
||||||
"mutedForeground": "120 30% 35%",
|
"mutedForeground": "18 12% 33%",
|
||||||
"accent": "280 80% 55%",
|
"accent": "12 60% 56%",
|
||||||
"accentForeground": "0 0% 100%",
|
"accentForeground": "20 30% 97%",
|
||||||
"destructive": "0 84% 60%",
|
"destructive": "0 68% 46%",
|
||||||
"destructiveForeground": "0 0% 98%",
|
"destructiveForeground": "20 26% 96%",
|
||||||
"border": "120 30% 85%",
|
"border": "18 18% 78%",
|
||||||
"input": "0 0% 90%",
|
"input": "18 16% 82%",
|
||||||
"ring": "120 70% 35%"
|
"ring": "16 52% 50%"
|
||||||
},
|
},
|
||||||
"darkColors": {
|
"darkColors": {
|
||||||
"background": "0 0% 4%",
|
"background": "16 17% 4%",
|
||||||
"foreground": "120 100% 50%",
|
"foreground": "22 28% 93%",
|
||||||
"card": "0 0% 8%",
|
"card": "16 15% 7%",
|
||||||
"cardForeground": "120 100% 50%",
|
"cardForeground": "22 28% 93%",
|
||||||
"popover": "0 0% 8%",
|
"popover": "16 15% 7%",
|
||||||
"popoverForeground": "120 100% 50%",
|
"popoverForeground": "22 28% 93%",
|
||||||
"primary": "120 100% 45%",
|
"primary": "18 64% 60%",
|
||||||
"primaryForeground": "0 0% 0%",
|
"primaryForeground": "16 24% 8%",
|
||||||
"secondary": "280 80% 30%",
|
"secondary": "16 12% 15%",
|
||||||
"secondaryForeground": "120 100% 50%",
|
"secondaryForeground": "22 22% 86%",
|
||||||
"muted": "0 0% 12%",
|
"muted": "16 9% 11%",
|
||||||
"mutedForeground": "120 50% 40%",
|
"mutedForeground": "22 16% 67%",
|
||||||
"accent": "280 100% 60%",
|
"accent": "10 70% 64%",
|
||||||
"accentForeground": "0 0% 100%",
|
"accentForeground": "16 24% 8%",
|
||||||
"destructive": "0 84% 60%",
|
"destructive": "0 72% 54%",
|
||||||
"destructiveForeground": "0 0% 98%",
|
"destructiveForeground": "22 24% 95%",
|
||||||
"border": "120 100% 20%",
|
"border": "18 20% 22%",
|
||||||
"input": "0 0% 15%",
|
"input": "18 16% 18%",
|
||||||
"ring": "120 100% 45%"
|
"ring": "18 64% 60%"
|
||||||
},
|
}
|
||||||
"mode": "dark"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "harley-quinn",
|
"id": "smoke-silver",
|
||||||
"name": "Harley Quinn",
|
"name": "Smoke & Silver",
|
||||||
"description": "Bold red, black, and playful pink",
|
"description": "Graphite and cool silver. Restrained, low on gold. The quiet booth.",
|
||||||
"theme": {
|
"theme": {
|
||||||
|
"mode": "both",
|
||||||
|
"typography": {
|
||||||
|
"fontHeading": "template:gotham:Antonio",
|
||||||
|
"fontBody": "template:gotham:Cormorant Garamond",
|
||||||
|
"fontMono": "template:gotham:IBM Plex Mono",
|
||||||
|
"fontSizeBase": "17",
|
||||||
|
"lineHeightBase": "1.6"
|
||||||
|
},
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "0 0% 100%",
|
"background": "210 8% 95%",
|
||||||
"foreground": "0 0% 5%",
|
"foreground": "210 13% 10%",
|
||||||
"card": "350 50% 98%",
|
"card": "210 10% 98%",
|
||||||
"cardForeground": "0 0% 5%",
|
"cardForeground": "210 13% 10%",
|
||||||
"popover": "350 50% 98%",
|
"popover": "210 10% 98%",
|
||||||
"popoverForeground": "0 0% 5%",
|
"popoverForeground": "210 13% 10%",
|
||||||
"primary": "350 85% 50%",
|
"primary": "210 8% 38%",
|
||||||
"primaryForeground": "0 0% 100%",
|
"primaryForeground": "210 12% 97%",
|
||||||
"secondary": "210 80% 90%",
|
"secondary": "210 8% 88%",
|
||||||
"secondaryForeground": "210 100% 20%",
|
"secondaryForeground": "210 10% 18%",
|
||||||
"muted": "0 0% 96%",
|
"muted": "210 8% 90%",
|
||||||
"mutedForeground": "350 20% 40%",
|
"mutedForeground": "210 9% 34%",
|
||||||
"accent": "330 100% 75%",
|
"accent": "42 40% 52%",
|
||||||
"accentForeground": "0 0% 0%",
|
"accentForeground": "210 12% 10%",
|
||||||
"destructive": "0 84% 60%",
|
"destructive": "0 66% 46%",
|
||||||
"destructiveForeground": "0 0% 98%",
|
"destructiveForeground": "210 12% 96%",
|
||||||
"border": "350 30% 88%",
|
"border": "210 8% 78%",
|
||||||
"input": "0 0% 90%",
|
"input": "210 8% 82%",
|
||||||
"ring": "350 85% 50%"
|
"ring": "210 8% 42%"
|
||||||
},
|
},
|
||||||
"darkColors": {
|
"darkColors": {
|
||||||
"background": "0 0% 5%",
|
"background": "210 13% 5%",
|
||||||
"foreground": "0 0% 95%",
|
"foreground": "210 15% 93%",
|
||||||
"card": "350 80% 10%",
|
"card": "210 12% 8%",
|
||||||
"cardForeground": "0 0% 95%",
|
"cardForeground": "210 15% 93%",
|
||||||
"popover": "350 80% 10%",
|
"popover": "210 12% 8%",
|
||||||
"popoverForeground": "0 0% 95%",
|
"popoverForeground": "210 15% 93%",
|
||||||
"primary": "350 100% 55%",
|
"primary": "210 14% 74%",
|
||||||
"primaryForeground": "0 0% 100%",
|
"primaryForeground": "210 12% 8%",
|
||||||
"secondary": "210 100% 50%",
|
"secondary": "210 10% 15%",
|
||||||
"secondaryForeground": "0 0% 100%",
|
"secondaryForeground": "210 12% 86%",
|
||||||
"muted": "0 0% 12%",
|
"muted": "210 8% 11%",
|
||||||
"mutedForeground": "350 30% 60%",
|
"mutedForeground": "210 11% 66%",
|
||||||
"accent": "330 100% 65%",
|
"accent": "42 56% 60%",
|
||||||
"accentForeground": "0 0% 0%",
|
"accentForeground": "210 12% 8%",
|
||||||
"destructive": "0 84% 60%",
|
"destructive": "0 70% 52%",
|
||||||
"destructiveForeground": "0 0% 98%",
|
"destructiveForeground": "210 12% 95%",
|
||||||
"border": "350 50% 20%",
|
"border": "210 12% 22%",
|
||||||
"input": "0 0% 15%",
|
"input": "210 10% 18%",
|
||||||
"ring": "350 100% 55%"
|
"ring": "210 14% 74%"
|
||||||
},
|
}
|
||||||
"mode": "dark"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mr-freeze",
|
"id": "bourbon",
|
||||||
"name": "Mr. Freeze",
|
"name": "Bourbon",
|
||||||
"description": "Cold ice blue and frozen white",
|
"description": "Espresso black and whisky amber. The last-pour palette.",
|
||||||
"theme": {
|
"theme": {
|
||||||
|
"mode": "both",
|
||||||
|
"typography": {
|
||||||
|
"fontHeading": "template:gotham:Antonio",
|
||||||
|
"fontBody": "template:gotham:Cormorant Garamond",
|
||||||
|
"fontMono": "template:gotham:IBM Plex Mono",
|
||||||
|
"fontSizeBase": "17",
|
||||||
|
"lineHeightBase": "1.6"
|
||||||
|
},
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "0 0% 100%",
|
"background": "36 24% 94%",
|
||||||
"foreground": "200 60% 10%",
|
"foreground": "28 22% 9%",
|
||||||
"card": "200 40% 98%",
|
"card": "36 26% 98%",
|
||||||
"cardForeground": "200 60% 10%",
|
"cardForeground": "28 22% 9%",
|
||||||
"popover": "200 40% 98%",
|
"popover": "36 26% 98%",
|
||||||
"popoverForeground": "200 60% 10%",
|
"popoverForeground": "28 22% 9%",
|
||||||
"primary": "195 90% 45%",
|
"primary": "30 62% 42%",
|
||||||
"primaryForeground": "0 0% 100%",
|
"primaryForeground": "36 30% 96%",
|
||||||
"secondary": "200 40% 92%",
|
"secondary": "36 16% 87%",
|
||||||
"secondaryForeground": "200 60% 10%",
|
"secondaryForeground": "28 16% 17%",
|
||||||
"muted": "200 30% 95%",
|
"muted": "36 14% 89%",
|
||||||
"mutedForeground": "200 30% 40%",
|
"mutedForeground": "28 14% 33%",
|
||||||
"accent": "180 80% 60%",
|
"accent": "26 66% 48%",
|
||||||
"accentForeground": "200 60% 10%",
|
"accentForeground": "36 30% 96%",
|
||||||
"destructive": "0 84% 60%",
|
"destructive": "0 68% 44%",
|
||||||
"destructiveForeground": "0 0% 98%",
|
"destructiveForeground": "36 26% 96%",
|
||||||
"border": "200 30% 88%",
|
"border": "34 20% 76%",
|
||||||
"input": "200 30% 85%",
|
"input": "34 18% 80%",
|
||||||
"ring": "195 90% 45%"
|
"ring": "30 62% 44%"
|
||||||
},
|
},
|
||||||
"darkColors": {
|
"darkColors": {
|
||||||
"background": "200 50% 8%",
|
"background": "28 21% 4%",
|
||||||
"foreground": "200 100% 95%",
|
"foreground": "36 30% 93%",
|
||||||
"card": "200 50% 12%",
|
"card": "28 18% 7%",
|
||||||
"cardForeground": "200 100% 95%",
|
"cardForeground": "36 30% 93%",
|
||||||
"popover": "200 50% 12%",
|
"popover": "28 18% 7%",
|
||||||
"popoverForeground": "200 100% 95%",
|
"popoverForeground": "36 30% 93%",
|
||||||
"primary": "195 100% 50%",
|
"primary": "32 72% 54%",
|
||||||
"primaryForeground": "200 50% 8%",
|
"primaryForeground": "28 24% 7%",
|
||||||
"secondary": "200 60% 20%",
|
"secondary": "28 14% 15%",
|
||||||
"secondaryForeground": "200 100% 95%",
|
"secondaryForeground": "36 24% 85%",
|
||||||
"muted": "200 40% 15%",
|
"muted": "28 11% 11%",
|
||||||
"mutedForeground": "200 40% 60%",
|
"mutedForeground": "36 16% 66%",
|
||||||
"accent": "180 100% 70%",
|
"accent": "24 80% 58%",
|
||||||
"accentForeground": "200 50% 8%",
|
"accentForeground": "28 24% 7%",
|
||||||
"destructive": "0 84% 60%",
|
"destructive": "0 72% 52%",
|
||||||
"destructiveForeground": "0 0% 98%",
|
"destructiveForeground": "36 26% 95%",
|
||||||
"border": "200 40% 25%",
|
"border": "30 22% 22%",
|
||||||
"input": "200 40% 20%",
|
"input": "30 18% 18%",
|
||||||
"ring": "195 100% 50%"
|
"ring": "32 72% 54%"
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
preview.png
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 2.4 MiB |
116
register.go
@ -1,116 +0,0 @@
|
|||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
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() },
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"$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": []
|
|
||||||
}
|
|
||||||
BIN
screenshots/01-home-light.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
screenshots/02-home-dark.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
screenshots/03-about-light.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
screenshots/04-about-dark.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
screenshots/05-blog-how-the-door-works-light.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
screenshots/06-blog-how-the-door-works-dark.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
seed/media/bar-interior.jpg
Normal file
|
After Width: | Height: | Size: 310 KiB |
BIN
seed/media/bar-scene.jpg
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
seed/media/bar-shelf.jpg
Normal file
|
After Width: | Height: | Size: 385 KiB |
BIN
seed/media/dark-lounge.jpg
Normal file
|
After Width: | Height: | Size: 315 KiB |
265
seed/seed.json
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
{
|
||||||
|
"demo": true,
|
||||||
|
"settings": {
|
||||||
|
"merge": {
|
||||||
|
"companyName": "Gotham House",
|
||||||
|
"tagline": "A members' club after dark."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": [
|
||||||
|
{ "id": "a1b2c3d4-0000-4000-8000-000000000001", "file": "media/bar-scene.jpg", "alt": "Bartenders at work behind a dark backbar, low-lit and grainy", "folder": "Gotham" },
|
||||||
|
{ "id": "a1b2c3d4-0000-4000-8000-000000000002", "file": "media/bar-shelf.jpg", "alt": "Warm brass-lit shelves of whisky bottles in a dim room", "folder": "Gotham" },
|
||||||
|
{ "id": "a1b2c3d4-0000-4000-8000-000000000003", "file": "media/bar-interior.jpg", "alt": "Backlit rows of bottles in silhouette against a dark wall", "folder": "Gotham" },
|
||||||
|
{ "id": "a1b2c3d4-0000-4000-8000-000000000004", "file": "media/dark-lounge.jpg", "alt": "A dark lounge under low hanging lamps casting pools of gold light", "folder": "Gotham" }
|
||||||
|
],
|
||||||
|
"data_tables": [
|
||||||
|
{
|
||||||
|
"key": "contact_submissions",
|
||||||
|
"name": "Door Enquiries",
|
||||||
|
"description": "Seeded contact-form submissions for the Gotham House demo. A submission becomes a row here and emails the site admins.",
|
||||||
|
"columns": [
|
||||||
|
{ "key": "name", "label": "Name", "type": "text", "required": true },
|
||||||
|
{ "key": "email", "label": "Email", "type": "email", "required": true },
|
||||||
|
{ "key": "party", "label": "Party size", "type": "text", "required": false },
|
||||||
|
{ "key": "message", "label": "Message", "type": "text", "required": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"slug": "/",
|
||||||
|
"title": "Gotham House",
|
||||||
|
"template_key": "landing",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"block_key": "hero",
|
||||||
|
"title": "Hero",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 1,
|
||||||
|
"content": {
|
||||||
|
"template": "centered",
|
||||||
|
"minHeight": "75vh",
|
||||||
|
"backgroundImage": "media:a1b2c3d4-0000-4000-8000-000000000001",
|
||||||
|
"overlayColor": "dark",
|
||||||
|
"overlayOpacity": 68,
|
||||||
|
"eyebrow": "Members only",
|
||||||
|
"headline": "Gotham House",
|
||||||
|
"subheadline": "A members' club after dark.",
|
||||||
|
"description": "Late rooms, a long bar and a short guest list. Doors from nine, Thursday to Sunday. Bring your card.",
|
||||||
|
"ctaPrimary": { "text": "Request the guest list", "url": "/contact" },
|
||||||
|
"ctaSecondary": { "text": "See the programme", "url": "#programme" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block_key": "gotham:events_marquee",
|
||||||
|
"title": "Programme",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 2,
|
||||||
|
"content": {
|
||||||
|
"eyebrow": "The programme",
|
||||||
|
"title": "Tonight and after",
|
||||||
|
"events": [
|
||||||
|
{ "date": "Thu", "name": "Late Set", "room": "Main Room", "note": "Resident selectors. Slow start, long finish.", "url": "/contact" },
|
||||||
|
{ "date": "Fri", "name": "The Gilt Sessions", "room": "The Brass Room", "note": "House and disco on the good system.", "url": "/contact" },
|
||||||
|
{ "date": "Sat", "name": "Members' Night", "room": "Whole House", "note": "Card on the door. No list, no entry.", "url": "/contact" },
|
||||||
|
{ "date": "Sun", "name": "Last Pour", "room": "The Cellar Bar", "note": "Quiet room. Records and a nightcap.", "url": "/contact" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block_key": "gotham:membership_tiers",
|
||||||
|
"title": "Membership",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 3,
|
||||||
|
"content": {
|
||||||
|
"eyebrow": "Membership",
|
||||||
|
"title": "Join the house",
|
||||||
|
"intro": "Two ways in. Both start at the door.",
|
||||||
|
"tiers": [
|
||||||
|
{
|
||||||
|
"name": "House",
|
||||||
|
"price": "$40",
|
||||||
|
"cadence": "/ month",
|
||||||
|
"description": "The standard card. Enough for most nights.",
|
||||||
|
"featured": false,
|
||||||
|
"applyLabel": "Apply",
|
||||||
|
"applyUrl": "/contact",
|
||||||
|
"features": [
|
||||||
|
{ "label": "Priority on the guest list" },
|
||||||
|
{ "label": "The main room and the cellar bar" },
|
||||||
|
{ "label": "Two guests a night" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Brass",
|
||||||
|
"price": "$120",
|
||||||
|
"cadence": "/ month",
|
||||||
|
"description": "The full card. Every room, every night.",
|
||||||
|
"featured": true,
|
||||||
|
"applyLabel": "Apply",
|
||||||
|
"applyUrl": "/contact",
|
||||||
|
"features": [
|
||||||
|
{ "label": "Everything in House" },
|
||||||
|
{ "label": "The Brass Room and private booths" },
|
||||||
|
{ "label": "First call on bottle service" },
|
||||||
|
{ "label": "Four guests a night" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block_key": "gotham:bottle_menu",
|
||||||
|
"title": "Bottle service",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 4,
|
||||||
|
"content": {
|
||||||
|
"eyebrow": "The cellar",
|
||||||
|
"title": "Bottle service",
|
||||||
|
"footnote": "Booths hold six. Ask the host for the full list.",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "Champagne",
|
||||||
|
"items": [
|
||||||
|
{ "name": "House brut", "note": "By the glass or the bottle.", "price": "$95" },
|
||||||
|
{ "name": "Vintage rose", "note": "A short shelf. Ask first.", "price": "$180" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Spirits",
|
||||||
|
"items": [
|
||||||
|
{ "name": "Aged rye", "note": "Neat, one large cube.", "price": "$140" },
|
||||||
|
{ "name": "Single malt", "note": "Islay. Bring water.", "price": "$160" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block_key": "cta",
|
||||||
|
"title": "Closing CTA",
|
||||||
|
"slot": "cta",
|
||||||
|
"sort_order": 1,
|
||||||
|
"content": {
|
||||||
|
"background": "band",
|
||||||
|
"heading": "The list closes at nine",
|
||||||
|
"text": "Send your name and party. We reply the same day.",
|
||||||
|
"primaryLabel": "Request the guest list",
|
||||||
|
"primaryUrl": "/contact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/about",
|
||||||
|
"title": "The House",
|
||||||
|
"template_key": "default",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "The House", "level": 1 } },
|
||||||
|
{ "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "<p>Gotham House keeps late hours behind an unmarked door. We run four nights a week and hold the rest of the week back.</p><p>The rooms are dark by design. The bar is long. The list is short. Members come first, and their guests come with them.</p>" } },
|
||||||
|
{ "block_key": "image", "title": "The bar", "slot": "main", "sort_order": 3, "content": { "src": "media:a1b2c3d4-0000-4000-8000-000000000002", "alt": "Warm brass-lit shelves of whisky bottles in a dim room", "caption": "The long bar. Low light, and a shelf that remembers you.", "size": "lg", "aspectRatio": "wide" } },
|
||||||
|
{ "block_key": "divider", "title": "Divider", "slot": "main", "sort_order": 4, "content": { "variant": "ornament", "icon": "lucide:diamond" } },
|
||||||
|
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 5, "content": { "style": "block", "align": "center", "quote": "Come late. Stay later.", "attribution": "The House" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/blog",
|
||||||
|
"title": "The Journal",
|
||||||
|
"template_key": "blog-index",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "card", "title": "Post: The Door", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "media": "media:a1b2c3d4-0000-4000-8000-000000000003", "title": "How the door works", "text": "<p>The list, the card and the guests. What to send and when.</p>", "link": "/blog/how-the-door-works", "linkLabel": "Read on" } },
|
||||||
|
{ "block_key": "card", "title": "Post: A Night", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "media": "media:a1b2c3d4-0000-4000-8000-000000000004", "title": "A night at the house", "text": "<p>From the first pour to the last record. How the evening runs.</p>", "link": "/blog/a-night-at-the-house", "linkLabel": "Read on" } },
|
||||||
|
{ "block_key": "card", "title": "Post: The Cellar", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "media": "media:a1b2c3d4-0000-4000-8000-000000000002", "title": "Notes from the cellar", "text": "<p>What we pour after midnight and why the list stays short.</p>", "link": "/blog/notes-from-the-cellar", "linkLabel": "Read on" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/blog/how-the-door-works",
|
||||||
|
"title": "How the door works",
|
||||||
|
"template_key": "article",
|
||||||
|
"parent_slug": "/blog",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "How the door works", "level": 1 } },
|
||||||
|
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Send your name and party by nine. We hold the list until then and reply the same day. No name, no note, no entry.</p><p>Members lead. A guest comes on a member's word, and the member is the one who answers for them. Bring your card. The door checks it, every night, without exception.</p><p>There is a cap. Some nights it is generous. Most nights it is not. When the room is full the list closes, and the closing is final. We do not reopen it for a better story told at the rail.</p><p>The door is not a negotiation. The people who work it have seen every version of the conversation you are about to start, and they were unmoved the first time. Arrive as though you belong, and more often than not you will.</p><p>Dress for a dark room. We do not print a code, because the room prints it for us. You will know within a step whether you have read it right. When in doubt, wear less colour and better shoes.</p><p>Photographs stay in your pocket. The house is a place people come to be unseen, and that only holds if everyone keeps the same agreement. Break it once and the card goes quiet.</p><p>If you are late, you are late to the list, not to us. The night runs on its own clock and will not wait, but it does not end early either. Come when you can. The door will still be there.</p><p>Lose the card and tell us before you tell anyone else. A card in the wrong hands is a name in the wrong mouth, and we would rather cancel it over a quiet word than read about it later.</p><p>That is the whole of it. Send the name, bring the card, keep the room's secrets, and the door becomes the easiest part of the evening.</p>" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/blog/a-night-at-the-house",
|
||||||
|
"title": "A night at the house",
|
||||||
|
"template_key": "article",
|
||||||
|
"parent_slug": "/blog",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "A night at the house", "level": 1 } },
|
||||||
|
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Doors open at nine. The first set starts slow, on purpose. The room is meant to be walked into, not fallen into, and the early hour belongs to the people who like it quiet.</p><p>The main room fills after eleven. That is when the residents lean into the good part of the night, and the floor stops being a place to stand and becomes a place to stay. If you came to dance, come late.</p><p>The brass room runs colder and closer. Fewer people, a shorter bar, a door within the door. It is where the house keeps its better hours for the cards that have earned them.</p><p>The cellar bar stays quiet, and that is the point. Records, low light, a bench of rye and a host who remembers what you drank last time. Conversations that would not survive the main room live down here.</p><p>Somewhere past one, the night finds its level. The people who were going to leave have left. The people who were always going to stay have found their corner. The house exhales.</p><p>We do not announce last call. The last pour arrives when the room is ready for it, poured a little slower than the ones before, and nobody has to be told what it means.</p><p>The lights do not come up so much as lean in. First the far corners, then the rail, then the floor. The house dims the evening down the way it brought it up.</p><p>Then the door, which held the list all night, holds it open instead. You leave the way you came, by a member's word, into a street that has no idea what the room behind it was doing.</p><p>Come back on a Sunday if you want to understand the place. The crowd is smaller, the records are older, and the house is most itself when almost no one is watching.</p>" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/blog/notes-from-the-cellar",
|
||||||
|
"title": "Notes from the cellar",
|
||||||
|
"template_key": "article",
|
||||||
|
"parent_slug": "/blog",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Notes from the cellar", "level": 1 } },
|
||||||
|
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>The list is short by choice. A long list is a way of hiding that you do not know what you are pouring. We keep ours brief so that every bottle on it has a reason to be there.</p><p>We pour champagne by the glass, because a good glass should not require a commitment. The house brut carries most nights. The vintage rosé sits on a short shelf for the members who ask, and it does not last.</p><p>There is a bench of rye the cellar is quietly proud of. Neat, one large cube, no ceremony. If you want it drowned we will do it, but the host will remember.</p><p>The single malt is Islay, and it is not for everyone, which is exactly why we keep it. Bring water. Let it open. The cellar is a slow room, and the whisky was made by people who understood that.</p><p>Bottle service is a booth and a decision, not a display. We do not run sparklers to the floor. What arrives is what you asked for, cold, on time, and without an audience.</p><p>Ask the host what is drinking well tonight. It is the most useful sentence in the room. The list on the table is the floor of the conversation, not the ceiling.</p><p>We hold a small reserve that never makes the printed page. It goes to the cards that have been patient, on nights that have earned it, and it is offered rather than requested.</p><p>Tell the host what you are drinking and the rest follows. The cellar is built on that one exchange: a name for a glass, a preference remembered, a night made a little better than the last.</p>" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/contact",
|
||||||
|
"title": "The Door",
|
||||||
|
"template_key": "contact",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"block_key": "contact-form",
|
||||||
|
"title": "Guest List Enquiry",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 1,
|
||||||
|
"content": {
|
||||||
|
"heading": "Request the guest list",
|
||||||
|
"description": "Send your name, the night and the party. We reply the same day.",
|
||||||
|
"submitLabel": "Send to the door",
|
||||||
|
"successMessage": "Thank you. Your name is with the door. We will reply by email.",
|
||||||
|
"fields": [
|
||||||
|
{ "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
|
||||||
|
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
|
||||||
|
{ "name": "party", "label": "Party size", "type": "text", "required": false, "width": "half" },
|
||||||
|
{ "name": "message", "label": "Which night, and any notes", "type": "textarea", "required": true, "width": "full" }
|
||||||
|
],
|
||||||
|
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block_key": "hours-location",
|
||||||
|
"title": "Door & Hours",
|
||||||
|
"slot": "aside",
|
||||||
|
"sort_order": 1,
|
||||||
|
"content": {
|
||||||
|
"heading": "The Door",
|
||||||
|
"hours": [
|
||||||
|
{ "day": "Thursday", "hours": "9pm to late" },
|
||||||
|
{ "day": "Friday to Saturday", "hours": "9pm to 4am" },
|
||||||
|
{ "day": "Sunday", "hours": "9pm to 2am" },
|
||||||
|
{ "day": "Monday to Wednesday", "closed": true }
|
||||||
|
],
|
||||||
|
"specialHoursNote": "Members' Night runs Saturday. Card on the door.",
|
||||||
|
"address": "1 Vane Court\nThe Old Quarter",
|
||||||
|
"phone": "+1 555 0139"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/login",
|
||||||
|
"title": "Members",
|
||||||
|
"template_key": "auth",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } },
|
||||||
|
{ "block_key": "text", "title": "Foot", "slot": "footer", "sort_order": 1, "content": { "text": "<p>Members book priority nights and private booths.</p>" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"menu_items": [
|
||||||
|
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
|
||||||
|
{ "menu": "main", "label": "The House", "page_slug": "/about", "sort_order": 2 },
|
||||||
|
{ "menu": "main", "label": "The Journal", "page_slug": "/blog", "sort_order": 3 },
|
||||||
|
{ "menu": "main", "label": "The Door", "page_slug": "/contact", "sort_order": 4 }
|
||||||
|
]
|
||||||
|
}
|
||||||
19
seed/workflows.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"workflows": [
|
||||||
|
{
|
||||||
|
"key": "contact_notify_admins",
|
||||||
|
"name": "Door enquiry to admins",
|
||||||
|
"description": "On a guest-list form submission, email the site admins.",
|
||||||
|
"enabled": true,
|
||||||
|
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"type": "email",
|
||||||
|
"name": "Email admins",
|
||||||
|
"template": "Form Submission Notification",
|
||||||
|
"recipients": { "mode": "admins", "scope": "all" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
35
stat_item.go
@ -1,35 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
// 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
@ -1,48 +0,0 @@
|
|||||||
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
@ -1,84 +0,0 @@
|
|||||||
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
@ -1,274 +0,0 @@
|
|||||||
// 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
@ -1,239 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
@ -1,491 +0,0 @@
|
|||||||
// 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
|
|
||||||
64
templates/email/gotham.ninjatpl
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<!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>{{ site_name }}</title>
|
||||||
|
<style type="text/css">
|
||||||
|
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; }
|
||||||
|
@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="background-color: {{ colors.background }}; margin: 0; padding: 0; font-family: &#39;Cormorant Garamond&#39;, &#39;Cormorant&#39;, Georgia, serif;">
|
||||||
|
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 32px 10px; background-color: {{ colors.background }};">
|
||||||
|
<table role="presentation" class="email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.background }}; border: 1px solid {{ colors.border }};">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 0; background-color: {{ colors.foreground }};">
|
||||||
|
<img src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20600%2080%22%3E%3Crect%20width%3D%22600%22%20height%3D%2280%22%20fill%3D%22%23191510%22%2F%3E%3Cg%20stroke%3D%22%23{{ colors.primary|default:"#c9a14a"|cut:"#" }}%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M300%2080%20L%20100%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20180%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20260%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20340%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20420%2010%22%2F%3E%3Cpath%20d%3D%22M300%2080%20L%20500%2010%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E" alt="" width="600" height="80" style="display: block; width: 100%; max-width: 600px; height: 80px; background-color: {{ colors.foreground }};">
|
||||||
|
{% if site_name %}<h1 style="margin: 0; padding: 24px 16px; font-family: &#39;Oswald&#39;, &#39;Antonio&#39;, Georgia, serif; font-size: 28px; text-transform: uppercase; letter-spacing: 0.18em; color: {{ colors.primary }};">{{ site_name }}</h1>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="content-padding" style="padding: 40px 48px; color: {{ colors.foreground }}; font-size: 16px; line-height: 1.7;">{{ body|safe }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 0 48px;"><hr style="border: none; border-top: 1px solid {{ colors.primary }}; margin: 0;"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 24px 48px; color: {{ colors.mutedForeground }}; font-size: 13px;">
|
||||||
|
<p style="margin: 0 0 8px; font-family: &#39;Oswald&#39;, &#39;Antonio&#39;, Georgia, serif; text-transform: uppercase; letter-spacing: 0.18em; color: {{ colors.foreground }};">{{ site_name }}</p>
|
||||||
|
{% if site_url %}<p style="margin: 0 0 8px;"><a href="{{ site_url }}" style="color: {{ colors.primary }}; text-decoration: none; border-bottom: 1px solid {{ colors.primary }};">{{ site_url }}</a></p>{% endif %}
|
||||||
|
{% if support_email %}<p style="margin: 12px 0 0; font-style: italic; color: {{ colors.mutedForeground }};">P.S. Reservations & enquiries: {{ support_email }}</p>{% endif %}
|
||||||
|
{% if unsubscribe_url %}<p style="margin: 16px 0 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground }}; text-decoration: none;">Unsubscribe</a></p>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
templates/gotham/article.ninjatpl
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col film-grain" style="font-family: var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif);">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<header class="w-full">
|
||||||
|
<div class="max-w-3xl mx-auto px-4">{{ slots.header|safe }}</div>
|
||||||
|
</header>
|
||||||
|
<div class="flex-grow grid gap-12 w-full px-4 py-12 md:py-16 {% if slots.aside %}max-w-5xl mx-auto md:grid-cols-[2fr_1fr]{% else %}max-w-2xl mx-auto{% endif %}">
|
||||||
|
<main class="prose max-w-none">
|
||||||
|
{% if slots.main %}<article class="reel-dropcap" data-reel-dropcap="true">{{ slots.main|safe }}</article>{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}
|
||||||
|
</main>
|
||||||
|
{% if slots.aside %}<aside class="w-full md:border-l md:border-[hsl(var(--primary)/0.3)] md:pl-10">{{ slots.aside|safe }}</aside>{% endif %}
|
||||||
|
</div>
|
||||||
|
<footer class="w-full mt-auto border-t border-[hsl(var(--primary)/0.35)]">
|
||||||
|
<div class="max-w-5xl mx-auto px-4">{{ slots.footer|safe }}</div>
|
||||||
|
</footer>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
<script>
|
||||||
|
(function(){if(window.__gothamMotion)return;window.__gothamMotion=1;var d=document,root=d.documentElement;root.classList.add('gotham-js');var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;function pending(){return d.querySelectorAll('.reveal:not(.is-visible)');}function reveal(el){el.classList.add('is-visible');}function inView(){var h=window.innerHeight||root.clientHeight;pending().forEach(function(el){var r=el.getBoundingClientRect();if(r.top<h*1.05&&r.bottom>-h*0.25)reveal(el);});}function all(){pending().forEach(reveal);}function run(){if(reduce){all();return;}inView();if(window.requestAnimationFrame){requestAnimationFrame(inView);}window.addEventListener("scroll",inView,{passive:true});window.addEventListener('resize',inView,{passive:true});window.addEventListener('load',function(){inView();setTimeout(inView,120);},{once:true});setTimeout(inView,400);setTimeout(all,650);d.querySelectorAll('[data-spotlight]').forEach(function(s){s.addEventListener('pointermove',function(ev){var r=s.getBoundingClientRect();s.style.setProperty('--sx',((ev.clientX-r.left)/r.width*100)+'%');s.style.setProperty('--sy',((ev.clientY-r.top)/r.height*100)+'%');});});}if(d.readyState!='loading')run();else d.addEventListener('DOMContentLoaded',run);})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
templates/gotham/auth.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col film-grain" style="font-family: var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif);">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<main class="flex-grow w-full flex flex-col items-center justify-center px-4 py-12" data-spotlight style="background-image:radial-gradient(circle at var(--sx,50%) var(--sy,42%), hsl(var(--primary)/0.10), transparent 60%);">
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<a href="/" class="block text-center" aria-label="{{ site_settings.Title }} home">
|
||||||
|
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt }}" class="mx-auto h-12 w-auto">{% else %}<span class="reel-caps text-2xl" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);color:hsl(var(--foreground));">{{ site_settings.Title|default:"BlockNinja" }}</span>{% endif %}
|
||||||
|
</a>
|
||||||
|
<div class="mx-auto mt-4 mb-6 h-4 w-32 reel-strip" aria-hidden="true"></div>
|
||||||
|
<div>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No form assigned to this page.</p></div>{% endif %}</div>
|
||||||
|
{% if slots.footer %}<div class="mt-6 text-center text-sm" style="color:hsl(var(--muted-foreground));">{{ slots.footer|safe }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
<script>
|
||||||
|
(function(){if(window.__gothamMotion)return;window.__gothamMotion=1;var d=document,root=d.documentElement;root.classList.add('gotham-js');var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;function pending(){return d.querySelectorAll('.reveal:not(.is-visible)');}function reveal(el){el.classList.add('is-visible');}function inView(){var h=window.innerHeight||root.clientHeight;pending().forEach(function(el){var r=el.getBoundingClientRect();if(r.top<h*1.05&&r.bottom>-h*0.25)reveal(el);});}function all(){pending().forEach(reveal);}function run(){if(reduce){all();return;}inView();if(window.requestAnimationFrame){requestAnimationFrame(inView);}window.addEventListener("scroll",inView,{passive:true});window.addEventListener('resize',inView,{passive:true});window.addEventListener('load',function(){inView();setTimeout(inView,120);},{once:true});setTimeout(inView,400);setTimeout(all,650);d.querySelectorAll('[data-spotlight]').forEach(function(s){s.addEventListener('pointermove',function(ev){var r=s.getBoundingClientRect();s.style.setProperty('--sx',((ev.clientX-r.left)/r.width*100)+'%');s.style.setProperty('--sy',((ev.clientY-r.top)/r.height*100)+'%');});});}if(d.readyState!='loading')run();else d.addEventListener('DOMContentLoaded',run);})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
templates/gotham/blog-index.ninjatpl
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col film-grain" style="font-family: var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif);">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
<div class="max-w-6xl mx-auto px-4 py-12 md:py-16">
|
||||||
|
{% if title %}
|
||||||
|
<div class="text-center reveal">
|
||||||
|
<h1 class="reel-caps reel-underline mx-auto w-fit text-4xl md:text-5xl" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);">{{ title }}</h1>
|
||||||
|
{% if page_meta.MetaDescription %}<p class="mt-4 text-lg italic" style="color:hsl(var(--muted-foreground));">{{ page_meta.MetaDescription }}</p>{% endif %}
|
||||||
|
<div class="mx-auto mt-6 h-4 w-40 reel-strip" aria-hidden="true"></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if slots.featured %}<section class="mt-12">{{ slots.featured|safe }}</section>{% endif %}
|
||||||
|
{% if slots.main %}<section class="mt-12">{{ slots.main|safe }}</section>{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No posts published yet.</p></div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% if slots.footer %}<footer class="w-full mt-auto border-t border-[hsl(var(--primary)/0.35)]">{{ slots.footer|safe }}</footer>{% endif %}
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
<script>
|
||||||
|
(function(){if(window.__gothamMotion)return;window.__gothamMotion=1;var d=document,root=d.documentElement;root.classList.add('gotham-js');var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;function pending(){return d.querySelectorAll('.reveal:not(.is-visible)');}function reveal(el){el.classList.add('is-visible');}function inView(){var h=window.innerHeight||root.clientHeight;pending().forEach(function(el){var r=el.getBoundingClientRect();if(r.top<h*1.05&&r.bottom>-h*0.25)reveal(el);});}function all(){pending().forEach(reveal);}function run(){if(reduce){all();return;}inView();if(window.requestAnimationFrame){requestAnimationFrame(inView);}window.addEventListener("scroll",inView,{passive:true});window.addEventListener('resize',inView,{passive:true});window.addEventListener('load',function(){inView();setTimeout(inView,120);},{once:true});setTimeout(inView,400);setTimeout(all,650);d.querySelectorAll('[data-spotlight]').forEach(function(s){s.addEventListener('pointermove',function(ev){var r=s.getBoundingClientRect();s.style.setProperty('--sx',((ev.clientX-r.left)/r.width*100)+'%');s.style.setProperty('--sy',((ev.clientY-r.top)/r.height*100)+'%');});});}if(d.readyState!='loading')run();else d.addEventListener('DOMContentLoaded',run);})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
templates/gotham/centered.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col film-grain" style="font-family: var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif);">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<header class="w-full border-b border-[hsl(var(--primary)/0.35)]"><div class="max-w-2xl mx-auto px-4">{{ slots.header|safe }}</div></header>
|
||||||
|
<main class="flex-grow max-w-2xl mx-auto w-full px-4 py-12 md:py-16">
|
||||||
|
{% if slots.main %}<article class="prose prose-lg max-w-none">{{ slots.main|safe }}</article>{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto border-t border-[hsl(var(--primary)/0.35)]"><div class="max-w-2xl mx-auto px-4">{{ slots.footer|safe }}</div></footer>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
<script>
|
||||||
|
(function(){if(window.__gothamMotion)return;window.__gothamMotion=1;var d=document,root=d.documentElement;root.classList.add('gotham-js');var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;function pending(){return d.querySelectorAll('.reveal:not(.is-visible)');}function reveal(el){el.classList.add('is-visible');}function inView(){var h=window.innerHeight||root.clientHeight;pending().forEach(function(el){var r=el.getBoundingClientRect();if(r.top<h*1.05&&r.bottom>-h*0.25)reveal(el);});}function all(){pending().forEach(reveal);}function run(){if(reduce){all();return;}inView();if(window.requestAnimationFrame){requestAnimationFrame(inView);}window.addEventListener("scroll",inView,{passive:true});window.addEventListener('resize',inView,{passive:true});window.addEventListener('load',function(){inView();setTimeout(inView,120);},{once:true});setTimeout(inView,400);setTimeout(all,650);d.querySelectorAll('[data-spotlight]').forEach(function(s){s.addEventListener('pointermove',function(ev){var r=s.getBoundingClientRect();s.style.setProperty('--sx',((ev.clientX-r.left)/r.width*100)+'%');s.style.setProperty('--sy',((ev.clientY-r.top)/r.height*100)+'%');});});}if(d.readyState!='loading')run();else d.addEventListener('DOMContentLoaded',run);})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
25
templates/gotham/contact.ninjatpl
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col film-grain" style="font-family: var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif);">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
<div class="max-w-6xl mx-auto px-4 py-12 md:py-16">
|
||||||
|
{% if slots.aside %}
|
||||||
|
<div class="grid gap-12 md:grid-cols-[2fr_1fr]">
|
||||||
|
<div>{% if slots.main %}{{ slots.main|safe }}{% endif %}</div>
|
||||||
|
<aside class="w-full md:border-l md:border-[hsl(var(--primary)/0.3)] md:pl-10">{{ slots.aside|safe }}</aside>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="max-w-3xl mx-auto">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% if slots.footer %}<footer class="w-full mt-auto border-t border-[hsl(var(--primary)/0.35)]">{{ slots.footer|safe }}</footer>{% endif %}
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
<script>
|
||||||
|
(function(){if(window.__gothamMotion)return;window.__gothamMotion=1;var d=document,root=d.documentElement;root.classList.add('gotham-js');var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;function pending(){return d.querySelectorAll('.reveal:not(.is-visible)');}function reveal(el){el.classList.add('is-visible');}function inView(){var h=window.innerHeight||root.clientHeight;pending().forEach(function(el){var r=el.getBoundingClientRect();if(r.top<h*1.05&&r.bottom>-h*0.25)reveal(el);});}function all(){pending().forEach(reveal);}function run(){if(reduce){all();return;}inView();if(window.requestAnimationFrame){requestAnimationFrame(inView);}window.addEventListener("scroll",inView,{passive:true});window.addEventListener('resize',inView,{passive:true});window.addEventListener('load',function(){inView();setTimeout(inView,120);},{once:true});setTimeout(inView,400);setTimeout(all,650);d.querySelectorAll('[data-spotlight]').forEach(function(s){s.addEventListener('pointermove',function(ev){var r=s.getBoundingClientRect();s.style.setProperty('--sx',((ev.clientX-r.left)/r.width*100)+'%');s.style.setProperty('--sy',((ev.clientY-r.top)/r.height*100)+'%');});});}if(d.readyState!='loading')run();else d.addEventListener('DOMContentLoaded',run);})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
templates/gotham/default.ninjatpl
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col film-grain" style="font-family: var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif);">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<header class="w-full">
|
||||||
|
<div class="max-w-5xl mx-auto px-4">{{ slots.header|safe }}</div>
|
||||||
|
</header>
|
||||||
|
<main class="flex-grow max-w-3xl mx-auto w-full px-4 py-12 md:py-16">
|
||||||
|
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto border-t border-[hsl(var(--primary)/0.35)]">
|
||||||
|
<div class="max-w-5xl mx-auto px-4">{{ slots.footer|safe }}</div>
|
||||||
|
</footer>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
<script>
|
||||||
|
(function(){if(window.__gothamMotion)return;window.__gothamMotion=1;var d=document,root=d.documentElement;root.classList.add('gotham-js');var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;function pending(){return d.querySelectorAll('.reveal:not(.is-visible)');}function reveal(el){el.classList.add('is-visible');}function inView(){var h=window.innerHeight||root.clientHeight;pending().forEach(function(el){var r=el.getBoundingClientRect();if(r.top<h*1.05&&r.bottom>-h*0.25)reveal(el);});}function all(){pending().forEach(reveal);}function run(){if(reduce){all();return;}inView();if(window.requestAnimationFrame){requestAnimationFrame(inView);}window.addEventListener("scroll",inView,{passive:true});window.addEventListener('resize',inView,{passive:true});window.addEventListener('load',function(){inView();setTimeout(inView,120);},{once:true});setTimeout(inView,400);setTimeout(all,650);d.querySelectorAll('[data-spotlight]').forEach(function(s){s.addEventListener('pointermove',function(ev){var r=s.getBoundingClientRect();s.style.setProperty('--sx',((ev.clientX-r.left)/r.width*100)+'%');s.style.setProperty('--sy',((ev.clientY-r.top)/r.height*100)+'%');});});}if(d.readyState!='loading')run();else d.addEventListener('DOMContentLoaded',run);})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
templates/gotham/full-width.ninjatpl
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col film-grain" style="font-family: var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif);">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<header class="w-full">{{ slots.header|safe }}</header>
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-4xl mx-auto py-20 px-4 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto border-t border-[hsl(var(--primary)/0.35)]">
|
||||||
|
<div class="max-w-5xl mx-auto px-4">{{ slots.footer|safe }}</div>
|
||||||
|
</footer>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
<script>
|
||||||
|
(function(){if(window.__gothamMotion)return;window.__gothamMotion=1;var d=document,root=d.documentElement;root.classList.add('gotham-js');var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;function pending(){return d.querySelectorAll('.reveal:not(.is-visible)');}function reveal(el){el.classList.add('is-visible');}function inView(){var h=window.innerHeight||root.clientHeight;pending().forEach(function(el){var r=el.getBoundingClientRect();if(r.top<h*1.05&&r.bottom>-h*0.25)reveal(el);});}function all(){pending().forEach(reveal);}function run(){if(reduce){all();return;}inView();if(window.requestAnimationFrame){requestAnimationFrame(inView);}window.addEventListener("scroll",inView,{passive:true});window.addEventListener('resize',inView,{passive:true});window.addEventListener('load',function(){inView();setTimeout(inView,120);},{once:true});setTimeout(inView,400);setTimeout(all,650);d.querySelectorAll('[data-spotlight]').forEach(function(s){s.addEventListener('pointermove',function(ev){var r=s.getBoundingClientRect();s.style.setProperty('--sx',((ev.clientX-r.left)/r.width*100)+'%');s.style.setProperty('--sy',((ev.clientY-r.top)/r.height*100)+'%');});});}if(d.readyState!='loading')run();else d.addEventListener('DOMContentLoaded',run);})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
templates/gotham/landing.ninjatpl
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col film-grain" style="font-family: var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif);">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<section class="w-full">{{ slots.hero|safe }}</section>
|
||||||
|
<main class="flex-grow">
|
||||||
|
{% if slots.main %}{{ slots.main|safe }}{% endif %}
|
||||||
|
</main>
|
||||||
|
<section class="w-full">{{ slots.cta|safe }}</section>
|
||||||
|
<footer class="w-full mt-auto border-t border-[hsl(var(--primary)/0.35)]">
|
||||||
|
<div class="max-w-5xl mx-auto px-4">{{ slots.footer|safe }}</div>
|
||||||
|
</footer>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
<script>
|
||||||
|
(function(){if(window.__gothamMotion)return;window.__gothamMotion=1;var d=document,root=d.documentElement;root.classList.add('gotham-js');var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;function pending(){return d.querySelectorAll('.reveal:not(.is-visible)');}function reveal(el){el.classList.add('is-visible');}function inView(){var h=window.innerHeight||root.clientHeight;pending().forEach(function(el){var r=el.getBoundingClientRect();if(r.top<h*1.05&&r.bottom>-h*0.25)reveal(el);});}function all(){pending().forEach(reveal);}function run(){if(reduce){all();return;}inView();if(window.requestAnimationFrame){requestAnimationFrame(inView);}window.addEventListener("scroll",inView,{passive:true});window.addEventListener('resize',inView,{passive:true});window.addEventListener('load',function(){inView();setTimeout(inView,120);},{once:true});setTimeout(inView,400);setTimeout(all,650);d.querySelectorAll('[data-spotlight]').forEach(function(s){s.addEventListener('pointermove',function(ev){var r=s.getBoundingClientRect();s.style.setProperty('--sx',((ev.clientX-r.left)/r.width*100)+'%');s.style.setProperty('--sy',((ev.clientY-r.top)/r.height*100)+'%');});});}if(d.readyState!='loading')run();else d.addEventListener('DOMContentLoaded',run);})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
templates/overrides/gotham/announcement-bar.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{# announcement-bar override — gilded banner. Dismiss <script>, data-bn-annkey and localStorage keying preserved verbatim. Fields mirror builtin: variant, message, linkText, linkUrl, dismissable. #}
|
||||||
|
{% set anncls = "bg-primary/15 text-foreground" %}
|
||||||
|
{% if variant == "promo" %}{% set anncls = "bg-primary text-primary-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
|
||||||
|
<div data-block-override="gotham:announcement-bar" data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full border-b border-[hsl(var(--primary)/0.4)] {{ anncls }} {{ class }}">
|
||||||
|
<div class="max-w-7xl mx-auto px-10 py-2 relative flex items-center justify-center gap-2 text-sm text-center">
|
||||||
|
<p class="reel-caps text-xs">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
|
||||||
|
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 rounded hover:bg-black/10 transition-colors"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if dismissable %}<script>
|
||||||
|
(function(){
|
||||||
|
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
|
||||||
|
bars.forEach(function(bar){
|
||||||
|
bar.setAttribute('data-bn-ann-wired','1');
|
||||||
|
var key='bnann:'+bar.getAttribute('data-bn-annkey');
|
||||||
|
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
|
||||||
|
var btn=bar.querySelector('[data-bn-ann-dismiss]');
|
||||||
|
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>{% endif %}
|
||||||
16
templates/overrides/gotham/audio-embed.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{# audio-embed override — gold-framed player card. Fields + facade JS mirror builtin verbatim: url, media, artwork, title, artist, download. #}
|
||||||
|
<figure data-block-override="gotham:audio-embed" class="bn-audio reel-frame my-8 overflow-hidden bg-card text-card-foreground{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="reel-mat h-16 w-16 shrink-0 overflow-hidden bg-muted">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="reel-caps truncate text-sm" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);color:hsl(var(--foreground));">{{ title }}</p>{% endif %}{% if artist %}<p class="truncate text-sm italic" style="color:hsl(var(--muted-foreground));">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="reel-btn inline-flex items-center gap-2 text-sm" aria-label="Play{% if title %}: {{ title }}{% endif %}"><svg class="inline-block h-4 w-4" width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg> Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="reel-link inline-flex items-center gap-1 text-sm"><svg class="inline-block h-4 w-4" width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#download"></use></svg> Download</a>{% endif %}</div></div></div></figure><script>
|
||||||
|
(function(){
|
||||||
|
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
|
||||||
|
document.addEventListener('click',function(e){
|
||||||
|
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
|
||||||
|
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
|
||||||
|
e.preventDefault();
|
||||||
|
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
|
||||||
|
if(!src)return;
|
||||||
|
var slot=btn.closest('[data-bn-audio-slot]');
|
||||||
|
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
|
||||||
|
btn.remove();slot.insertBefore(a,slot.firstChild);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
33
templates/overrides/gotham/auth-form.ninjatpl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{# auth-form override — gilded login/register. Wiring is UNTOUCHABLE and reproduced exactly from builtin auth_blocks.go: single shared #auth-message, /api/auth/login (email,password) and /api/auth/register (username,email,password), hx-target #auth-message. Captcha widget injected from _captcha_widget when captchaEnabled (server enforces). Fields: default_tab, show_social (social omitted — needs Go/site config; graceful). #}
|
||||||
|
{% if context.isPublicLoggedIn %}
|
||||||
|
<div data-block-override="gotham:auth-form" class="auth-form-logged-in mx-auto max-w-md text-center py-8">
|
||||||
|
<p class="italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">You are signed in as <strong style="color:hsl(var(--foreground));">{{ context.publicUsername }}</strong></p>
|
||||||
|
<a href="/account" class="reel-link mt-3 inline-block">My account</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div data-block-override="gotham:auth-form" class="auth-form reel-frame mx-auto max-w-md bg-card p-8">
|
||||||
|
<div id="auth-message"></div>
|
||||||
|
<div class="mb-6 flex border-b border-[hsl(var(--primary)/0.3)]">
|
||||||
|
<button type="button" onclick="document.getElementById('login-panel').classList.remove('hidden');document.getElementById('register-panel').classList.add('hidden');this.setAttribute('data-active','1');this.nextElementSibling.removeAttribute('data-active');" data-auth-tab {% if default_tab != "register" %}data-active="1"{% endif %} class="reel-caps flex-1 py-2 text-center text-xs border-b-2 border-transparent data-[active]:border-[hsl(var(--primary))] data-[active]:text-primary">Sign in</button>
|
||||||
|
<button type="button" onclick="document.getElementById('register-panel').classList.remove('hidden');document.getElementById('login-panel').classList.add('hidden');this.setAttribute('data-active','1');this.previousElementSibling.removeAttribute('data-active');" data-auth-tab {% if default_tab == "register" %}data-active="1"{% endif %} class="reel-caps flex-1 py-2 text-center text-xs border-b-2 border-transparent data-[active]:border-[hsl(var(--primary))] data-[active]:text-primary">Register</button>
|
||||||
|
</div>
|
||||||
|
<div id="login-panel" class="{% if default_tab == "register" %}hidden{% endif %}">
|
||||||
|
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
|
||||||
|
<div class="mb-3"><label class="reel-caps block text-xs mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-none border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
|
||||||
|
<div class="mb-4"><label class="reel-caps block text-xs mb-1" style="color:hsl(var(--primary));">Password</label><input type="password" name="password" required class="w-full rounded-none border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
|
||||||
|
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
|
||||||
|
<button type="submit" class="reel-btn w-full" hx-disabled-elt="this">Sign in</button>
|
||||||
|
<p class="text-center mt-3 text-sm"><a href="/forgot-password" class="reel-link">Forgot password?</a></p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="register-panel" class="{% if default_tab != "register" %}hidden{% endif %}">
|
||||||
|
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
|
||||||
|
<div class="mb-3"><label class="reel-caps block text-xs mb-1" style="color:hsl(var(--primary));">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="w-full rounded-none border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
|
||||||
|
<div class="mb-3"><label class="reel-caps block text-xs mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-none border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
|
||||||
|
<div class="mb-4"><label class="reel-caps block text-xs mb-1" style="color:hsl(var(--primary));">Password</label><input type="password" name="password" required minlength="8" class="w-full rounded-none border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
|
||||||
|
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
|
||||||
|
<button type="submit" class="reel-btn w-full" hx-disabled-elt="this">Create account</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
31
templates/overrides/gotham/auth-status.ninjatpl
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{# auth-status override — header login state. Logged-out: Sign in / Reserve links. Logged-in: gold monogram + dropdown (logout posts /api/auth/logout). Driven by context.isPublicLoggedIn / context.publicDisplayName / context.publicUsername. #}
|
||||||
|
{% if context.isPublicLoggedIn %}
|
||||||
|
<div data-block-override="gotham:auth-status" class="auth-status relative">
|
||||||
|
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="reel-caps flex items-center gap-2 text-xs opacity-90 hover:opacity-100 px-3 py-1.5 transition-colors">
|
||||||
|
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full text-[0.7rem] font-bold" style="background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));">{{ context.publicDisplayName|default:context.publicUsername|first|upper }}</span>
|
||||||
|
<span>{{ context.publicDisplayName|default:context.publicUsername }}</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
||||||
|
</button>
|
||||||
|
<div id="auth-dropdown-menu" class="hidden absolute right-0 top-full mt-1 w-48 reel-frame bg-card py-1 z-50">
|
||||||
|
<a href="/account" class="block px-4 py-2 text-sm text-foreground hover:bg-accent">My profile</a>
|
||||||
|
<a href="/account/reviews" class="block px-4 py-2 text-sm text-foreground hover:bg-accent">My reviews</a>
|
||||||
|
<hr class="my-1 reel-rule"/>
|
||||||
|
<button hx-post="/api/auth/logout" hx-swap="none" class="block w-full text-left px-4 py-2 text-sm text-destructive hover:bg-accent">Sign out</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var menu = document.getElementById('auth-dropdown-menu');
|
||||||
|
var btn = document.querySelector('.auth-status > button');
|
||||||
|
if (!menu || !btn) return;
|
||||||
|
if (!btn.contains(e.target) && !menu.contains(e.target)) { menu.classList.add('hidden'); }
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div data-block-override="gotham:auth-status" class="auth-status flex items-center gap-2">
|
||||||
|
<a href="/login" class="reel-caps text-xs px-3 py-1.5 opacity-80 hover:opacity-100 hover:text-primary transition-colors">Sign in</a>
|
||||||
|
<a href="/login?tab=register" class="reel-btn text-xs">Reserve</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
22
templates/overrides/gotham/author-bio-hero.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{# author-bio-hero (gotham) — author-page header from the `authors` provider. #}
|
||||||
|
{% if author %}
|
||||||
|
<section data-block-override="gotham:author-bio-hero" class="reel-rule-thick bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
|
||||||
|
{% if showAvatar and author.avatar_url %}<div class="mb-6 h-24 w-24 overflow-hidden rounded-full bg-muted ring-2 ring-primary">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||||
|
<div class="mb-4 flex items-center justify-center gap-4"><span class="reel-rule w-12"></span><span class="reel-rule w-12"></span></div>
|
||||||
|
<h1 class="reel-caps text-3xl md:text-4xl text-foreground" style="font-family:var(--font-heading, "Antonio", "Oswald", Georgia, serif)">{{ author.display_name }}</h1>
|
||||||
|
{% if author.job_title or author.company %}<p class="reel-caps mt-2 text-sm text-primary">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
|
||||||
|
{% if author.bio %}<p class="mt-5 max-w-2xl leading-relaxed text-muted-foreground" style="font-family:var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif)">{{ author.bio }}</p>{% endif %}
|
||||||
|
<div class="mt-6 flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground">
|
||||||
|
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
|
||||||
|
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if showSocial %}<div class="mt-6 flex items-center justify-center gap-4">
|
||||||
|
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Website"><svg class="h-5 w-5" width="20" height="20" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
|
||||||
|
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Twitter"><svg class="h-5 w-5" width="20" height="20" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
|
||||||
|
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="LinkedIn"><svg class="h-5 w-5" width="20" height="20" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
|
||||||
|
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="GitHub"><svg class="h-5 w-5" width="20" height="20" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
|
||||||
|
</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
14
templates/overrides/gotham/author-bio.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{# author-bio (gotham) — byline card sourced from the `post` provider's author_* fields. #}
|
||||||
|
{% if post.author_name %}
|
||||||
|
<aside data-block-override="gotham:author-bio" class="reel-frame bg-card p-6 text-card-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if heading %}<p class="reel-caps mb-4 text-xs text-primary">{{ heading }}</p>{% endif %}
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full bg-muted ring-1 ring-[hsl(var(--primary)/0.5)]">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h3 class="reel-caps text-base text-card-foreground" style="font-family:var(--font-heading, "Antonio", "Oswald", Georgia, serif)">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-primary">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
|
||||||
|
<span class="reel-rule mt-2 block w-10"></span>
|
||||||
|
{% if post.author_bio %}<p class="mt-2 text-sm text-muted-foreground" style="font-family:var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif)">{{ post.author_bio }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
{% endif %}
|
||||||
24
templates/overrides/gotham/blog-hero.ninjatpl
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{# blog-hero (gotham) — blog-index header + featured posts from the `posts` provider. #}
|
||||||
|
<section data-block-override="gotham:blog-hero" class="bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
<div class="mx-auto max-w-2xl text-center">
|
||||||
|
{% if title %}<h1 class="reel-caps reel-underline text-4xl md:text-5xl text-foreground" style="font-family:var(--font-heading, "Antonio", "Oswald", Georgia, serif)">{{ title }}</h1>{% endif %}
|
||||||
|
{% if subtitle %}<p class="mt-4 text-lg text-muted-foreground" style="font-family:var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif)">{{ subtitle }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if posts %}
|
||||||
|
<div class="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||||
|
{% for post in posts %}
|
||||||
|
<article class="group flex flex-col{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-8{% endif %}">
|
||||||
|
{% if post.featured_image_url %}<a href="{{ post.url }}" class="reel-mat mb-4 block overflow-hidden bg-muted{% if forloop.First %} md:mb-0 md:w-1/2{% endif %}">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" %}</a>{% endif %}
|
||||||
|
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
|
||||||
|
<span class="reel-rule mb-4 block w-12"></span>
|
||||||
|
<h2 class="reel-caps {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} leading-snug text-foreground" style="font-family:var(--font-heading, "Antonio", "Oswald", Georgia, serif)"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h2>
|
||||||
|
{% if showExcerpt and post.excerpt %}<p class="mt-3 text-sm text-muted-foreground" style="font-family:var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif)">{{ post.excerpt }}</p>{% endif %}
|
||||||
|
<div class="mt-4 flex flex-wrap items-center gap-x-3 text-xs text-muted-foreground">{% if post.author_name %}<span class="reel-caps text-primary">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
15
templates/overrides/gotham/breadcrumbs.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{# breadcrumbs (gotham) — trail from the `breadcrumbs` provider (page ancestry). #}
|
||||||
|
{# showHome prepends a Home link; separator is text ("/") for default or a chevron #}
|
||||||
|
{# for the arrows template. The trailing is_current item renders as plain text. #}
|
||||||
|
{% set arrows = template == "arrows" %}
|
||||||
|
<nav aria-label="Breadcrumb" data-block-override="gotham:breadcrumbs" class="py-3{% if class %} {{ class }}{% endif %}">
|
||||||
|
<ol class="flex flex-wrap items-center gap-1 text-xs">
|
||||||
|
{% if showHome %}<li class="flex items-center"><a href="/" class="reel-caps text-muted-foreground transition-colors hover:text-primary">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
|
||||||
|
{% for item in breadcrumbs %}
|
||||||
|
<li class="flex items-center">
|
||||||
|
{% if showHome or not forloop.First %}{% if arrows %}<svg class="mx-2 h-3.5 w-3.5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>{% else %}<span class="mx-2 text-primary">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
|
||||||
|
{% if item.is_current %}{% if showCurrent %}<span class="reel-caps text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="reel-caps text-muted-foreground transition-colors hover:text-primary">{{ item.label }}</a>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
10
templates/overrides/gotham/button.ninjatpl
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{# button override — gold-ringed lacquer pill. Fields (new builtin contract, per OVERRIDE-CONTRACT §4): link, style, label, size, icon, iconPosition, newTab, align. #}
|
||||||
|
{% with align=align|default:"left" %}
|
||||||
|
<div data-block-override="gotham:button" class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if style == "ghost" or style == "link" or style == "secondary" %}
|
||||||
|
{% if icon %}{% set _ic1 = icon %}{% endif %}<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="reel-link inline-flex items-center gap-2 text-sm">{% if icon and iconPosition != "right" %}<svg class="inline-block h-4 w-4" width="16" height="16" aria-hidden="true"><use href="/icons/{{ _ic1|split:":"|first }}.svg#{{ _ic1|split:":"|last }}"></use></svg>{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}<svg class="inline-block h-4 w-4" width="16" height="16" aria-hidden="true"><use href="/icons/{{ _ic1|split:":"|first }}.svg#{{ _ic1|split:":"|last }}"></use></svg>{% endif %}</a>
|
||||||
|
{% else %}
|
||||||
|
{% if icon %}{% set _ic2 = icon %}{% endif %}<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="reel-btn inline-flex items-center gap-2">{% if icon and iconPosition != "right" %}<svg class="inline-block h-4 w-4" width="16" height="16" aria-hidden="true"><use href="/icons/{{ _ic2|split:":"|first }}.svg#{{ _ic2|split:":"|last }}"></use></svg>{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}<svg class="inline-block h-4 w-4" width="16" height="16" aria-hidden="true"><use href="/icons/{{ _ic2|split:":"|first }}.svg#{{ _ic2|split:":"|last }}"></use></svg>{% endif %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
4
templates/overrides/gotham/card.ninjatpl
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{# card override — angular gold Deco panel. Fields mirror builtin: layout, link, media, title, text, linkLabel. Media via {% img %}. #}
|
||||||
|
{% with layout=layout|default:"vertical" %}
|
||||||
|
<article data-block-override="gotham:card" class="bn-card reel-frame overflow-hidden bg-card text-card-foreground transition-shadow hover:reel-foil {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">{% if link %}<a href="{{ link }}" class="contents">{% endif %}{% if media %}<div class="overflow-hidden bg-muted {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}<div class="flex flex-col gap-2 p-5 md:p-6 {% if layout == "horizontal" %}justify-center{% endif %}">{% if title %}<h3 class="reel-caps text-lg" style="font-family:var(--font-heading, 'Antonio', 'Oswald', 'Bebas Neue', sans-serif);color:hsl(var(--foreground));">{{ title }}</h3>{% endif %}{% if text %}<div class="text-sm leading-relaxed" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="mt-2 inline-flex items-center gap-1 text-sm reel-link">{{ linkLabel }}<svg class="inline-block h-4 w-4" width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></span>{% endif %}</div>{% if link %}</a>{% endif %}</article>
|
||||||
|
{% endwith %}
|
||||||