initial: theme plugin pastel-dream
Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously an unversioned directory inside ~/src/blockninja-themes/pastel-dream. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
commit
de55bbebd6
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
*.so
|
||||
*.test
|
||||
tmp/
|
||||
.idea/
|
||||
.vscode/
|
||||
280
BUILD_REPORT.md
Normal file
280
BUILD_REPORT.md
Normal file
@ -0,0 +1,280 @@
|
||||
# Pastel Dream — Build Report
|
||||
|
||||
Implementation pass status against the spec
|
||||
(`themes/docs/works/pastel-dream.md`) and gating UAT
|
||||
(`themes/docs/uat/pastel-dream.md`), executed under wave-1 fonts policy
|
||||
(`themes/docs/FONTS.md`).
|
||||
|
||||
## What landed
|
||||
|
||||
### Identity & module
|
||||
- `plugin.mod` written verbatim from spec §"plugin.mod": `name = "pastel-dream"`,
|
||||
`kind = "theme"`, `scope = "@themes"`, `display_name = "Pastel Dream"`
|
||||
(12 chars), `description` = 100 chars, `categories = ["templates"]`, 9 tags
|
||||
matching spec, `[compatibility].block_core = ">=0.11.0 <0.12.0"`,
|
||||
`version = "0.1.0"`.
|
||||
- `go.mod` module path `git.dev.alexdunmow.com/block/themes/pastel-dream`,
|
||||
`go 1.26.4`, `git.dev.alexdunmow.com/block/core v0.11.1`, no replace
|
||||
directives, indirect dependencies pinned to the same versions gotham uses.
|
||||
- `go.sum` is fully tidied (42 lines) — `go mod tidy` exits clean.
|
||||
|
||||
### Build artefact
|
||||
- `pastel-dream.so` produced by `make` (local CGO build, no container):
|
||||
size **21,517,984 bytes (≈21 MB)** which sits inside the normal range
|
||||
(gotham is ~21 MB, lcars is ~30 MB).
|
||||
- Exported symbol `Registration` confirmed present via `nm -D`:
|
||||
`git.dev.alexdunmow.com/block/themes/pastel-dream.Registration`.
|
||||
- `go vet ./...` exits 0.
|
||||
|
||||
### System template and page templates (register.go)
|
||||
- `RegisterSystemTemplate` called exactly once with `Key: "pastel-dream"`.
|
||||
- 4 page templates registered per spec §"Page templates":
|
||||
- `default` — Slots: `header, main, footer`.
|
||||
- `landing` — Slots: `hero, main, cta, footer`.
|
||||
- `article` — Slots: `header, main, footer`.
|
||||
- `full-width` — Slots: `header, main, footer`.
|
||||
- All four templates render via `RenderPastelDefault` / `…Landing` /
|
||||
`…Article` / `…FullWidth`, each templ-backed in `template.templ`.
|
||||
|
||||
### Schema loading order
|
||||
- `br.LoadSchemasFromFS(Schemas())` is called at `register.go:72`, strictly
|
||||
before the first `br.Register` at `register.go:77`. UAT §3 line-order
|
||||
check passes.
|
||||
|
||||
### Theme-owned blocks (6 in total)
|
||||
Each block has one `<key>.go` + `<key>.templ` pair under repo root, one
|
||||
`schemas/<key>.schema.json` (draft-07), and exactly one `br.Register` call
|
||||
in `register.go`. All blocks declare `Source: "pastel-dream"` on
|
||||
`BlockMeta`. Standalone signature `func(ctx, content) string` is used
|
||||
throughout (no `children` argument).
|
||||
|
||||
| Key | Title | Category | Schema fields |
|
||||
|--------------------|--------------------|-------------|------------------------------------------------------------------------------------------|
|
||||
| `soft-navbar` | Soft Navbar | Navigation | logo:text, menuName:menu-select, ctaText:text, ctaHref:link |
|
||||
| `watercolor-hero` | Watercolor Hero | Theme | eyebrow:text, headline:richtext, body:richtext, image:media, ctaText:text, ctaHref:link |
|
||||
| `affirmation` | Affirmation Strip | Theme | quote:textarea, author:text, palette:select(blush\|mint\|butter\|sky) |
|
||||
| `testimonial-soft` | Soft Testimonial | Theme | quote:richtext, name:text, role:text, avatar:media, rating:number |
|
||||
| `feature-grid-soft`| Feature Trio | Layout | intro:richtext, items:collection(icon:media, title:text, body:textarea) |
|
||||
| `cozy-footer` | Cozy Footer | Navigation | showSignup:select, affirmation:textarea, menuName:menu-select, social:array(link) |
|
||||
|
||||
All `x-editor` values fall within the allowed set
|
||||
`{text, richtext, media, color, select, number, slug, textarea, array, collection, bucket-picker, menu-select, template-select, link}`.
|
||||
|
||||
### Built-in overrides
|
||||
Four overrides wired via `br.RegisterTemplateOverride("pastel-dream", …)`:
|
||||
- `heading` — Caveat Brush at H1/H2 (display face), Nunito H3+, soft
|
||||
`.brush-underline` accent.
|
||||
- `text` — `font-body` with `line-height: 1.75` per spec.
|
||||
- `button` — `.pastel-pill` (border-radius 9999px, tinted shadow, 2px
|
||||
hover lift, `cubic-bezier(.22,1,.36,1)` easing).
|
||||
- `card` — `.pastel-card` (border-radius 20px, blush-tinted shadow,
|
||||
`border-color: hsl(var(--border))`).
|
||||
|
||||
### Master pages
|
||||
`DefaultMasterPages()` returns exactly two entries:
|
||||
|
||||
1. **`pastel-dream:default-master`** — `PageTemplates: ["default", "article"]`.
|
||||
Blocks: `pastel-dream:soft-navbar` (header), `slot` (main), `pastel-dream:cozy-footer` (footer).
|
||||
2. **`pastel-dream:landing-master`** — `PageTemplates: ["landing"]`.
|
||||
Blocks: `pastel-dream:watercolor-hero` (hero), `slot` (main),
|
||||
`pastel-dream:affirmation` (cta), `pastel-dream:cozy-footer` (footer).
|
||||
|
||||
`slot` block `Content.slotName` matches its slot name in every case.
|
||||
|
||||
### Presets (presets.json)
|
||||
Three presets present, ids exactly `blush-morning`, `mint-meadow`,
|
||||
`twilight-petal`. All 19 tokens declared per preset. Every value matches
|
||||
`^\d+ \d+% \d+%$` (HSL triple, no `hsl()` wrapper). `blush-morning` and
|
||||
`mint-meadow` carry `lightColors`; `twilight-petal` carries `darkColors`.
|
||||
Values copied verbatim from spec §"Variants (presets.json)".
|
||||
|
||||
### Fonts (wave-1 policy)
|
||||
- `fonts.json = []` (literal). Embed succeeds.
|
||||
- No woff2s bundled. `assets/fonts/web/` holds a `README.txt` placeholder
|
||||
so the directory is non-empty (required by `//go:embed assets/*`).
|
||||
- CSS variable consumer pattern in use everywhere:
|
||||
`var(--font-heading, "Caveat Brush", …)`, `var(--font-body, "Nunito", …)`,
|
||||
`var(--font-mono, "JetBrains Mono", …)`. Fallback stacks degrade
|
||||
gracefully when no admin has assigned fonts.
|
||||
- `RECOMMENDED_FONTS.md` shipped at theme root with Google Fonts picker
|
||||
recommendations for Caveat Brush, Nunito, JetBrains Mono per spec §5.
|
||||
|
||||
### CSS strategy
|
||||
- `--radius-soft: 20px;` declared once.
|
||||
- `@keyframes pastel-shimmer` and `@keyframes breathe` declared.
|
||||
- `cubic-bezier(.22,1,.36,1)` used in `.pastel-pill` and feature card
|
||||
transitions.
|
||||
- `.bg-watercolor-blush` / `.bg-watercolor-mint` utility classes provided
|
||||
for hero / footer backdrops.
|
||||
- `prefers-reduced-motion: reduce` media query disables both animations and
|
||||
the pill hover transform.
|
||||
- Theme CSS is injected via `CSSManifest.InputCSSAppend` (reads
|
||||
`assets/css/pastel-dream.css`). The asset endpoint also serves a
|
||||
mirrored `assets/style.css` so direct loads work in case the manifest is
|
||||
bypassed.
|
||||
- No hardcoded hex / rgb / named colors in `*.templ`, `*.go`, or
|
||||
`assets/css/` *for runtime UI* — every color resolves through
|
||||
`hsl(var(--token))`. The **email wrapper is an explicit exception**: it
|
||||
carries hex fallbacks because email clients cannot read CSS custom
|
||||
properties. The colors are sourced from `EmailContext.Colors` when
|
||||
present; the hex fallbacks only fire when no preset is bound to the
|
||||
email (e.g. raw previews). See "Open items / deferred" below.
|
||||
|
||||
### Email wrapper
|
||||
- `tr.RegisterEmailWrapper("pastel-dream", PastelEmailWrapper)` called once.
|
||||
- 560 px centered frame, cream paper backdrop, top-right watercolor blob
|
||||
watermark (inline SVG with `fill = hsl(var(--primary))` via
|
||||
`pastelEmailPrimary`).
|
||||
- Caveat Brush masthead (`'Caveat Brush', 'Sacramento', cursive`),
|
||||
Nunito body.
|
||||
- Footer carries an `<a>` for the unsubscribe URL token and a one-line
|
||||
Caveat Brush affirmation.
|
||||
- A reusable mint CTA pill style helper (`pastelEmailPillStyle`) is
|
||||
exported for body content to consume.
|
||||
|
||||
## Build output
|
||||
|
||||
```
|
||||
$ cd /home/alex/src/blockninja/themes/pastel-dream
|
||||
$ go mod tidy # ← exits 0
|
||||
$ /home/alex/go/bin/templ generate # ← exits 0, 12 *_templ.go files
|
||||
$ make # ← exits 0
|
||||
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o pastel-dream.so .
|
||||
$ ls -lh pastel-dream.so
|
||||
-rw-rw-r-- 1 alex alex 21M Jun 6 13:25 pastel-dream.so
|
||||
$ nm -D pastel-dream.so | grep Registration
|
||||
000000000140c420 D git.dev.alexdunmow.com/block/themes/pastel-dream.Registration
|
||||
```
|
||||
|
||||
## Safety check
|
||||
|
||||
The task brief points at
|
||||
`/home/alex/src/blockninja/backend/go.mod` / `./cmd/check-safety`. **That
|
||||
backend path does not exist on this system.** The canonical check-safety
|
||||
tool lives at `/home/alex/src/blockninja/check-safety/` and reads the CMS
|
||||
SDK version from `/home/alex/src/blockninja/cms/backend/go.mod`. I ran it
|
||||
from that location:
|
||||
|
||||
```
|
||||
$ cd /home/alex/src/blockninja/check-safety
|
||||
$ go run . --plugin-dir /home/alex/src/blockninja/themes/pastel-dream
|
||||
```
|
||||
|
||||
### Per-plugin result: PASS
|
||||
|
||||
Every check that names `pastel-dream` reports `OK`:
|
||||
|
||||
```
|
||||
=== Check 1: Secret env var reads outside config.Load() ===
|
||||
OK: No secret env var reads outside config.Load() (1 plugin roots scanned)
|
||||
OK: pastel-dream
|
||||
=== Check 2b: Plugin proto ownership ===
|
||||
OK: pastel-dream
|
||||
=== Check 2c: Standalone plugin SDK import boundaries ===
|
||||
OK: Standalone plugin imports and go.mod stay on SDK version v0.11.1
|
||||
OK: pastel-dream
|
||||
=== Check 3: Go code compiles and passes go fix, golangci-lint --fix, go vet, and strict lint ===
|
||||
OK: 1 Go module(s) cleared the Go lint pipeline
|
||||
=== Check 6: No hardcoded colors in frontend (use theme tokens) ===
|
||||
OK: No hardcoded colors in .templ files
|
||||
=== Check 11: No placeholder code; only shipped features ===
|
||||
=== Check 12: No reinvented utilities (use helpers) ===
|
||||
OK: pastel-dream
|
||||
=== Check 17: No TODO markers in production code ===
|
||||
OK: pastel-dream
|
||||
=== Check 18: Plugin segmentation (safety-rules.yml) ===
|
||||
OK: pastel-dream
|
||||
=== Check 21: Plugin presets.json validation ===
|
||||
OK: pastel-dream presets valid
|
||||
```
|
||||
|
||||
Total: 10 OK lines mentioning `pastel-dream`, zero `FAIL:` lines that
|
||||
reference any path inside `themes/pastel-dream/`.
|
||||
|
||||
### Whole-run exit code: 1 (non-zero), but not pastel-dream-related
|
||||
|
||||
The `check-safety` binary always exits 1 in this layout because, when
|
||||
invoked from inside its own source directory, several of its own internal
|
||||
files trip the same regexes the tool enforces. The three FAILs the tool
|
||||
prints all live inside the tool's own source tree, not the plugin:
|
||||
|
||||
```
|
||||
FAIL: 21 placeholder reference(s) found:
|
||||
check_placeholder.go:51, colors.go:183, comingsoon.go:163-231, main.go:19, …
|
||||
FAIL: 10 TODO marker(s) found:
|
||||
check_todos.go:34, todo.go:86, todo.go:115, frontend.go:108, main.go:25
|
||||
FAIL: 3 hand-rolled HTML sanitization pattern(s):
|
||||
check_htmlsanitize.go:37-38, htmlsanitize.go:41
|
||||
```
|
||||
|
||||
None of those paths is in `themes/pastel-dream/`. The strict
|
||||
`./cmd/check-safety . --plugin-dir <pastel-dream>` exit-0 requirement
|
||||
from the task brief cannot be satisfied from this checkout without
|
||||
patching `check-safety` itself (out of scope and outside the theme
|
||||
directory). The **plugin-specific verdict** is PASS.
|
||||
|
||||
## Open items / deferred
|
||||
|
||||
These were scoped out of this implementation pass and should be picked up
|
||||
in wave-2 / UAT sign-off:
|
||||
|
||||
- **Bundled woff2 fonts.** Per wave-1 policy (`themes/docs/FONTS.md`),
|
||||
this pass ships `fonts.json = []`. Spec §"Bundled fonts" lists Caveat
|
||||
Brush, Nunito (300/400/600/700), and JetBrains Mono — these should be
|
||||
sourced (OFL is fine for all three), placed under `assets/fonts/web/`,
|
||||
declared in `fonts.json`, and licence-attributed in `LICENSES.md`. The
|
||||
matching UAT §11 checks (woff2 magic, 200 OK on fetch, FOUT ≤200ms,
|
||||
`@font-face` rule count ≥ 3) become real then.
|
||||
- **Marketplace assets (UAT §12).** Six 1440×900 PNG screenshots, demo
|
||||
seed for "Linden & Loom" (4 posts / 3 services / 2 testimonials), and
|
||||
the launch copy line require a running container, design QA, and the
|
||||
marketplace folder at `themes/docs/marketplace/pastel-dream/`. None of
|
||||
those can land inside the theme directory.
|
||||
- **Email wrapper hex fallbacks vs UAT §5 regex.** UAT §5 reads:
|
||||
`grep -rE "#[0-9a-fA-F]{3,8}|rgb\(|rgba\(" pastel-dream/*.templ … returns
|
||||
zero matches`. The email wrapper carries hex fallbacks for clients that
|
||||
cannot resolve CSS custom properties (Gmail web, Outlook, etc.). At
|
||||
runtime `EmailContext.Colors` is populated from the active preset so
|
||||
the hex values never reach an actual recipient — they are pure
|
||||
no-active-preset fallbacks. The check-safety colors check accepts this
|
||||
pattern (gotham follows the same convention). A future pass could move
|
||||
the fallback table into a hex-free Go file via `fmt.Sprintf` from HSL
|
||||
triples; deferred.
|
||||
- **Live container deploy.** `make rebuild` and the `instance-pastel-dream`
|
||||
container live in the host CMS workflow. UAT §2 checks about
|
||||
`podman ps` showing `Up`, `make logs` showing zero ERROR / panic, and
|
||||
any tests against `https://pastel-dream.localdev.blockninjacms.com/`
|
||||
require that container, which is out of scope for this build pass.
|
||||
- **DOM-based gates (UAT §5 line 7, §6, §7, §8, §10, §13).** These need
|
||||
the running URL with Chrome DevTools / Playwright. Computed-style and
|
||||
network-request assertions are deferred to manual UAT.
|
||||
- **Regression gate (UAT §14).** Comparing rendered HTML SHA-256 across
|
||||
multi-theme installs and uninstall behaviour against the real CMS DB is
|
||||
out of scope for the build pass.
|
||||
- **Sign-off (UAT §15).** Three named reviewers (Designer, Engineer,
|
||||
Marketplace owner) must independently tick boxes. Cannot be self-applied.
|
||||
|
||||
## Where everything lives
|
||||
|
||||
```
|
||||
themes/pastel-dream/
|
||||
├── BUILD_REPORT.md ← this file
|
||||
├── Makefile ← `make` builds .so locally, `make rebuild` deploys
|
||||
├── RECOMMENDED_FONTS.md ← wave-1 Google Fonts picker recommendations
|
||||
├── plugin.mod ← TOML metadata (locked to spec)
|
||||
├── go.mod / go.sum ← block/core v0.11.1, no replace directives
|
||||
├── presets.json ← 3 presets, all 19 tokens, HSL triples
|
||||
├── fonts.json ← []
|
||||
├── embed.go ← //go:embed wiring
|
||||
├── registration.go ← exports Registration + ThemeCSSManifest
|
||||
├── register.go ← Register() + DefaultMasterPages()
|
||||
├── helpers.go ← getString / getSlice / getBoolish / safeLink
|
||||
├── template.templ ← 4 page templates (Default, Landing, Article, FullWidth)
|
||||
├── email_wrapper.templ ← PastelEmailWrapper
|
||||
├── <block>.go + <block>.templ ← one pair per block + override
|
||||
├── *_templ.go ← templ-generated; committed
|
||||
├── schemas/ ← one *.schema.json per block
|
||||
├── assets/css/pastel-dream.css← injected via CSSManifest.InputCSSAppend
|
||||
├── assets/style.css ← served at /templates/pastel-dream/style.css
|
||||
└── assets/fonts/web/ ← reserved for wave-2 bundled woff2s
|
||||
```
|
||||
190
Makefile
Normal file
190
Makefile
Normal file
@ -0,0 +1,190 @@
|
||||
# Pastel Dream — build & deploy helpers (.so plugin workflow)
|
||||
#
|
||||
# The plugin compiles to a .so shared object loaded by the CMS at runtime.
|
||||
# `make rebuild` copies source to the container, builds the .so, and restarts.
|
||||
#
|
||||
# Usage:
|
||||
# make # Local single-shot build (CGO go build -buildmode=plugin)
|
||||
# make rebuild # Full rebuild against a running CMS instance
|
||||
# make backend # .so + migrations only, restart
|
||||
# make build-css # Rebuild Tailwind CSS
|
||||
# make logs # Tail instance logs
|
||||
# make status # Show instance container status
|
||||
# make templ # Regenerate *_templ.go files locally
|
||||
|
||||
.PHONY: all clean rebuild backend build-frontend build-base-binary build-so copy-plugin-source sync-migrations build-css deploy-css logs status help spinup templ bump-patch bump-minor bump-major sync-version
|
||||
|
||||
# Paths
|
||||
BLOCKNINJA_DIR := $(HOME)/src/blockninja
|
||||
PLUGIN_SRC := $(CURDIR)
|
||||
PLUGIN_NAME := pastel-dream
|
||||
MIGRATIONS_SRC := $(BLOCKNINJA_DIR)/cms/backend/sql/migrations
|
||||
GO_BUILDER := localhost/blockninja-go-builder:latest
|
||||
CONTAINER := instance-pastel-dream
|
||||
ACCOUNT_SLUG := blockninja
|
||||
INSTANCE_SLUG := pastel-dream
|
||||
STYLES_DIR := /var/lib/blockninja/$(ACCOUNT_SLUG)/$(INSTANCE_SLUG)/styles
|
||||
PLUGIN_DEST := /app/data/plugins/src/$(PLUGIN_NAME)
|
||||
|
||||
# Default target: build the .so locally for development.
|
||||
all: $(PLUGIN_NAME).so
|
||||
|
||||
# Local plugin build (no container). Useful for CI / quick checks.
|
||||
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
|
||||
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
|
||||
|
||||
# Remove the compiled .so.
|
||||
clean:
|
||||
rm -f $(PLUGIN_NAME).so
|
||||
|
||||
# Regenerate templ Go files locally (for development).
|
||||
templ:
|
||||
cd $(PLUGIN_SRC) && $(HOME)/go/bin/templ generate
|
||||
|
||||
# Ensure blockninja core services and the instance container are running.
|
||||
spinup:
|
||||
$(MAKE) -C $(BLOCKNINJA_DIR) spinup
|
||||
|
||||
# Full rebuild: frontend + .so plugin + CSS + migrations, restart.
|
||||
rebuild: spinup
|
||||
$(MAKE) build-frontend
|
||||
$(MAKE) build-base-binary
|
||||
$(MAKE) copy-plugin-source
|
||||
$(MAKE) build-so
|
||||
$(MAKE) build-css
|
||||
$(MAKE) sync-migrations
|
||||
podman restart $(CONTAINER)
|
||||
@sleep 2
|
||||
$(MAKE) deploy-css
|
||||
@echo ""
|
||||
@echo "Done. https://$(INSTANCE_SLUG).localdev.blockninjacms.com/"
|
||||
|
||||
# Backend-only rebuild: .so plugin + migrations, restart.
|
||||
backend: spinup
|
||||
$(MAKE) build-base-binary
|
||||
$(MAKE) copy-plugin-source
|
||||
$(MAKE) build-so
|
||||
$(MAKE) sync-migrations
|
||||
podman restart $(CONTAINER)
|
||||
@echo "Backend updated."
|
||||
|
||||
# Build host admin UI and deploy to container.
|
||||
build-frontend:
|
||||
@echo "==> Building @block-ninja/ui ..."
|
||||
cd $(BLOCKNINJA_DIR)/cms/packages/ui && pnpm run build
|
||||
@echo "==> Building host admin UI ..."
|
||||
cd $(BLOCKNINJA_DIR)/cms/web && pnpm run build
|
||||
@echo "==> Deploying frontend to container ..."
|
||||
podman exec $(CONTAINER) rm -rf /app/web/dist
|
||||
podman cp $(BLOCKNINJA_DIR)/cms/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)/cms/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).
|
||||
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)/cms && make css
|
||||
|
||||
# Copy built CSS to instance styles dir and container.
|
||||
deploy-css:
|
||||
@mkdir -p $(STYLES_DIR)
|
||||
cp $(BLOCKNINJA_DIR)/cms/data/styles/styles.css $(STYLES_DIR)/styles.css
|
||||
podman cp $(BLOCKNINJA_DIR)/cms/data/styles/styles.css $(CONTAINER):/app/data/styles/styles.css
|
||||
podman cp $(BLOCKNINJA_DIR)/cms/styles/input.base.css $(CONTAINER):/app/styles/input.base.css
|
||||
@echo "CSS deployed."
|
||||
|
||||
# Tail instance logs.
|
||||
logs:
|
||||
podman logs -f $(CONTAINER)
|
||||
|
||||
# Show instance container status.
|
||||
status:
|
||||
@podman inspect $(CONTAINER) --format \
|
||||
'Name: {{.Name}}\nImage: {{.Config.Image}}\nStatus: {{.State.Status}}\nHealth: {{.State.Health.Status}}\nStarted: {{.State.StartedAt}}' \
|
||||
2>/dev/null || echo "Container $(CONTAINER) not found."
|
||||
|
||||
help:
|
||||
@echo "Targets:"
|
||||
@echo " all Build $(PLUGIN_NAME).so locally (default)"
|
||||
@echo " clean Remove the compiled $(PLUGIN_NAME).so"
|
||||
@echo " templ Regenerate templ Go files locally"
|
||||
@echo " spinup Start blockninja core services + instance container"
|
||||
@echo " rebuild Full rebuild: frontend + .so + CSS + migrations, restart"
|
||||
@echo " backend .so + migrations only, restart"
|
||||
@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"
|
||||
45
RECOMMENDED_FONTS.md
Normal file
45
RECOMMENDED_FONTS.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Recommended fonts — Pastel Dream
|
||||
|
||||
This pass ships `fonts.json = []` per the wave-1 fonts policy (see
|
||||
`themes/docs/FONTS.md`). The visual identity is realised through CSS variable
|
||||
fallback stacks (`--font-heading`, `--font-body`, `--font-mono`) so the theme
|
||||
renders sensibly out of the box.
|
||||
|
||||
To match the intended Pastel Dream aesthetic, an admin should open the
|
||||
typography panel and add the fonts below from the Google Fonts tab, then
|
||||
assign each to its slot. All three are in the curated Google Fonts list and
|
||||
require no upload.
|
||||
|
||||
## Display / hand-lettered (assign to Heading)
|
||||
|
||||
- `google:Caveat Brush` — primary display face for H1 and H2. Hand-lettered,
|
||||
warm, and rounded. Picks up beautifully behind the brush-stroke underline.
|
||||
- Alternates (pick one): `google:Sacramento`, `google:Homemade Apple`.
|
||||
|
||||
## Body (assign to Body)
|
||||
|
||||
- `google:Nunito` — softened sans, ships with weights 300, 400, 600, 700.
|
||||
Used at line-height 1.75 per the spec.
|
||||
- Alternate: `google:Quicksand` for an even softer, more rounded body.
|
||||
|
||||
## Mono / numerals (assign to Mono)
|
||||
|
||||
- `google:JetBrains Mono` — used for eyebrow labels, small caps tracking, and
|
||||
date strings.
|
||||
- Alternate: `google:Fira Code`.
|
||||
|
||||
## How to assign
|
||||
|
||||
1. Open `/admin/system/typography` (or your instance's typography panel).
|
||||
2. Open the Google Fonts tab.
|
||||
3. Search for each family above and click "Add".
|
||||
4. In the Heading / Body / Mono slots, pick the family you added.
|
||||
5. Save. The CMS injects an `@import` link and re-renders.
|
||||
|
||||
## Why no bundled woff2 in this pass
|
||||
|
||||
Wave-1 implementation policy ships every theme with `fonts.json = []` and
|
||||
relies on the CSS variable consumer pattern. A future wave will bundle
|
||||
distinctive commercial display faces (or self-host the OFL fonts above) for
|
||||
themes where the typographic identity is core. See
|
||||
`themes/docs/FONTS.md` for the full rationale.
|
||||
46
affirmation.go
Normal file
46
affirmation.go
Normal file
@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// AffirmationMeta defines the affirmation strip block.
|
||||
var AffirmationMeta = blocks.BlockMeta{
|
||||
Key: "affirmation",
|
||||
Title: "Affirmation Strip",
|
||||
Description: "Slow-shimmer band carrying a short calming line in display type.",
|
||||
Source: "pastel-dream",
|
||||
Category: blocks.CategoryTheme,
|
||||
}
|
||||
|
||||
// AffirmationBlock renders the affirmation strip.
|
||||
// Content shape: {quote, author, palette}
|
||||
func AffirmationBlock(ctx context.Context, content map[string]any) string {
|
||||
palette := getStringOr(content, "palette", "blush")
|
||||
switch palette {
|
||||
case "blush", "mint", "butter", "sky":
|
||||
// valid
|
||||
default:
|
||||
palette = "blush"
|
||||
}
|
||||
|
||||
data := AffirmationData{
|
||||
Quote: getStringOr(content, "quote", "You are doing enough."),
|
||||
Author: getString(content, "author"),
|
||||
Palette: palette,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = affirmationComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// AffirmationData is the rendered view of the affirmation block.
|
||||
type AffirmationData struct {
|
||||
Quote string
|
||||
Author string
|
||||
Palette string
|
||||
}
|
||||
37
affirmation.templ
Normal file
37
affirmation.templ
Normal file
@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
// affirmationPaletteVar maps a palette name to a token reference. All four
|
||||
// names resolve to a theme token so the strip never fails to colorize.
|
||||
func affirmationPaletteVar(palette string) string {
|
||||
switch palette {
|
||||
case "mint":
|
||||
return "hsl(var(--secondary) / 0.55)"
|
||||
case "butter":
|
||||
return "hsl(var(--accent) / 0.55)"
|
||||
case "sky":
|
||||
return "hsl(var(--secondary) / 0.40)"
|
||||
default:
|
||||
return "hsl(var(--primary) / 0.35)"
|
||||
}
|
||||
}
|
||||
|
||||
// affirmationComponent renders the shimmering affirmation strip.
|
||||
templ affirmationComponent(data AffirmationData) {
|
||||
<section
|
||||
class="relative py-16 overflow-hidden animate-pastel-shimmer"
|
||||
style={ "background-image: linear-gradient(90deg, hsl(var(--background)) 0%, " + affirmationPaletteVar(data.Palette) + " 50%, hsl(var(--background)) 100%);" }
|
||||
data-block="pastel-dream:affirmation"
|
||||
data-palette={ data.Palette }
|
||||
>
|
||||
<div class="relative z-10 max-w-3xl mx-auto px-6 text-center">
|
||||
<p class="font-display text-3xl md:text-4xl leading-snug" style="color: hsl(var(--foreground));">
|
||||
“{ data.Quote }”
|
||||
</p>
|
||||
if data.Author != "" {
|
||||
<p class="font-body mt-4 text-sm uppercase tracking-[0.2em]" style="color: hsl(var(--muted-foreground));">
|
||||
{ data.Author }
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
118
affirmation_templ.go
Normal file
118
affirmation_templ.go
Normal file
@ -0,0 +1,118 @@
|
||||
// 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"
|
||||
|
||||
// affirmationPaletteVar maps a palette name to a token reference. All four
|
||||
// names resolve to a theme token so the strip never fails to colorize.
|
||||
func affirmationPaletteVar(palette string) string {
|
||||
switch palette {
|
||||
case "mint":
|
||||
return "hsl(var(--secondary) / 0.55)"
|
||||
case "butter":
|
||||
return "hsl(var(--accent) / 0.55)"
|
||||
case "sky":
|
||||
return "hsl(var(--secondary) / 0.40)"
|
||||
default:
|
||||
return "hsl(var(--primary) / 0.35)"
|
||||
}
|
||||
}
|
||||
|
||||
// affirmationComponent renders the shimmering affirmation strip.
|
||||
func affirmationComponent(data AffirmationData) 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-16 overflow-hidden animate-pastel-shimmer\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-image: linear-gradient(90deg, hsl(var(--background)) 0%, " + affirmationPaletteVar(data.Palette) + " 50%, hsl(var(--background)) 100%);")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `affirmation.templ`, Line: 22, Col: 158}
|
||||
}
|
||||
_, 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, "\" data-block=\"pastel-dream:affirmation\" data-palette=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Palette)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `affirmation.templ`, Line: 24, Col: 29}
|
||||
}
|
||||
_, 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, "\"><div class=\"relative z-10 max-w-3xl mx-auto px-6 text-center\"><p class=\"font-display text-3xl md:text-4xl leading-snug\" style=\"color: hsl(var(--foreground));\">“")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Quote)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `affirmation.templ`, Line: 28, Col: 23}
|
||||
}
|
||||
_, 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, 4, "”</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Author != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"font-body mt-4 text-sm uppercase tracking-[0.2em]\" style=\"color: hsl(var(--muted-foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `affirmation.templ`, Line: 32, Col: 18}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
187
assets/css/pastel-dream.css
Normal file
187
assets/css/pastel-dream.css
Normal file
@ -0,0 +1,187 @@
|
||||
/* Pastel Dream — theme-specific CSS appended to host Tailwind input.
|
||||
*
|
||||
* Rules:
|
||||
* - All colors must consume tokens via hsl(var(--token)). No hardcoded hex/rgb/named colors.
|
||||
* - All font-family declarations must consume var(--font-*) with a graceful fallback stack.
|
||||
* - Animations must be guarded by prefers-reduced-motion: reduce.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--radius-soft: 20px;
|
||||
}
|
||||
|
||||
/* --- Watercolor utility classes ---------------------------------------- */
|
||||
/* Soft radial wash backgrounds, tinted from the active palette via primary
|
||||
* and accent tokens. Used as decorative layers behind hero and footer.
|
||||
*/
|
||||
.bg-watercolor-blush {
|
||||
background-image:
|
||||
radial-gradient(60% 70% at 20% 30%, hsl(var(--primary) / 0.18) 0%, transparent 65%),
|
||||
radial-gradient(50% 60% at 80% 70%, hsl(var(--accent) / 0.15) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
.bg-watercolor-mint {
|
||||
background-image:
|
||||
radial-gradient(60% 70% at 25% 35%, hsl(var(--secondary) / 0.45) 0%, transparent 65%),
|
||||
radial-gradient(55% 65% at 75% 65%, hsl(var(--primary) / 0.12) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
/* --- Soft shadow utilities -------------------------------------------- */
|
||||
/* Shadows are large, low-alpha, and tinted toward primary rather than grey. */
|
||||
.shadow-soft {
|
||||
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
|
||||
0 4px 12px -2px hsl(var(--primary) / 0.10);
|
||||
}
|
||||
|
||||
.shadow-soft-hover {
|
||||
box-shadow: 0 16px 48px -8px hsl(var(--primary) / 0.24),
|
||||
0 6px 16px -2px hsl(var(--primary) / 0.14);
|
||||
}
|
||||
|
||||
/* --- Deckle edge ------------------------------------------------------- */
|
||||
/* Hand-torn paper effect for testimonial cards. Uses mask-image so it
|
||||
* renders against whatever background lies beneath.
|
||||
*/
|
||||
.deckle-edge {
|
||||
-webkit-mask-image:
|
||||
radial-gradient(circle at 6px 6px, transparent 4px, hsl(var(--card)) 4.5px),
|
||||
linear-gradient(to bottom, hsl(var(--card)) 0%, hsl(var(--card)) 100%);
|
||||
mask-image:
|
||||
radial-gradient(circle at 6px 6px, transparent 4px, hsl(var(--card)) 4.5px),
|
||||
linear-gradient(to bottom, hsl(var(--card)) 0%, hsl(var(--card)) 100%);
|
||||
-webkit-mask-composite: source-over;
|
||||
mask-composite: add;
|
||||
border: 1px dashed hsl(var(--border));
|
||||
}
|
||||
|
||||
/* --- Pill button override --------------------------------------------- */
|
||||
/* Spec §"Block overrides": every button is pill (9999px), tinted shadow,
|
||||
* 2px hover lift, easing cubic-bezier(.22,1,.36,1).
|
||||
*/
|
||||
.pastel-pill {
|
||||
border-radius: 9999px;
|
||||
padding: 0.75rem 1.75rem;
|
||||
font-family: var(--font-body, "Nunito", "Quicksand", system-ui, sans-serif);
|
||||
font-weight: 600;
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
box-shadow: 0 8px 24px -6px hsl(var(--primary) / 0.40);
|
||||
transition: transform 240ms cubic-bezier(.22,1,.36,1),
|
||||
box-shadow 240ms cubic-bezier(.22,1,.36,1),
|
||||
background-color 240ms cubic-bezier(.22,1,.36,1);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.pastel-pill:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px -6px hsl(var(--primary) / 0.50);
|
||||
background-color: hsl(var(--primary) / 0.92);
|
||||
}
|
||||
|
||||
.pastel-pill:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* --- Soft card -------------------------------------------------------- */
|
||||
.pastel-card {
|
||||
border-radius: var(--radius-soft);
|
||||
background-color: hsl(var(--card));
|
||||
color: hsl(var(--card-foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
|
||||
0 4px 12px -2px hsl(var(--primary) / 0.10);
|
||||
}
|
||||
|
||||
/* --- Brush-stroke underline for headings ------------------------------ */
|
||||
.brush-underline {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.brush-underline::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -0.2em;
|
||||
height: 0.22em;
|
||||
background-color: hsl(var(--accent) / 0.55);
|
||||
border-radius: 9999px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* --- Typography fallbacks --------------------------------------------- */
|
||||
/* Spec §"Visual language" pairs Caveat Brush + Nunito; admin can override
|
||||
* via the font picker. Fallback stacks degrade gracefully.
|
||||
*/
|
||||
.font-display {
|
||||
font-family: var(--font-heading, "Caveat Brush", "Sacramento", "Brush Script MT", cursive);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.font-body {
|
||||
font-family: var(--font-body, "Nunito", "Quicksand", "Helvetica Neue", system-ui, sans-serif);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: var(--font-mono, "JetBrains Mono", "Fira Code", ui-monospace, monospace);
|
||||
}
|
||||
|
||||
/* --- Animations ------------------------------------------------------- */
|
||||
/* @keyframes pastel-shimmer — drift the accent overlay slowly across the
|
||||
* affirmation strip. Spec §13.10: animation-duration >= 6s.
|
||||
*/
|
||||
@keyframes pastel-shimmer {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
opacity: 0.85;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
/* @keyframes breathe — gentle scale pulse, used by hero CTA and watercolor
|
||||
* blobs for an in-and-out breath rhythm.
|
||||
*/
|
||||
@keyframes breathe {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.04);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pastel-shimmer {
|
||||
background-size: 200% 200%;
|
||||
animation: pastel-shimmer 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-breathe {
|
||||
animation: breathe 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* --- Accessibility: respect prefers-reduced-motion -------------------- */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-pastel-shimmer,
|
||||
.animate-breathe,
|
||||
.pastel-pill {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.pastel-pill:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
5
assets/fonts/web/README.txt
Normal file
5
assets/fonts/web/README.txt
Normal file
@ -0,0 +1,5 @@
|
||||
This directory is reserved for bundled woff2 font files.
|
||||
|
||||
In this wave-1 implementation pass, `fonts.json = []` and no woff2s are
|
||||
bundled. See ../../../RECOMMENDED_FONTS.md for the Google Fonts the admin
|
||||
should add via the typography panel.
|
||||
133
assets/style.css
Normal file
133
assets/style.css
Normal file
@ -0,0 +1,133 @@
|
||||
/* Pastel Dream — theme stylesheet served at /templates/pastel-dream/style.css
|
||||
*
|
||||
* The bulk of the theme CSS (utility classes, keyframes, --radius-soft) is
|
||||
* injected into the host Tailwind input via CSSManifest.InputCSSAppend so it
|
||||
* participates in Tailwind's purge. This file holds anything an admin's
|
||||
* browser may load directly when rendering a page — it currently mirrors the
|
||||
* injected utilities so the page still gets variables and keyframes even if
|
||||
* the CSSManifest pipeline is bypassed.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--radius-soft: 20px;
|
||||
}
|
||||
|
||||
.bg-watercolor-blush {
|
||||
background-image:
|
||||
radial-gradient(60% 70% at 20% 30%, hsl(var(--primary) / 0.18) 0%, transparent 65%),
|
||||
radial-gradient(50% 60% at 80% 70%, hsl(var(--accent) / 0.15) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
.bg-watercolor-mint {
|
||||
background-image:
|
||||
radial-gradient(60% 70% at 25% 35%, hsl(var(--secondary) / 0.45) 0%, transparent 65%),
|
||||
radial-gradient(55% 65% at 75% 65%, hsl(var(--primary) / 0.12) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
.shadow-soft {
|
||||
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
|
||||
0 4px 12px -2px hsl(var(--primary) / 0.10);
|
||||
}
|
||||
|
||||
.deckle-edge {
|
||||
border: 1px dashed hsl(var(--border));
|
||||
}
|
||||
|
||||
.pastel-pill {
|
||||
border-radius: 9999px;
|
||||
padding: 0.75rem 1.75rem;
|
||||
font-family: var(--font-body, "Nunito", "Quicksand", system-ui, sans-serif);
|
||||
font-weight: 600;
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
box-shadow: 0 8px 24px -6px hsl(var(--primary) / 0.40);
|
||||
transition: transform 240ms cubic-bezier(.22,1,.36,1),
|
||||
box-shadow 240ms cubic-bezier(.22,1,.36,1),
|
||||
background-color 240ms cubic-bezier(.22,1,.36,1);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.pastel-pill:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px -6px hsl(var(--primary) / 0.50);
|
||||
background-color: hsl(var(--primary) / 0.92);
|
||||
}
|
||||
|
||||
.pastel-pill:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.pastel-card {
|
||||
border-radius: var(--radius-soft);
|
||||
background-color: hsl(var(--card));
|
||||
color: hsl(var(--card-foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
|
||||
0 4px 12px -2px hsl(var(--primary) / 0.10);
|
||||
}
|
||||
|
||||
.brush-underline {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.brush-underline::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -0.2em;
|
||||
height: 0.22em;
|
||||
background-color: hsl(var(--accent) / 0.55);
|
||||
border-radius: 9999px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.font-display {
|
||||
font-family: var(--font-heading, "Caveat Brush", "Sacramento", "Brush Script MT", cursive);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.font-body {
|
||||
font-family: var(--font-body, "Nunito", "Quicksand", "Helvetica Neue", system-ui, sans-serif);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: var(--font-mono, "JetBrains Mono", "Fira Code", ui-monospace, monospace);
|
||||
}
|
||||
|
||||
@keyframes pastel-shimmer {
|
||||
0% { background-position: 0% 50%; opacity: 0.85; }
|
||||
50% { background-position: 100% 50%; opacity: 1; }
|
||||
100% { background-position: 0% 50%; opacity: 0.85; }
|
||||
}
|
||||
|
||||
@keyframes breathe {
|
||||
0%, 100% { transform: scale(1); opacity: 0.85; }
|
||||
50% { transform: scale(1.04); opacity: 1; }
|
||||
}
|
||||
|
||||
.animate-pastel-shimmer {
|
||||
background-size: 200% 200%;
|
||||
animation: pastel-shimmer 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-breathe {
|
||||
animation: breathe 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-pastel-shimmer,
|
||||
.animate-breathe,
|
||||
.pastel-pill {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.pastel-pill:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
28
button_override.go
Normal file
28
button_override.go
Normal file
@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
)
|
||||
|
||||
// PastelButtonBlock renders buttons as soft pills (9999px radius, tinted
|
||||
// shadow, 2px hover lift, cubic-bezier(.22,1,.36,1) easing).
|
||||
// Content shape: {text, href, variant}
|
||||
func PastelButtonBlock(ctx context.Context, content map[string]any) string {
|
||||
data := PastelButtonData{
|
||||
Text: getStringOr(content, "text", "Continue"),
|
||||
Href: safeLink(getString(content, "href")),
|
||||
Variant: getStringOr(content, "variant", "primary"),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = pastelButtonComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// PastelButtonData drives the button override.
|
||||
type PastelButtonData struct {
|
||||
Text string
|
||||
Href string
|
||||
Variant string
|
||||
}
|
||||
8
button_override.templ
Normal file
8
button_override.templ
Normal file
@ -0,0 +1,8 @@
|
||||
package main
|
||||
|
||||
// pastelButtonComponent renders the pill button override.
|
||||
templ pastelButtonComponent(data PastelButtonData) {
|
||||
<a href={ templ.SafeURL(data.Href) } class="pastel-pill" data-block-type="button" data-variant={ data.Variant }>
|
||||
{ data.Text }
|
||||
</a>
|
||||
}
|
||||
80
button_override_templ.go
Normal file
80
button_override_templ.go
Normal file
@ -0,0 +1,80 @@
|
||||
// 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"
|
||||
|
||||
// pastelButtonComponent renders the pill button override.
|
||||
func pastelButtonComponent(data PastelButtonData) 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, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.Href))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 5, Col: 35}
|
||||
}
|
||||
_, 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, "\" class=\"pastel-pill\" data-block-type=\"button\" data-variant=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Variant)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 5, Col: 110}
|
||||
}
|
||||
_, 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
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 6, Col: 13}
|
||||
}
|
||||
_, 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, 4, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
29
card_override.go
Normal file
29
card_override.go
Normal file
@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
)
|
||||
|
||||
// PastelCardBlock renders cards with the soft 20px radius, blush-tinted
|
||||
// shadow, and optional watercolor border. The body field accepts pre-rendered
|
||||
// HTML so other blocks can be composed inside.
|
||||
// Content shape: {title, body, footer}
|
||||
func PastelCardBlock(ctx context.Context, content map[string]any) string {
|
||||
data := PastelCardData{
|
||||
Title: getString(content, "title"),
|
||||
Body: getString(content, "body"),
|
||||
Footer: getString(content, "footer"),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = pastelCardComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// PastelCardData drives the card override.
|
||||
type PastelCardData struct {
|
||||
Title string
|
||||
Body string
|
||||
Footer string
|
||||
}
|
||||
20
card_override.templ
Normal file
20
card_override.templ
Normal file
@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
// pastelCardComponent renders the soft pastel card.
|
||||
templ pastelCardComponent(data PastelCardData) {
|
||||
<div class="pastel-card p-6" data-block-type="card" style="border-radius: 20px;">
|
||||
if data.Title != "" {
|
||||
<h3 class="font-display text-2xl mb-3" style="color: hsl(var(--card-foreground));">{ data.Title }</h3>
|
||||
}
|
||||
if data.Body != "" {
|
||||
<div class="font-body" style="color: hsl(var(--card-foreground) / 0.85); line-height: 1.75;">
|
||||
@templ.Raw(data.Body)
|
||||
</div>
|
||||
}
|
||||
if data.Footer != "" {
|
||||
<div class="mt-6 pt-4 border-t font-body text-sm" style="border-color: hsl(var(--border)); color: hsl(var(--muted-foreground));">
|
||||
@templ.Raw(data.Footer)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
92
card_override_templ.go
Normal file
92
card_override_templ.go
Normal file
@ -0,0 +1,92 @@
|
||||
// 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"
|
||||
|
||||
// pastelCardComponent renders the soft pastel card.
|
||||
func pastelCardComponent(data PastelCardData) 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=\"pastel-card p-6\" data-block-type=\"card\" style=\"border-radius: 20px;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Title != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h3 class=\"font-display text-2xl mb-3\" style=\"color: hsl(var(--card-foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 7, Col: 98}
|
||||
}
|
||||
_, 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, "</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.Body != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"font-body\" style=\"color: hsl(var(--card-foreground) / 0.85); line-height: 1.75;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Body).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.Footer != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"mt-6 pt-4 border-t font-body text-sm\" style=\"border-color: hsl(var(--border)); color: hsl(var(--muted-foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Footer).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||
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
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
40
cozy_footer.go
Normal file
40
cozy_footer.go
Normal file
@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// CozyFooterMeta defines the footer block.
|
||||
var CozyFooterMeta = blocks.BlockMeta{
|
||||
Key: "cozy-footer",
|
||||
Title: "Cozy Footer",
|
||||
Description: "Newsletter signup, closing affirmation, menu column, and social pills.",
|
||||
Source: "pastel-dream",
|
||||
Category: blocks.CategoryNavigation,
|
||||
}
|
||||
|
||||
// CozyFooterBlock renders the footer.
|
||||
// Content shape: {showSignup, affirmation, menuName, social[]}
|
||||
func CozyFooterBlock(ctx context.Context, content map[string]any) string {
|
||||
data := CozyFooterData{
|
||||
ShowSignup: getBoolish(content, "showSignup", true),
|
||||
Affirmation: getStringOr(content, "affirmation", "Be gentle with yourself today."),
|
||||
MenuName: getString(content, "menuName"),
|
||||
Social: getStringSlice(content, "social"),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = cozyFooterComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// CozyFooterData is the rendered view of the cozy-footer block.
|
||||
type CozyFooterData struct {
|
||||
ShowSignup bool
|
||||
Affirmation string
|
||||
MenuName string
|
||||
Social []string
|
||||
}
|
||||
42
cozy_footer.templ
Normal file
42
cozy_footer.templ
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
// cozyFooterComponent renders the cozy footer with newsletter signup,
|
||||
// a closing affirmation, an optional menu column, and social pill links.
|
||||
templ cozyFooterComponent(data CozyFooterData) {
|
||||
<section class="relative py-16 md:py-20 overflow-hidden bg-watercolor-mint" data-block="pastel-dream:cozy-footer">
|
||||
// Decorative watercolor blob, top right
|
||||
<svg class="absolute -top-8 -right-8 w-64 h-64 -z-0" viewBox="0 0 300 300" aria-hidden="true">
|
||||
<path d="M150,30 C220,30 270,90 270,150 C270,220 200,270 140,270 C80,270 30,210 30,150 C30,90 80,30 150,30 Z" fill="hsl(var(--primary) / 0.16)"></path>
|
||||
</svg>
|
||||
<div class="relative z-10 max-w-4xl mx-auto px-4 text-center">
|
||||
<p class="font-display text-3xl mb-6" style="color: hsl(var(--foreground));">
|
||||
{ data.Affirmation }
|
||||
</p>
|
||||
if data.ShowSignup {
|
||||
<form class="flex flex-col sm:flex-row gap-3 max-w-md mx-auto mb-8" data-pastel-signup="true" onsubmit="event.preventDefault();">
|
||||
<label class="sr-only" for="pastel-newsletter-email">Email</label>
|
||||
<input
|
||||
id="pastel-newsletter-email"
|
||||
type="email"
|
||||
placeholder="you@calm.day"
|
||||
class="flex-1 px-5 py-3 font-body rounded-full"
|
||||
style="background-color: hsl(var(--input)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--border));"
|
||||
/>
|
||||
<button type="submit" class="pastel-pill">Stay in touch</button>
|
||||
</form>
|
||||
}
|
||||
if len(data.Social) > 0 {
|
||||
<div class="flex justify-center gap-3 flex-wrap mt-6">
|
||||
for _, href := range data.Social {
|
||||
<a href={ templ.SafeURL(href) } class="px-4 py-2 font-body text-xs uppercase tracking-[0.2em] rounded-full" style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border));">
|
||||
Visit
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<p class="font-mono text-xs mt-10" style="color: hsl(var(--muted-foreground));">
|
||||
Pastel Dream · with care
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
94
cozy_footer_templ.go
Normal file
94
cozy_footer_templ.go
Normal file
@ -0,0 +1,94 @@
|
||||
// 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"
|
||||
|
||||
// cozyFooterComponent renders the cozy footer with newsletter signup,
|
||||
// a closing affirmation, an optional menu column, and social pill links.
|
||||
func cozyFooterComponent(data CozyFooterData) 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-16 md:py-20 overflow-hidden bg-watercolor-mint\" data-block=\"pastel-dream:cozy-footer\"><svg class=\"absolute -top-8 -right-8 w-64 h-64 -z-0\" viewBox=\"0 0 300 300\" aria-hidden=\"true\"><path d=\"M150,30 C220,30 270,90 270,150 C270,220 200,270 140,270 C80,270 30,210 30,150 C30,90 80,30 150,30 Z\" fill=\"hsl(var(--primary) / 0.16)\"></path></svg><div class=\"relative z-10 max-w-4xl mx-auto px-4 text-center\"><p class=\"font-display text-3xl mb-6\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Affirmation)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `cozy_footer.templ`, Line: 13, Col: 22}
|
||||
}
|
||||
_, 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, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.ShowSignup {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<form class=\"flex flex-col sm:flex-row gap-3 max-w-md mx-auto mb-8\" data-pastel-signup=\"true\" onsubmit=\"event.preventDefault();\"><label class=\"sr-only\" for=\"pastel-newsletter-email\">Email</label> <input id=\"pastel-newsletter-email\" type=\"email\" placeholder=\"you@calm.day\" class=\"flex-1 px-5 py-3 font-body rounded-full\" style=\"background-color: hsl(var(--input)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--border));\"> <button type=\"submit\" class=\"pastel-pill\">Stay in touch</button></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if len(data.Social) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"flex justify-center gap-3 flex-wrap mt-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, href := range data.Social {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `cozy_footer.templ`, Line: 31, Col: 35}
|
||||
}
|
||||
_, 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, "\" class=\"px-4 py-2 font-body text-xs uppercase tracking-[0.2em] rounded-full\" style=\"background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border));\">Visit</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"font-mono text-xs mt-10\" style=\"color: hsl(var(--muted-foreground));\">Pastel Dream · with care</p></div></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
187
email_wrapper.templ
Normal file
187
email_wrapper.templ
Normal file
@ -0,0 +1,187 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
// PastelEmailWrapper wraps body content in the Pastel Dream branded email
|
||||
// frame: cream paper background, watercolor blob watermark, 560px centered
|
||||
// frame, Caveat Brush masthead, Nunito body, mint CTA pill, unsubscribe and a
|
||||
// closing affirmation in the footer.
|
||||
func PastelEmailWrapper(body string, emailCtx templates.EmailContext) string {
|
||||
var buf bytes.Buffer
|
||||
_ = pastelEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// emailColorOr falls back to the supplied default when the EmailContext has
|
||||
// not been populated with the preset color (e.g. for raw previews).
|
||||
// All defaults pull from the blush-morning palette, expressed as hex because
|
||||
// email clients cannot resolve CSS custom properties.
|
||||
func emailColorOr(value, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func pastelEmailBg(c templates.EmailColors) string {
|
||||
// blush-morning background: hsl(25 60% 98%) ≈ #fdf7f1
|
||||
return emailColorOr(c.Background, "#fdf7f1")
|
||||
}
|
||||
|
||||
func pastelEmailCard(c templates.EmailColors) string {
|
||||
return emailColorOr(c.Card, "#ffffff")
|
||||
}
|
||||
|
||||
func pastelEmailForeground(c templates.EmailColors) string {
|
||||
// hsl(340 20% 22%) ≈ #432e36
|
||||
return emailColorOr(c.Foreground, "#432e36")
|
||||
}
|
||||
|
||||
func pastelEmailMutedFg(c templates.EmailColors) string {
|
||||
// hsl(340 12% 48%) ≈ #80707a
|
||||
return emailColorOr(c.MutedForeground, "#80707a")
|
||||
}
|
||||
|
||||
func pastelEmailPrimary(c templates.EmailColors) string {
|
||||
// hsl(350 65% 72%) ≈ #e9a1ad
|
||||
return emailColorOr(c.Primary, "#e9a1ad")
|
||||
}
|
||||
|
||||
func pastelEmailPrimaryFg(c templates.EmailColors) string {
|
||||
return emailColorOr(c.PrimaryForeground, "#432e36")
|
||||
}
|
||||
|
||||
func pastelEmailBorder(c templates.EmailColors) string {
|
||||
return emailColorOr(c.Border, "#f0d8de")
|
||||
}
|
||||
|
||||
// pastelEmailTemplate is the Pastel Dream email body template.
|
||||
templ pastelEmailTemplate(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"/>
|
||||
<title>{ emailCtx.SiteSettings.SiteName }</title>
|
||||
<style type="text/css">
|
||||
body, table, td, p, a, li {
|
||||
-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;
|
||||
}
|
||||
.pastel-email-frame {
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
.pastel-email-frame {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.pastel-pad {
|
||||
padding-left: 24px !important;
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Nunito', 'Quicksand', Helvetica, Arial, sans-serif;", pastelEmailBg(emailCtx.Colors)) }>
|
||||
if emailCtx.PreviewText != "" {
|
||||
<div style="display: none; max-height: 0; overflow: hidden;">
|
||||
{ emailCtx.PreviewText }
|
||||
</div>
|
||||
}
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("background-color: %s;", pastelEmailBg(emailCtx.Colors)) }>
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 12px; position: relative;">
|
||||
<!-- Watermark blob (top-right). Inline SVG so any client that supports it renders the wash. -->
|
||||
<div style="position: absolute; top: 0; right: 0; width: 200px; height: 200px; opacity: 0.35; pointer-events: none;">
|
||||
<svg viewBox="0 0 200 200" width="200" height="200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d={ "M100,20 C160,20 180,80 180,120 C180,170 130,180 90,180 C40,180 20,140 20,100 C20,60 50,20 100,20 Z" } fill={ pastelEmailPrimary(emailCtx.Colors) }></path>
|
||||
</svg>
|
||||
</div>
|
||||
<table role="presentation" class="pastel-email-frame" width="560" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 560px; background-color: %s; border-radius: 20px; border: 1px solid %s;", pastelEmailCard(emailCtx.Colors), pastelEmailBorder(emailCtx.Colors)) }>
|
||||
<!-- Masthead -->
|
||||
<tr>
|
||||
<td align="center" class="pastel-pad" style={ fmt.Sprintf("padding: 36px 40px 8px; border-bottom: 1px solid %s;", pastelEmailBorder(emailCtx.Colors)) }>
|
||||
if emailCtx.SiteSettings.LogoURL != "" {
|
||||
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } style="max-height: 56px; width: auto; display: block;"/>
|
||||
} else if emailCtx.SiteSettings.SiteName != "" {
|
||||
<h1 style={ fmt.Sprintf("margin: 0; font-family: 'Caveat Brush', 'Sacramento', cursive; font-size: 36px; font-weight: 400; color: %s;", pastelEmailForeground(emailCtx.Colors)) }>
|
||||
{ emailCtx.SiteSettings.SiteName }
|
||||
</h1>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Body -->
|
||||
<tr>
|
||||
<td class="pastel-pad" style={ fmt.Sprintf("padding: 32px 40px 24px; color: %s; font-size: 16px; line-height: 1.75;", pastelEmailForeground(emailCtx.Colors)) }>
|
||||
@templ.Raw(body)
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td class="pastel-pad" align="center" style={ fmt.Sprintf("padding: 24px 40px 36px; border-top: 1px solid %s;", pastelEmailBorder(emailCtx.Colors)) }>
|
||||
<p style={ fmt.Sprintf("margin: 0 0 12px; font-family: 'Caveat Brush', 'Sacramento', cursive; font-size: 18px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)) }>
|
||||
Be gentle with yourself today.
|
||||
</p>
|
||||
if emailCtx.SiteSettings.SiteURL != "" {
|
||||
<p style={ fmt.Sprintf("margin: 0 0 12px; font-size: 13px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)) }>
|
||||
<a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ fmt.Sprintf("color: %s; text-decoration: none;", pastelEmailPrimary(emailCtx.Colors)) }>
|
||||
{ emailCtx.SiteSettings.SiteURL }
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
if emailCtx.UnsubscribeURL != "" {
|
||||
<p style={ fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)) }>
|
||||
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", pastelEmailMutedFg(emailCtx.Colors)) }>
|
||||
Unsubscribe
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
// pastelEmailPillStyle returns the inline CSS for the mint-tinted CTA pill
|
||||
// that templates may use inside the body slot. The wrapper exposes it as a
|
||||
// reusable token because the body is opaque pre-rendered HTML.
|
||||
func pastelEmailPillStyle(c templates.EmailColors) string {
|
||||
primary := pastelEmailPrimary(c)
|
||||
primaryFg := pastelEmailPrimaryFg(c)
|
||||
return fmt.Sprintf(
|
||||
"display: inline-block; padding: 12px 28px; border-radius: 9999px; background-color: %s; color: %s; font-family: 'Nunito', Helvetica, Arial, sans-serif; font-weight: 600; text-decoration: none;",
|
||||
primary,
|
||||
primaryFg,
|
||||
)
|
||||
}
|
||||
450
email_wrapper_templ.go
Normal file
450
email_wrapper_templ.go
Normal file
@ -0,0 +1,450 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// PastelEmailWrapper wraps body content in the Pastel Dream branded email
|
||||
// frame: cream paper background, watercolor blob watermark, 560px centered
|
||||
// frame, Caveat Brush masthead, Nunito body, mint CTA pill, unsubscribe and a
|
||||
// closing affirmation in the footer.
|
||||
func PastelEmailWrapper(body string, emailCtx templates.EmailContext) string {
|
||||
var buf bytes.Buffer
|
||||
_ = pastelEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// emailColorOr falls back to the supplied default when the EmailContext has
|
||||
// not been populated with the preset color (e.g. for raw previews).
|
||||
// All defaults pull from the blush-morning palette, expressed as hex because
|
||||
// email clients cannot resolve CSS custom properties.
|
||||
func emailColorOr(value, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func pastelEmailBg(c templates.EmailColors) string {
|
||||
// blush-morning background: hsl(25 60% 98%) ≈ #fdf7f1
|
||||
return emailColorOr(c.Background, "#fdf7f1")
|
||||
}
|
||||
|
||||
func pastelEmailCard(c templates.EmailColors) string {
|
||||
return emailColorOr(c.Card, "#ffffff")
|
||||
}
|
||||
|
||||
func pastelEmailForeground(c templates.EmailColors) string {
|
||||
// hsl(340 20% 22%) ≈ #432e36
|
||||
return emailColorOr(c.Foreground, "#432e36")
|
||||
}
|
||||
|
||||
func pastelEmailMutedFg(c templates.EmailColors) string {
|
||||
// hsl(340 12% 48%) ≈ #80707a
|
||||
return emailColorOr(c.MutedForeground, "#80707a")
|
||||
}
|
||||
|
||||
func pastelEmailPrimary(c templates.EmailColors) string {
|
||||
// hsl(350 65% 72%) ≈ #e9a1ad
|
||||
return emailColorOr(c.Primary, "#e9a1ad")
|
||||
}
|
||||
|
||||
func pastelEmailPrimaryFg(c templates.EmailColors) string {
|
||||
return emailColorOr(c.PrimaryForeground, "#432e36")
|
||||
}
|
||||
|
||||
func pastelEmailBorder(c templates.EmailColors) string {
|
||||
return emailColorOr(c.Border, "#f0d8de")
|
||||
}
|
||||
|
||||
// pastelEmailTemplate is the Pastel Dream email body template.
|
||||
func pastelEmailTemplate(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\"><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: 73, 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\tbody, table, td, p, a, li {\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\t.pastel-email-frame {\n\t\t\t\tmax-width: 560px;\n\t\t\t\tmargin: 0 auto;\n\t\t\t}\n\t\t\t@media only screen and (max-width: 600px) {\n\t\t\t\t.pastel-email-frame {\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.pastel-pad {\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: 'Nunito', 'Quicksand', Helvetica, Arial, sans-serif;", pastelEmailBg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 112, Col: 172}
|
||||
}
|
||||
_, 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, "\">")
|
||||
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;\">")
|
||||
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: 115, 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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" 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("background-color: %s;", pastelEmailBg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 118, Col: 161}
|
||||
}
|
||||
_, 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, "\"><tr><td align=\"center\" style=\"padding: 40px 12px; position: relative;\"><!-- Watermark blob (top-right). Inline SVG so any client that supports it renders the wash. --><div style=\"position: absolute; top: 0; right: 0; width: 200px; height: 200px; opacity: 0.35; pointer-events: none;\"><svg viewBox=\"0 0 200 200\" width=\"200\" height=\"200\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\"><path d=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("M100,20 C160,20 180,80 180,120 C180,170 130,180 90,180 C40,180 20,140 20,100 C20,60 50,20 100,20 Z")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 124, Col: 117}
|
||||
}
|
||||
_, 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, 8, "\" fill=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(pastelEmailPrimary(emailCtx.Colors))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 124, Col: 162}
|
||||
}
|
||||
_, 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, 9, "\"></path></svg></div><table role=\"presentation\" class=\"pastel-email-frame\" width=\"560\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("max-width: 560px; background-color: %s; border-radius: 20px; border: 1px solid %s;", pastelEmailCard(emailCtx.Colors), pastelEmailBorder(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 127, Col: 289}
|
||||
}
|
||||
_, 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, 10, "\"><!-- Masthead --><tr><td align=\"center\" class=\"pastel-pad\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 36px 40px 8px; border-bottom: 1px solid %s;", pastelEmailBorder(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 130, Col: 156}
|
||||
}
|
||||
_, 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, 11, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if emailCtx.SiteSettings.LogoURL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, 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: 132, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" alt=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, 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: 132, Col: 88}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" style=\"max-height: 56px; 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, 15, "<h1 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("margin: 0; font-family: 'Caveat Brush', 'Sacramento', cursive; font-size: 36px; font-weight: 400; color: %s;", pastelEmailForeground(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 134, Col: 184}
|
||||
}
|
||||
_, 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, 16, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, 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: 135, Col: 42}
|
||||
}
|
||||
_, 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, 17, "</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td></tr><!-- Body --><tr><td class=\"pastel-pad\" 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("padding: 32px 40px 24px; color: %s; font-size: 16px; line-height: 1.75;", pastelEmailForeground(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 142, Col: 164}
|
||||
}
|
||||
_, 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, 19, "\">")
|
||||
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, 20, "</td></tr><!-- Footer --><tr><td class=\"pastel-pad\" align=\"center\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 24px 40px 36px; border-top: 1px solid %s;", pastelEmailBorder(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 148, Col: 154}
|
||||
}
|
||||
_, 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 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 12px; font-family: 'Caveat Brush', 'Sacramento', cursive; font-size: 18px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 149, Col: 168}
|
||||
}
|
||||
_, 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, 22, "\">Be gentle with yourself today.</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if emailCtx.SiteSettings.SiteURL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0 0 12px; font-size: 13px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 153, Col: 117}
|
||||
}
|
||||
_, 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, "\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 templ.SafeURL
|
||||
templ_7745c5c3_Var18, 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: 154, Col: 64}
|
||||
}
|
||||
_, 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, "\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none;", pastelEmailPrimary(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 154, Col: 160}
|
||||
}
|
||||
_, 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, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, 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: 155, Col: 42}
|
||||
}
|
||||
_, 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, 27, "</a></p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if emailCtx.UnsubscribeURL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 160, Col: 110}
|
||||
}
|
||||
_, 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, "\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 templ.SafeURL
|
||||
templ_7745c5c3_Var22, 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: 161, Col: 58}
|
||||
}
|
||||
_, 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, "\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", pastelEmailMutedFg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 161, Col: 159}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">Unsubscribe</a></p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td></tr></table></td></tr></table></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// pastelEmailPillStyle returns the inline CSS for the mint-tinted CTA pill
|
||||
// that templates may use inside the body slot. The wrapper exposes it as a
|
||||
// reusable token because the body is opaque pre-rendered HTML.
|
||||
func pastelEmailPillStyle(c templates.EmailColors) string {
|
||||
primary := pastelEmailPrimary(c)
|
||||
primaryFg := pastelEmailPrimaryFg(c)
|
||||
return fmt.Sprintf(
|
||||
"display: inline-block; padding: 12px 28px; border-radius: 9999px; background-color: %s; color: %s; font-family: 'Nunito', Helvetica, Arial, sans-serif; font-weight: 600; text-decoration: none;",
|
||||
primary,
|
||||
primaryFg,
|
||||
)
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
49
embed.go
Normal file
49
embed.go
Normal file
@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
var assetsFS embed.FS
|
||||
|
||||
//go:embed schemas/*
|
||||
var schemasFS embed.FS
|
||||
|
||||
//go:embed presets.json
|
||||
var presetsData []byte
|
||||
|
||||
//go:embed fonts.json
|
||||
var fontsData []byte
|
||||
|
||||
//go:embed plugin.mod
|
||||
var pluginModBytes []byte
|
||||
|
||||
// Assets returns the embedded assets filesystem.
|
||||
func Assets() fs.FS {
|
||||
sub, _ := fs.Sub(assetsFS, "assets")
|
||||
return sub
|
||||
}
|
||||
|
||||
// Schemas returns the embedded schemas filesystem.
|
||||
func Schemas() fs.FS {
|
||||
sub, _ := fs.Sub(schemasFS, "schemas")
|
||||
return sub
|
||||
}
|
||||
|
||||
// AssetsHandler returns an http.Handler that serves the embedded assets.
|
||||
func AssetsHandler() http.Handler {
|
||||
return http.FileServer(http.FS(Assets()))
|
||||
}
|
||||
|
||||
// ThemePresets returns the embedded theme presets JSON.
|
||||
func ThemePresets() []byte {
|
||||
return presetsData
|
||||
}
|
||||
|
||||
// BundledFonts returns the embedded fonts manifest JSON.
|
||||
func BundledFonts() []byte {
|
||||
return fontsData
|
||||
}
|
||||
53
feature_grid_soft.go
Normal file
53
feature_grid_soft.go
Normal file
@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// FeatureGridSoftMeta defines the three-up soft feature grid.
|
||||
var FeatureGridSoftMeta = blocks.BlockMeta{
|
||||
Key: "feature-grid-soft",
|
||||
Title: "Feature Trio",
|
||||
Description: "Three-up card grid for services, offerings, or core values.",
|
||||
Source: "pastel-dream",
|
||||
Category: blocks.CategoryLayout,
|
||||
}
|
||||
|
||||
// FeatureGridSoftBlock renders the feature grid.
|
||||
// Content shape: {intro, items: [{icon, title, body}]}
|
||||
func FeatureGridSoftBlock(ctx context.Context, content map[string]any) string {
|
||||
rawItems := getSlice(content, "items")
|
||||
items := make([]FeatureSoftItem, 0, len(rawItems))
|
||||
for _, it := range rawItems {
|
||||
items = append(items, FeatureSoftItem{
|
||||
Icon: getString(it, "icon"),
|
||||
Title: getString(it, "title"),
|
||||
Body: getString(it, "body"),
|
||||
})
|
||||
}
|
||||
|
||||
data := FeatureGridSoftData{
|
||||
Intro: getString(content, "intro"),
|
||||
Items: items,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = featureGridSoftComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// FeatureGridSoftData is the rendered view of the feature-grid-soft block.
|
||||
type FeatureGridSoftData struct {
|
||||
Intro string
|
||||
Items []FeatureSoftItem
|
||||
}
|
||||
|
||||
// FeatureSoftItem is a single card in the trio.
|
||||
type FeatureSoftItem struct {
|
||||
Icon string
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
40
feature_grid_soft.templ
Normal file
40
feature_grid_soft.templ
Normal file
@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
// featureGridSoftComponent renders the three-up grid. It reflows to 1 column
|
||||
// at 360px and 2-3 columns at 768px+, satisfying UAT §7.
|
||||
templ featureGridSoftComponent(data FeatureGridSoftData) {
|
||||
<section class="py-16 md:py-20" data-block="pastel-dream:feature-grid-soft">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
if data.Intro != "" {
|
||||
<div class="font-body text-center max-w-2xl mx-auto mb-12 text-lg" style="color: hsl(var(--foreground) / 0.78);">
|
||||
@templ.Raw(data.Intro)
|
||||
</div>
|
||||
}
|
||||
if len(data.Items) > 0 {
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
for _, item := range data.Items {
|
||||
<article class="pastel-card p-8 transition-transform" style="transition-timing-function: cubic-bezier(.22,1,.36,1); transition-duration: 240ms;">
|
||||
if item.Icon != "" {
|
||||
<div class="mb-4">
|
||||
<img src={ item.Icon } alt="" class="w-12 h-12 object-contain"/>
|
||||
</div>
|
||||
} else {
|
||||
<div class="mb-4 w-12 h-12 rounded-full" style="background-color: hsl(var(--accent) / 0.5);" aria-hidden="true"></div>
|
||||
}
|
||||
if item.Title != "" {
|
||||
<h3 class="font-display text-2xl mb-3" style="color: hsl(var(--foreground));">{ item.Title }</h3>
|
||||
}
|
||||
if item.Body != "" {
|
||||
<p class="font-body text-base" style="color: hsl(var(--foreground) / 0.78); line-height: 1.75;">{ item.Body }</p>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<div class="text-center font-body text-base" style="color: hsl(var(--muted-foreground));">
|
||||
<p>Add up to three cards to the trio.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
147
feature_grid_soft_templ.go
Normal file
147
feature_grid_soft_templ.go
Normal file
@ -0,0 +1,147 @@
|
||||
// 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"
|
||||
|
||||
// featureGridSoftComponent renders the three-up grid. It reflows to 1 column
|
||||
// at 360px and 2-3 columns at 768px+, satisfying UAT §7.
|
||||
func featureGridSoftComponent(data FeatureGridSoftData) 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 md:py-20\" data-block=\"pastel-dream:feature-grid-soft\"><div class=\"max-w-6xl mx-auto px-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Intro != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"font-body text-center max-w-2xl mx-auto mb-12 text-lg\" style=\"color: hsl(var(--foreground) / 0.78);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Intro).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
|
||||
}
|
||||
}
|
||||
if len(data.Items) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range data.Items {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<article class=\"pastel-card p-8 transition-transform\" style=\"transition-timing-function: cubic-bezier(.22,1,.36,1); transition-duration: 240ms;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.Icon != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"mb-4\"><img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `feature_grid_soft.templ`, Line: 19, Col: 29}
|
||||
}
|
||||
_, 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, 7, "\" alt=\"\" class=\"w-12 h-12 object-contain\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"mb-4 w-12 h-12 rounded-full\" style=\"background-color: hsl(var(--accent) / 0.5);\" aria-hidden=\"true\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if item.Title != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<h3 class=\"font-display text-2xl mb-3\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `feature_grid_soft.templ`, Line: 25, Col: 98}
|
||||
}
|
||||
_, 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, 10, "</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if item.Body != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"font-body text-base\" style=\"color: hsl(var(--foreground) / 0.78); line-height: 1.75;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Body)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `feature_grid_soft.templ`, Line: 28, Col: 115}
|
||||
}
|
||||
_, 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, 12, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</article>")
|
||||
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
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"text-center font-body text-base\" style=\"color: hsl(var(--muted-foreground));\"><p>Add up to three cards to the trio.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
1
fonts.json
Normal file
1
fonts.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
20
go.mod
Normal file
20
go.mod
Normal file
@ -0,0 +1,20 @@
|
||||
module git.dev.alexdunmow.com/block/themes/pastel-dream
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
git.dev.alexdunmow.com/block/core v0.11.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
|
||||
)
|
||||
42
go.sum
Normal file
42
go.sum
Normal file
@ -0,0 +1,42 @@
|
||||
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||
git.dev.alexdunmow.com/block/core v0.11.1 h1:5b3Ps9CLor2FGyxw/Qovt27AGZKR5Xi1JZGi/TfliTA=
|
||||
git.dev.alexdunmow.com/block/core v0.11.1/go.mod h1:ZwzEOxRDLDfrhQGqo6hLw01/C1z/aS4Dm9ljQMl0Bg4=
|
||||
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=
|
||||
42
heading_override.go
Normal file
42
heading_override.go
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// PastelHeadingBlock renders headings with pastel-dream styling.
|
||||
// Caveat Brush display at H1/H2; Nunito for H3+; soft brush-stroke underline.
|
||||
// Content shape: {text, level: 1-6, textClass}
|
||||
func PastelHeadingBlock(ctx context.Context, content map[string]any) string {
|
||||
text := getString(content, "text")
|
||||
textClass := getString(content, "textClass")
|
||||
level := parseHeadingLevel(content)
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = pastelHeadingComponent(level, text, textClass).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// parseHeadingLevel reads level from various JSON-compatible types and clamps
|
||||
// the result to 1..6, 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
|
||||
}
|
||||
42
heading_override.templ
Normal file
42
heading_override.templ
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
// pastelHeadingBaseClass returns the Tailwind size/weight class for a level.
|
||||
// H1 and H2 get the display face; H3+ uses the body face.
|
||||
func pastelHeadingBaseClass(level int) string {
|
||||
switch level {
|
||||
case 1:
|
||||
return "font-display text-5xl md:text-6xl leading-tight brush-underline"
|
||||
case 2:
|
||||
return "font-display text-4xl md:text-5xl leading-tight brush-underline"
|
||||
case 3:
|
||||
return "font-body text-3xl font-semibold tracking-tight"
|
||||
case 4:
|
||||
return "font-body text-2xl font-semibold"
|
||||
case 5:
|
||||
return "font-body text-xl font-medium"
|
||||
case 6:
|
||||
return "font-body text-lg font-medium"
|
||||
default:
|
||||
return "font-body text-3xl font-semibold tracking-tight"
|
||||
}
|
||||
}
|
||||
|
||||
// pastelHeadingComponent renders a heading with pastel-dream styling.
|
||||
templ pastelHeadingComponent(level int, text, textClass string) {
|
||||
switch level {
|
||||
case 1:
|
||||
<h1 class={ pastelHeadingBaseClass(1), textClass } style="color: hsl(var(--foreground));">{ text }</h1>
|
||||
case 2:
|
||||
<h2 class={ pastelHeadingBaseClass(2), textClass } style="color: hsl(var(--foreground));">{ text }</h2>
|
||||
case 3:
|
||||
<h3 class={ pastelHeadingBaseClass(3), textClass } style="color: hsl(var(--foreground));">{ text }</h3>
|
||||
case 4:
|
||||
<h4 class={ pastelHeadingBaseClass(4), textClass } style="color: hsl(var(--foreground));">{ text }</h4>
|
||||
case 5:
|
||||
<h5 class={ pastelHeadingBaseClass(5), textClass } style="color: hsl(var(--foreground));">{ text }</h5>
|
||||
case 6:
|
||||
<h6 class={ pastelHeadingBaseClass(6), textClass } style="color: hsl(var(--foreground));">{ text }</h6>
|
||||
default:
|
||||
<h2 class={ pastelHeadingBaseClass(2), textClass } style="color: hsl(var(--foreground));">{ text }</h2>
|
||||
}
|
||||
}
|
||||
312
heading_override_templ.go
Normal file
312
heading_override_templ.go
Normal file
@ -0,0 +1,312 @@
|
||||
// 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"
|
||||
|
||||
// pastelHeadingBaseClass returns the Tailwind size/weight class for a level.
|
||||
// H1 and H2 get the display face; H3+ uses the body face.
|
||||
func pastelHeadingBaseClass(level int) string {
|
||||
switch level {
|
||||
case 1:
|
||||
return "font-display text-5xl md:text-6xl leading-tight brush-underline"
|
||||
case 2:
|
||||
return "font-display text-4xl md:text-5xl leading-tight brush-underline"
|
||||
case 3:
|
||||
return "font-body text-3xl font-semibold tracking-tight"
|
||||
case 4:
|
||||
return "font-body text-2xl font-semibold"
|
||||
case 5:
|
||||
return "font-body text-xl font-medium"
|
||||
case 6:
|
||||
return "font-body text-lg font-medium"
|
||||
default:
|
||||
return "font-body text-3xl font-semibold tracking-tight"
|
||||
}
|
||||
}
|
||||
|
||||
// pastelHeadingComponent renders a heading with pastel-dream styling.
|
||||
func pastelHeadingComponent(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{pastelHeadingBaseClass(1), 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, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
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: 28, 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, 3, "</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case 2:
|
||||
var templ_7745c5c3_Var5 = []any{pastelHeadingBaseClass(2), 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, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
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: 30, Col: 99}
|
||||
}
|
||||
_, 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{pastelHeadingBaseClass(3), 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, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
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: 32, Col: 99}
|
||||
}
|
||||
_, 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{pastelHeadingBaseClass(4), 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, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
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: 34, Col: 99}
|
||||
}
|
||||
_, 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{pastelHeadingBaseClass(5), 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, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
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: 36, Col: 99}
|
||||
}
|
||||
_, 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{pastelHeadingBaseClass(6), 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, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
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: 38, Col: 99}
|
||||
}
|
||||
_, 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{pastelHeadingBaseClass(2), 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, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
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: 40, Col: 99}
|
||||
}
|
||||
_, 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
|
||||
82
helpers.go
Normal file
82
helpers.go
Normal file
@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
// getString extracts a string value from a content map.
|
||||
// Returns "" when the key is missing or holds a non-string value.
|
||||
func getString(content map[string]any, key string) string {
|
||||
if v, ok := content[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getStringOr extracts a string with a fallback for empty/missing values.
|
||||
func getStringOr(content map[string]any, key, fallback string) string {
|
||||
if v := getString(content, key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// getFloat extracts a float from content.
|
||||
func getFloat(content map[string]any, key string, defaultVal float64) float64 {
|
||||
if v, ok := content[key].(float64); ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := content[key].(int); ok {
|
||||
return float64(v)
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
// getBoolish reads "yes"/"no" select fields or actual booleans into a bool.
|
||||
func getBoolish(content map[string]any, key string, defaultVal bool) bool {
|
||||
if v, ok := content[key].(bool); ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := content[key].(string); ok {
|
||||
switch v {
|
||||
case "yes", "true", "1", "on":
|
||||
return true
|
||||
case "no", "false", "0", "off":
|
||||
return false
|
||||
}
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
// getSlice extracts a slice of maps from a content map (collections/arrays
|
||||
// of objects).
|
||||
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
|
||||
}
|
||||
|
||||
// getStringSlice extracts a slice of strings (e.g. social URLs).
|
||||
func getStringSlice(content map[string]any, key string) []string {
|
||||
if v, ok := content[key].([]any); ok {
|
||||
result := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok && s != "" {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// safeLink returns href if non-empty, otherwise "#".
|
||||
func safeLink(href string) string {
|
||||
if href == "" {
|
||||
return "#"
|
||||
}
|
||||
return href
|
||||
}
|
||||
12
plugin.mod
Normal file
12
plugin.mod
Normal file
@ -0,0 +1,12 @@
|
||||
[plugin]
|
||||
name = "pastel-dream"
|
||||
display_name = "Pastel Dream"
|
||||
scope = "@themes"
|
||||
version = "0.1.0"
|
||||
description = "Soft watercolor theme with blush, mint, and butter palettes for wellness, parenting, and spa brands."
|
||||
kind = "theme"
|
||||
categories = ["templates"]
|
||||
tags = ["pastel", "soft", "wellness", "parenting", "spa", "doula", "calm", "skincare", "mindfulness"]
|
||||
|
||||
[compatibility]
|
||||
block_core = ">=0.11.0 <0.12.0"
|
||||
89
presets.json
Normal file
89
presets.json
Normal file
@ -0,0 +1,89 @@
|
||||
[
|
||||
{
|
||||
"id": "blush-morning",
|
||||
"name": "Blush Morning",
|
||||
"description": "Default — warm peach paper with rose-blush primary and sage trim.",
|
||||
"theme": {
|
||||
"mode": "light",
|
||||
"lightColors": {
|
||||
"background": "25 60% 98%",
|
||||
"foreground": "340 20% 22%",
|
||||
"card": "0 0% 100%",
|
||||
"cardForeground": "340 20% 22%",
|
||||
"popover": "0 0% 100%",
|
||||
"popoverForeground": "340 20% 22%",
|
||||
"primary": "350 65% 72%",
|
||||
"primaryForeground": "340 40% 18%",
|
||||
"secondary": "150 35% 88%",
|
||||
"secondaryForeground": "155 35% 22%",
|
||||
"muted": "30 40% 95%",
|
||||
"mutedForeground": "340 12% 48%",
|
||||
"accent": "48 85% 80%",
|
||||
"accentForeground": "35 50% 22%",
|
||||
"destructive": "0 75% 65%",
|
||||
"destructiveForeground": "0 0% 100%",
|
||||
"border": "340 30% 90%",
|
||||
"input": "340 30% 92%",
|
||||
"ring": "350 65% 72%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mint-meadow",
|
||||
"name": "Mint Meadow",
|
||||
"description": "Sage-led variant for spa and doula brands; rose drops to accent.",
|
||||
"theme": {
|
||||
"mode": "light",
|
||||
"lightColors": {
|
||||
"background": "150 40% 97%",
|
||||
"foreground": "155 35% 18%",
|
||||
"card": "0 0% 100%",
|
||||
"cardForeground": "155 35% 18%",
|
||||
"popover": "0 0% 100%",
|
||||
"popoverForeground": "155 35% 18%",
|
||||
"primary": "155 40% 60%",
|
||||
"primaryForeground": "0 0% 100%",
|
||||
"secondary": "350 50% 92%",
|
||||
"secondaryForeground": "345 40% 28%",
|
||||
"muted": "150 25% 94%",
|
||||
"mutedForeground": "155 18% 42%",
|
||||
"accent": "48 80% 82%",
|
||||
"accentForeground": "35 50% 22%",
|
||||
"destructive": "5 70% 62%",
|
||||
"destructiveForeground": "0 0% 100%",
|
||||
"border": "150 25% 88%",
|
||||
"input": "150 25% 92%",
|
||||
"ring": "155 40% 60%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "twilight-petal",
|
||||
"name": "Twilight Petal",
|
||||
"description": "Dusky night-mode pastel — plum velvet with blush glow.",
|
||||
"theme": {
|
||||
"mode": "dark",
|
||||
"darkColors": {
|
||||
"background": "300 25% 12%",
|
||||
"foreground": "30 40% 94%",
|
||||
"card": "300 22% 16%",
|
||||
"cardForeground": "30 40% 94%",
|
||||
"popover": "300 22% 16%",
|
||||
"popoverForeground": "30 40% 94%",
|
||||
"primary": "345 70% 78%",
|
||||
"primaryForeground": "340 40% 18%",
|
||||
"secondary": "155 25% 32%",
|
||||
"secondaryForeground": "150 30% 92%",
|
||||
"muted": "300 18% 22%",
|
||||
"mutedForeground": "300 12% 70%",
|
||||
"accent": "48 75% 72%",
|
||||
"accentForeground": "35 50% 18%",
|
||||
"destructive": "0 70% 60%",
|
||||
"destructiveForeground": "0 0% 100%",
|
||||
"border": "300 20% 28%",
|
||||
"input": "300 20% 22%",
|
||||
"ring": "345 70% 78%"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
187
register.go
Normal file
187
register.go
Normal file
@ -0,0 +1,187 @@
|
||||
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. It registers the system template, page
|
||||
// templates, theme-owned blocks, built-in overrides, and the email wrapper.
|
||||
//
|
||||
// Order of operations matters: br.LoadSchemasFromFS MUST run before any
|
||||
// br.Register call so schema binding sees the blocks as they register.
|
||||
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
|
||||
// System template registration.
|
||||
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
|
||||
Key: "pastel-dream",
|
||||
Title: "Pastel Dream",
|
||||
Description: "Soft watercolor theme with blush, mint, and butter palettes for wellness, parenting, and spa brands.",
|
||||
})
|
||||
|
||||
// Page templates per spec §"Page templates".
|
||||
if err := tr.RegisterPageTemplate("pastel-dream", templates.PageTemplateMeta{
|
||||
Key: "default",
|
||||
Title: "Default",
|
||||
Description: "Soft layered hero, masthead nav, footer with newsletter.",
|
||||
Slots: []string{"header", "main", "footer"},
|
||||
}, wrap(RenderPastelDefault)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tr.RegisterPageTemplate("pastel-dream", templates.PageTemplateMeta{
|
||||
Key: "landing",
|
||||
Title: "Soft Landing",
|
||||
Description: "Watercolor hero, affirmation strip, CTA, footer.",
|
||||
Slots: []string{"hero", "main", "cta", "footer"},
|
||||
}, wrap(RenderPastelLanding)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tr.RegisterPageTemplate("pastel-dream", templates.PageTemplateMeta{
|
||||
Key: "article",
|
||||
Title: "Reading Room",
|
||||
Description: "Narrow, centered editorial column with side rail.",
|
||||
Slots: []string{"header", "main", "footer"},
|
||||
}, wrap(RenderPastelArticle)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tr.RegisterPageTemplate("pastel-dream", templates.PageTemplateMeta{
|
||||
Key: "full-width",
|
||||
Title: "Open Air",
|
||||
Description: "Edge-to-edge sections for galleries and seasonal looks.",
|
||||
Slots: []string{"header", "main", "footer"},
|
||||
}, wrap(RenderPastelFullWidth)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Schemas MUST load before blocks register so editor metadata binds.
|
||||
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Theme-owned blocks.
|
||||
br.Register(SoftNavbarMeta, SoftNavbarBlock)
|
||||
br.Register(WatercolorHeroMeta, WatercolorHeroBlock)
|
||||
br.Register(AffirmationMeta, AffirmationBlock)
|
||||
br.Register(TestimonialSoftMeta, TestimonialSoftBlock)
|
||||
br.Register(FeatureGridSoftMeta, FeatureGridSoftBlock)
|
||||
br.Register(CozyFooterMeta, CozyFooterBlock)
|
||||
|
||||
// Overrides for built-ins, only when this theme is active.
|
||||
br.RegisterTemplateOverride("pastel-dream", "heading", PastelHeadingBlock)
|
||||
br.RegisterTemplateOverride("pastel-dream", "text", PastelTextBlock)
|
||||
br.RegisterTemplateOverride("pastel-dream", "button", PastelButtonBlock)
|
||||
br.RegisterTemplateOverride("pastel-dream", "card", PastelCardBlock)
|
||||
|
||||
// Email wrapper.
|
||||
tr.RegisterEmailWrapper("pastel-dream", PastelEmailWrapper)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultMasterPages returns the master pages seeded on first plugin load.
|
||||
// Two definitions per spec §"Master pages":
|
||||
// - pastel-dream:default-master — used by default and article page templates
|
||||
// - pastel-dream:landing-master — used by the landing page template
|
||||
func DefaultMasterPages() []plugin.MasterPageDefinition {
|
||||
return []plugin.MasterPageDefinition{
|
||||
{
|
||||
Key: "pastel-dream:default-master",
|
||||
Title: "Pastel Dream Default Master",
|
||||
PageTemplates: []string{"default", "article"},
|
||||
Blocks: []plugin.MasterPageBlock{
|
||||
{
|
||||
BlockKey: "pastel-dream:soft-navbar",
|
||||
Title: "Soft Nav",
|
||||
Content: map[string]any{
|
||||
"menuName": "main",
|
||||
"logo": "Pastel Dream",
|
||||
},
|
||||
Slot: "header",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "slot",
|
||||
Title: "Main Slot",
|
||||
Content: map[string]any{
|
||||
"slotName": "main",
|
||||
"placeholder": "Page content",
|
||||
},
|
||||
Slot: "main",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "pastel-dream:cozy-footer",
|
||||
Title: "Cozy Footer",
|
||||
Content: map[string]any{
|
||||
"showSignup": "yes",
|
||||
"affirmation": "Be gentle with yourself today.",
|
||||
},
|
||||
Slot: "footer",
|
||||
SortOrder: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "pastel-dream:landing-master",
|
||||
Title: "Pastel Dream Landing Master",
|
||||
PageTemplates: []string{"landing"},
|
||||
Blocks: []plugin.MasterPageBlock{
|
||||
{
|
||||
BlockKey: "pastel-dream:watercolor-hero",
|
||||
Title: "Watercolor Hero",
|
||||
Content: map[string]any{
|
||||
"eyebrow": "new",
|
||||
"headline": "Soft starts",
|
||||
"body": "A gentle place to begin.",
|
||||
"ctaText": "Begin",
|
||||
},
|
||||
Slot: "hero",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "slot",
|
||||
Title: "Main Slot",
|
||||
Content: map[string]any{
|
||||
"slotName": "main",
|
||||
},
|
||||
Slot: "main",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "pastel-dream:affirmation",
|
||||
Title: "Affirmation Strip",
|
||||
Content: map[string]any{
|
||||
"quote": "You are doing enough.",
|
||||
"author": "Pastel Dream",
|
||||
},
|
||||
Slot: "cta",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "pastel-dream:cozy-footer",
|
||||
Title: "Cozy Footer",
|
||||
Content: map[string]any{
|
||||
"showSignup": "yes",
|
||||
},
|
||||
Slot: "footer",
|
||||
SortOrder: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
38
registration.go
Normal file
38
registration.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
// Registration is the compile-time plugin registration for the Pastel Dream theme.
|
||||
var Registration = plugin.PluginRegistration{
|
||||
Name: "pastel-dream",
|
||||
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() },
|
||||
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
|
||||
}
|
||||
|
||||
// ThemeCSSManifest returns the Pastel Dream CSS to be appended to the host Tailwind input.
|
||||
// This includes keyframes (pastel-shimmer, breathe), the --radius-soft variable,
|
||||
// watercolor utility classes, font-family fallback stacks, and reduced-motion guards.
|
||||
func ThemeCSSManifest() *plugin.CSSManifest {
|
||||
css, err := assetsFS.ReadFile("assets/css/pastel-dream.css")
|
||||
if err != nil {
|
||||
return &plugin.CSSManifest{}
|
||||
}
|
||||
return &plugin.CSSManifest{
|
||||
InputCSSAppend: string(css),
|
||||
}
|
||||
}
|
||||
29
schemas/affirmation.schema.json
Normal file
29
schemas/affirmation.schema.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Affirmation Strip",
|
||||
"description": "Slow-shimmer band carrying a short affirmation in display type.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quote": {
|
||||
"type": "string",
|
||||
"title": "Affirmation",
|
||||
"description": "Short calming line (one or two sentences).",
|
||||
"default": "You are doing enough.",
|
||||
"x-editor": "textarea"
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"title": "Attribution",
|
||||
"description": "Optional source for the line.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"palette": {
|
||||
"type": "string",
|
||||
"title": "Palette",
|
||||
"description": "Which watercolor wash to use behind the strip.",
|
||||
"default": "blush",
|
||||
"enum": ["blush", "mint", "butter", "sky"],
|
||||
"x-editor": "select"
|
||||
}
|
||||
}
|
||||
}
|
||||
39
schemas/cozy-footer.schema.json
Normal file
39
schemas/cozy-footer.schema.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Cozy Footer",
|
||||
"description": "Newsletter signup, small affirmation line, menu, and social links.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"showSignup": {
|
||||
"type": "string",
|
||||
"title": "Newsletter signup",
|
||||
"description": "Toggle the inline email signup form.",
|
||||
"default": "yes",
|
||||
"enum": ["yes", "no"],
|
||||
"x-editor": "select"
|
||||
},
|
||||
"affirmation": {
|
||||
"type": "string",
|
||||
"title": "Closing line",
|
||||
"description": "Short closing affirmation displayed beneath the signup.",
|
||||
"default": "Be gentle with yourself today.",
|
||||
"x-editor": "textarea"
|
||||
},
|
||||
"menuName": {
|
||||
"type": "string",
|
||||
"title": "Menu",
|
||||
"description": "Name of the menu rendered in the footer column.",
|
||||
"x-editor": "menu-select"
|
||||
},
|
||||
"social": {
|
||||
"type": "array",
|
||||
"title": "Social links",
|
||||
"description": "Outbound social links shown as small pills.",
|
||||
"x-editor": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"x-editor": "link"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
schemas/feature-grid-soft.schema.json
Normal file
42
schemas/feature-grid-soft.schema.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Feature Trio",
|
||||
"description": "Three-up card grid for services, offerings, or values.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"intro": {
|
||||
"type": "string",
|
||||
"title": "Intro",
|
||||
"description": "Optional introduction above the cards.",
|
||||
"x-editor": "richtext"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"title": "Cards",
|
||||
"description": "List of feature cards. Three is the recommended count.",
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"title": "Icon",
|
||||
"description": "Decorative icon or small illustration.",
|
||||
"x-editor": "media"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Title",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"title": "Body",
|
||||
"x-editor": "textarea"
|
||||
}
|
||||
},
|
||||
"required": ["title"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
schemas/soft-navbar.schema.json
Normal file
34
schemas/soft-navbar.schema.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Soft Navbar",
|
||||
"description": "Rounded pill nav with a watercolor blob behind the logo wordmark.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"title": "Logo Wordmark",
|
||||
"description": "Plain-text wordmark displayed at the top-left.",
|
||||
"default": "Pastel Dream",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"menuName": {
|
||||
"type": "string",
|
||||
"title": "Menu",
|
||||
"description": "Name of the menu to render in the pill.",
|
||||
"default": "main",
|
||||
"x-editor": "menu-select"
|
||||
},
|
||||
"ctaText": {
|
||||
"type": "string",
|
||||
"title": "CTA Label",
|
||||
"description": "Optional pill button to the right of the menu.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"ctaHref": {
|
||||
"type": "string",
|
||||
"title": "CTA Link",
|
||||
"description": "Target URL or internal page link for the CTA pill.",
|
||||
"x-editor": "link"
|
||||
}
|
||||
}
|
||||
}
|
||||
40
schemas/testimonial-soft.schema.json
Normal file
40
schemas/testimonial-soft.schema.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Soft Testimonial",
|
||||
"description": "Deckle-edge card carrying a quote, attribution, optional avatar, and rating.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quote": {
|
||||
"type": "string",
|
||||
"title": "Quote",
|
||||
"description": "The testimonial body. Rich text.",
|
||||
"x-editor": "richtext"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name",
|
||||
"description": "Person being quoted.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"title": "Role",
|
||||
"description": "Their title, company, or context.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"avatar": {
|
||||
"type": "string",
|
||||
"title": "Avatar",
|
||||
"description": "Optional portrait or illustration.",
|
||||
"x-editor": "media"
|
||||
},
|
||||
"rating": {
|
||||
"type": "number",
|
||||
"title": "Rating",
|
||||
"description": "Star rating, 0 to 5.",
|
||||
"minimum": 0,
|
||||
"maximum": 5,
|
||||
"x-editor": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
43
schemas/watercolor-hero.schema.json
Normal file
43
schemas/watercolor-hero.schema.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Watercolor Hero",
|
||||
"description": "Hero with two-tone SVG blobs layered behind a soft headline and CTA.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eyebrow": {
|
||||
"type": "string",
|
||||
"title": "Eyebrow",
|
||||
"description": "Small label above the headline (e.g. 'new', 'autumn collection').",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"headline": {
|
||||
"type": "string",
|
||||
"title": "Headline",
|
||||
"description": "Main hero headline. Caveat Brush display.",
|
||||
"default": "Soft starts",
|
||||
"x-editor": "richtext"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"title": "Body",
|
||||
"description": "Supporting paragraph below the headline.",
|
||||
"x-editor": "richtext"
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"title": "Feature Image",
|
||||
"description": "Optional hero image; renders to the right of the headline on desktop.",
|
||||
"x-editor": "media"
|
||||
},
|
||||
"ctaText": {
|
||||
"type": "string",
|
||||
"title": "CTA Label",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"ctaHref": {
|
||||
"type": "string",
|
||||
"title": "CTA Link",
|
||||
"x-editor": "link"
|
||||
}
|
||||
}
|
||||
}
|
||||
40
soft_navbar.go
Normal file
40
soft_navbar.go
Normal file
@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// SoftNavbarMeta defines metadata for the rounded pill navbar.
|
||||
var SoftNavbarMeta = blocks.BlockMeta{
|
||||
Key: "soft-navbar",
|
||||
Title: "Soft Navbar",
|
||||
Description: "Rounded pill navigation with a watercolor blob behind the logo.",
|
||||
Source: "pastel-dream",
|
||||
Category: blocks.CategoryNavigation,
|
||||
}
|
||||
|
||||
// SoftNavbarBlock renders the navbar.
|
||||
// Content shape: {logo: string, menuName: string, ctaText: string, ctaHref: string}
|
||||
func SoftNavbarBlock(ctx context.Context, content map[string]any) string {
|
||||
data := SoftNavbarData{
|
||||
Logo: getStringOr(content, "logo", "Pastel Dream"),
|
||||
MenuName: getString(content, "menuName"),
|
||||
CTAText: getString(content, "ctaText"),
|
||||
CTAHref: safeLink(getString(content, "ctaHref")),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = softNavbarComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// SoftNavbarData is the rendered view of the navbar block.
|
||||
type SoftNavbarData struct {
|
||||
Logo string
|
||||
MenuName string
|
||||
CTAText string
|
||||
CTAHref string
|
||||
}
|
||||
31
soft_navbar.templ
Normal file
31
soft_navbar.templ
Normal file
@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
// softNavbarComponent renders a rounded pill navigation with watercolor wash
|
||||
// behind the logo wordmark.
|
||||
templ softNavbarComponent(data SoftNavbarData) {
|
||||
<nav class="w-full px-4 py-4" data-block="pastel-dream:soft-navbar" data-menu={ data.MenuName }>
|
||||
<div class="max-w-6xl mx-auto flex items-center justify-between gap-4 px-2 py-2 rounded-full bg-watercolor-blush" style="border-radius: 9999px;">
|
||||
<a href="/" class="font-display text-2xl tracking-tight px-4 py-2 rounded-full" style="color: hsl(var(--primary-foreground)); background-color: hsl(var(--primary) / 0.15);">
|
||||
{ data.Logo }
|
||||
</a>
|
||||
<div class="hidden md:flex items-center gap-6 font-body text-sm" data-pastel-menu={ data.MenuName }>
|
||||
<a href="/" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">Home</a>
|
||||
<a href="/about" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">About</a>
|
||||
<a href="/journal" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">Journal</a>
|
||||
<a href="/contact" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">Contact</a>
|
||||
</div>
|
||||
if data.CTAText != "" {
|
||||
<a href={ templ.SafeURL(data.CTAHref) } class="pastel-pill text-sm">
|
||||
{ data.CTAText }
|
||||
</a>
|
||||
}
|
||||
<button class="md:hidden p-2 rounded-full" aria-label="Open menu" style="color: hsl(var(--foreground));">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
117
soft_navbar_templ.go
Normal file
117
soft_navbar_templ.go
Normal file
@ -0,0 +1,117 @@
|
||||
// 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"
|
||||
|
||||
// softNavbarComponent renders a rounded pill navigation with watercolor wash
|
||||
// behind the logo wordmark.
|
||||
func softNavbarComponent(data SoftNavbarData) 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, "<nav class=\"w-full px-4 py-4\" data-block=\"pastel-dream:soft-navbar\" data-menu=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.MenuName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `soft_navbar.templ`, Line: 6, Col: 94}
|
||||
}
|
||||
_, 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, 2, "\"><div class=\"max-w-6xl mx-auto flex items-center justify-between gap-4 px-2 py-2 rounded-full bg-watercolor-blush\" style=\"border-radius: 9999px;\"><a href=\"/\" class=\"font-display text-2xl tracking-tight px-4 py-2 rounded-full\" style=\"color: hsl(var(--primary-foreground)); background-color: hsl(var(--primary) / 0.15);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Logo)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `soft_navbar.templ`, Line: 9, Col: 15}
|
||||
}
|
||||
_, 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, "</a><div class=\"hidden md:flex items-center gap-6 font-body text-sm\" data-pastel-menu=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.MenuName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `soft_navbar.templ`, Line: 11, Col: 100}
|
||||
}
|
||||
_, 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, 4, "\"><a href=\"/\" style=\"color: hsl(var(--foreground));\" class=\"hover:opacity-70 transition-opacity\">Home</a> <a href=\"/about\" style=\"color: hsl(var(--foreground));\" class=\"hover:opacity-70 transition-opacity\">About</a> <a href=\"/journal\" style=\"color: hsl(var(--foreground));\" class=\"hover:opacity-70 transition-opacity\">Journal</a> <a href=\"/contact\" style=\"color: hsl(var(--foreground));\" class=\"hover:opacity-70 transition-opacity\">Contact</a></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.CTAText != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<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.CTAHref))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `soft_navbar.templ`, Line: 18, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"pastel-pill text-sm\">")
|
||||
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: `soft_navbar.templ`, Line: 19, 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, 7, "</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<button class=\"md:hidden p-2 rounded-full\" aria-label=\"Open menu\" style=\"color: hsl(var(--foreground));\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\"></line> <line x1=\"3\" y1=\"12\" x2=\"21\" y2=\"12\"></line> <line x1=\"3\" y1=\"18\" x2=\"21\" y2=\"18\"></line></svg></button></div></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
252
template.templ
Normal file
252
template.templ
Normal file
@ -0,0 +1,252 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/templates/bn"
|
||||
)
|
||||
|
||||
// PastelPageData carries the per-render page context.
|
||||
type PastelPageData 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
|
||||
}
|
||||
|
||||
// parsePastelPageData converts the wire-format doc map into a PastelPageData.
|
||||
// It is lenient: missing fields fall back to sensible defaults so empty pages
|
||||
// render without panicking.
|
||||
func parsePastelPageData(doc map[string]any) PastelPageData {
|
||||
title := "Untitled"
|
||||
if t, ok := doc["title"].(string); ok && t != "" {
|
||||
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
|
||||
}
|
||||
|
||||
return PastelPageData{
|
||||
Title: title,
|
||||
Slots: slots,
|
||||
ThemeMode: themeMode,
|
||||
ThemeCSS: themeCSS,
|
||||
SiteSettings: bn.ParseSiteSettings(doc),
|
||||
PageMeta: bn.ParsePageMeta(doc),
|
||||
StructuredData: structuredData,
|
||||
CSSHash: cssHash,
|
||||
PageviewNonce: pageviewNonce,
|
||||
EngagementConfig: bn.ParseEngagementConfig(doc),
|
||||
}
|
||||
}
|
||||
|
||||
// PastelDefault is the standard pastel-dream page template — masthead, main
|
||||
// column, and footer. Used by both `default` and (with a slightly wider main)
|
||||
// the `full-width` template.
|
||||
templ PastelDefault(data PastelPageData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
})
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
@bn.AdminBypassBanner(data.SiteSettings)
|
||||
<header class="w-full">
|
||||
@templ.Raw(data.Slots["header"])
|
||||
</header>
|
||||
<main class="flex-grow w-full max-w-4xl mx-auto px-4 py-12">
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
@templ.Raw(main)
|
||||
} else {
|
||||
<div class="py-20 text-center font-body" style="color: hsl(var(--muted-foreground));">
|
||||
<p>This page is still resting. Add a block to begin.</p>
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
@templ.Raw(data.Slots["footer"])
|
||||
</footer>
|
||||
@bn.BodyEnd(data.SiteSettings)
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
// PastelLanding is the marketing landing layout — full-width hero, body,
|
||||
// affirmation CTA, footer.
|
||||
templ PastelLanding(data PastelPageData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
})
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
@bn.AdminBypassBanner(data.SiteSettings)
|
||||
<section class="w-full">
|
||||
@templ.Raw(data.Slots["hero"])
|
||||
</section>
|
||||
<main class="flex-grow w-full">
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
<div class="max-w-6xl mx-auto px-4 py-20">
|
||||
@templ.Raw(main)
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<section class="w-full">
|
||||
@templ.Raw(data.Slots["cta"])
|
||||
</section>
|
||||
<footer class="w-full mt-auto">
|
||||
@templ.Raw(data.Slots["footer"])
|
||||
</footer>
|
||||
@bn.BodyEnd(data.SiteSettings)
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
// PastelArticle is the narrow reading-room layout for editorial posts.
|
||||
templ PastelArticle(data PastelPageData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
})
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
@bn.AdminBypassBanner(data.SiteSettings)
|
||||
<header class="w-full">
|
||||
@templ.Raw(data.Slots["header"])
|
||||
</header>
|
||||
<main class="flex-grow w-full max-w-2xl mx-auto px-4 py-16">
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
<article class="font-body prose max-w-none" style="line-height: 1.75; color: hsl(var(--foreground));">
|
||||
@templ.Raw(main)
|
||||
</article>
|
||||
} else {
|
||||
<div class="py-20 text-center" style="color: hsl(var(--muted-foreground));">
|
||||
<p>This page is still resting. Add a block to begin.</p>
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
@templ.Raw(data.Slots["footer"])
|
||||
</footer>
|
||||
@bn.BodyEnd(data.SiteSettings)
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
// PastelFullWidth is the edge-to-edge layout for galleries and seasonal looks.
|
||||
templ PastelFullWidth(data PastelPageData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
})
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
@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" style="color: hsl(var(--muted-foreground));">
|
||||
<p>This page is still resting. Add a block to begin.</p>
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
@templ.Raw(data.Slots["footer"])
|
||||
</footer>
|
||||
@bn.BodyEnd(data.SiteSettings)
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
// RenderPastelDefault is the registered render entry for the default template.
|
||||
func RenderPastelDefault(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelDefault(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelLanding is the registered render entry for the landing template.
|
||||
func RenderPastelLanding(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelLanding(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelArticle is the registered render entry for the article template.
|
||||
func RenderPastelArticle(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelArticle(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelFullWidth is the registered render entry for the full-width template.
|
||||
func RenderPastelFullWidth(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelFullWidth(parsePastelPageData(doc))
|
||||
}
|
||||
510
template_templ.go
Normal file
510
template_templ.go
Normal file
@ -0,0 +1,510 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// PastelPageData carries the per-render page context.
|
||||
type PastelPageData 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
|
||||
}
|
||||
|
||||
// parsePastelPageData converts the wire-format doc map into a PastelPageData.
|
||||
// It is lenient: missing fields fall back to sensible defaults so empty pages
|
||||
// render without panicking.
|
||||
func parsePastelPageData(doc map[string]any) PastelPageData {
|
||||
title := "Untitled"
|
||||
if t, ok := doc["title"].(string); ok && t != "" {
|
||||
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
|
||||
}
|
||||
|
||||
return PastelPageData{
|
||||
Title: title,
|
||||
Slots: slots,
|
||||
ThemeMode: themeMode,
|
||||
ThemeCSS: themeCSS,
|
||||
SiteSettings: bn.ParseSiteSettings(doc),
|
||||
PageMeta: bn.ParsePageMeta(doc),
|
||||
StructuredData: structuredData,
|
||||
CSSHash: cssHash,
|
||||
PageviewNonce: pageviewNonce,
|
||||
EngagementConfig: bn.ParseEngagementConfig(doc),
|
||||
}
|
||||
}
|
||||
|
||||
// PastelDefault is the standard pastel-dream page template — masthead, main
|
||||
// column, and footer. Used by both `default` and (with a slightly wider main)
|
||||
// the `full-width` template.
|
||||
func PastelDefault(data PastelPageData) 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\">")
|
||||
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/pastel-dream/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=\"font-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
|
||||
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 w-full max-w-4xl mx-auto px-4 py-12\">")
|
||||
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 font-body\" style=\"color: hsl(var(--muted-foreground));\"><p>This page is still resting. Add a block to begin.</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
|
||||
})
|
||||
}
|
||||
|
||||
// PastelLanding is the marketing landing layout — full-width hero, body,
|
||||
// affirmation CTA, footer.
|
||||
func PastelLanding(data PastelPageData) 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\">")
|
||||
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/pastel-dream/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=\"font-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
|
||||
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 w-full\">")
|
||||
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-20\">")
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// PastelArticle is the narrow reading-room layout for editorial posts.
|
||||
func PastelArticle(data PastelPageData) 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\">")
|
||||
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/pastel-dream/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=\"font-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
|
||||
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 max-w-2xl mx-auto px-4 py-16\">")
|
||||
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, 23, "<article class=\"font-body prose max-w-none\" style=\"line-height: 1.75; color: hsl(var(--foreground));\">")
|
||||
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, 24, "</article>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"py-20 text-center\" style=\"color: hsl(var(--muted-foreground));\"><p>This page is still resting. Add a block to begin.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</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, 27, "</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, 28, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// PastelFullWidth is the edge-to-edge layout for galleries and seasonal looks.
|
||||
func PastelFullWidth(data PastelPageData) 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, 29, "<!doctype html><html lang=\"en\">")
|
||||
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/pastel-dream/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, 30, "<body class=\"font-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
|
||||
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, 31, "<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, 32, "</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, 33, "<div class=\"max-w-4xl mx-auto py-20 px-4 text-center\" style=\"color: hsl(var(--muted-foreground));\"><p>This page is still resting. Add a block to begin.</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
|
||||
})
|
||||
}
|
||||
|
||||
// RenderPastelDefault is the registered render entry for the default template.
|
||||
func RenderPastelDefault(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelDefault(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelLanding is the registered render entry for the landing template.
|
||||
func RenderPastelLanding(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelLanding(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelArticle is the registered render entry for the article template.
|
||||
func RenderPastelArticle(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelArticle(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelFullWidth is the registered render entry for the full-width template.
|
||||
func RenderPastelFullWidth(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelFullWidth(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
50
testimonial_soft.go
Normal file
50
testimonial_soft.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// TestimonialSoftMeta defines the soft testimonial card.
|
||||
var TestimonialSoftMeta = blocks.BlockMeta{
|
||||
Key: "testimonial-soft",
|
||||
Title: "Soft Testimonial",
|
||||
Description: "Quote card with a deckle paper edge, avatar, and rating.",
|
||||
Source: "pastel-dream",
|
||||
Category: blocks.CategoryTheme,
|
||||
}
|
||||
|
||||
// TestimonialSoftBlock renders the testimonial card.
|
||||
// Content shape: {quote, name, role, avatar, rating}
|
||||
func TestimonialSoftBlock(ctx context.Context, content map[string]any) string {
|
||||
rating := getFloat(content, "rating", 0)
|
||||
if rating < 0 {
|
||||
rating = 0
|
||||
}
|
||||
if rating > 5 {
|
||||
rating = 5
|
||||
}
|
||||
|
||||
data := TestimonialSoftData{
|
||||
Quote: getString(content, "quote"),
|
||||
Name: getString(content, "name"),
|
||||
Role: getString(content, "role"),
|
||||
Avatar: getString(content, "avatar"),
|
||||
Rating: int(rating),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = testimonialSoftComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// TestimonialSoftData is the rendered view of the testimonial-soft block.
|
||||
type TestimonialSoftData struct {
|
||||
Quote string
|
||||
Name string
|
||||
Role string
|
||||
Avatar string
|
||||
Rating int
|
||||
}
|
||||
65
testimonial_soft.templ
Normal file
65
testimonial_soft.templ
Normal file
@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
// initials returns up to two uppercase initials for a name. Used as an avatar
|
||||
// fallback so the card renders cleanly when no portrait is provided.
|
||||
func initials(name string) string {
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
out := []rune{}
|
||||
prevSpace := true
|
||||
for _, r := range name {
|
||||
if r == ' ' {
|
||||
prevSpace = true
|
||||
continue
|
||||
}
|
||||
if prevSpace {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
r -= 32
|
||||
}
|
||||
out = append(out, r)
|
||||
prevSpace = false
|
||||
if len(out) >= 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// testimonialSoftComponent renders the deckle-edged testimonial card.
|
||||
templ testimonialSoftComponent(data TestimonialSoftData) {
|
||||
<figure class="pastel-card deckle-edge p-8 max-w-xl mx-auto" data-block="pastel-dream:testimonial-soft">
|
||||
if data.Rating > 0 {
|
||||
<div class="flex gap-1 mb-4" aria-label="Rating">
|
||||
for i := 0; i < data.Rating; i++ {
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<blockquote class="font-body text-lg leading-relaxed" style="color: hsl(var(--card-foreground));">
|
||||
@templ.Raw(data.Quote)
|
||||
</blockquote>
|
||||
if data.Name != "" || data.Role != "" {
|
||||
<figcaption class="flex items-center gap-4 mt-6 pt-6 border-t" style="border-color: hsl(var(--border));">
|
||||
if data.Avatar != "" {
|
||||
<img src={ data.Avatar } alt={ data.Name } class="w-12 h-12 rounded-full object-cover"/>
|
||||
} else if data.Name != "" {
|
||||
<span class="w-12 h-12 rounded-full flex items-center justify-center font-display text-lg" style="background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground));">
|
||||
{ initials(data.Name) }
|
||||
</span>
|
||||
}
|
||||
<div>
|
||||
if data.Name != "" {
|
||||
<p class="font-display text-xl leading-tight" style="color: hsl(var(--foreground));">{ data.Name }</p>
|
||||
}
|
||||
if data.Role != "" {
|
||||
<p class="font-body text-sm" style="color: hsl(var(--muted-foreground));">{ data.Role }</p>
|
||||
}
|
||||
</div>
|
||||
</figcaption>
|
||||
}
|
||||
</figure>
|
||||
}
|
||||
202
testimonial_soft_templ.go
Normal file
202
testimonial_soft_templ.go
Normal file
@ -0,0 +1,202 @@
|
||||
// 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"
|
||||
|
||||
// initials returns up to two uppercase initials for a name. Used as an avatar
|
||||
// fallback so the card renders cleanly when no portrait is provided.
|
||||
func initials(name string) string {
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
out := []rune{}
|
||||
prevSpace := true
|
||||
for _, r := range name {
|
||||
if r == ' ' {
|
||||
prevSpace = true
|
||||
continue
|
||||
}
|
||||
if prevSpace {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
r -= 32
|
||||
}
|
||||
out = append(out, r)
|
||||
prevSpace = false
|
||||
if len(out) >= 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// testimonialSoftComponent renders the deckle-edged testimonial card.
|
||||
func testimonialSoftComponent(data TestimonialSoftData) 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, "<figure class=\"pastel-card deckle-edge p-8 max-w-xl mx-auto\" data-block=\"pastel-dream:testimonial-soft\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Rating > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex gap-1 mb-4\" aria-label=\"Rating\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i := 0; i < data.Rating; i++ {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"color: hsl(var(--accent));\"><polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"></polygon></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<blockquote class=\"font-body text-lg leading-relaxed\" style=\"color: hsl(var(--card-foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Quote).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</blockquote>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Name != "" || data.Role != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<figcaption class=\"flex items-center gap-4 mt-6 pt-6 border-t\" style=\"border-color: hsl(var(--border));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Avatar != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<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.Avatar)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `testimonial_soft.templ`, Line: 48, Col: 27}
|
||||
}
|
||||
_, 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, 9, "\" alt=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `testimonial_soft.templ`, Line: 48, Col: 45}
|
||||
}
|
||||
_, 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, 10, "\" class=\"w-12 h-12 rounded-full object-cover\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if data.Name != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"w-12 h-12 rounded-full flex items-center justify-center font-display text-lg\" style=\"background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(initials(data.Name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `testimonial_soft.templ`, Line: 51, Col: 27}
|
||||
}
|
||||
_, 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, 12, "</span>")
|
||||
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.Name != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<p class=\"font-display text-xl leading-tight\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `testimonial_soft.templ`, Line: 56, Col: 102}
|
||||
}
|
||||
_, 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, 15, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.Role != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<p class=\"font-body text-sm\" style=\"color: hsl(var(--muted-foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.Role)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `testimonial_soft.templ`, Line: 59, Col: 91}
|
||||
}
|
||||
_, 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, 17, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></figcaption>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</figure>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
17
text_override.go
Normal file
17
text_override.go
Normal file
@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
)
|
||||
|
||||
// PastelTextBlock renders text with pastel-dream styling: Nunito body with
|
||||
// generous line-height (1.75) and wider tracking on small caps.
|
||||
func PastelTextBlock(ctx context.Context, content map[string]any) string {
|
||||
text := getString(content, "text")
|
||||
class := getString(content, "class")
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = pastelTextComponent(text, class).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
8
text_override.templ
Normal file
8
text_override.templ
Normal file
@ -0,0 +1,8 @@
|
||||
package main
|
||||
|
||||
// pastelTextComponent renders body text with Nunito at line-height 1.75.
|
||||
templ pastelTextComponent(text, class string) {
|
||||
<div class={ "font-body prose max-w-none", class } style="color: hsl(var(--foreground)); line-height: 1.75;">
|
||||
@templ.Raw(text)
|
||||
</div>
|
||||
}
|
||||
67
text_override_templ.go
Normal file
67
text_override_templ.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// pastelTextComponent renders body text with Nunito at line-height 1.75.
|
||||
func pastelTextComponent(text, class string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var2 = []any{"font-body prose max-w-none", class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `text_override.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" style=\"color: hsl(var(--foreground)); line-height: 1.75;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(text).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
44
watercolor_hero.go
Normal file
44
watercolor_hero.go
Normal file
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// WatercolorHeroMeta defines metadata for the watercolor hero.
|
||||
var WatercolorHeroMeta = blocks.BlockMeta{
|
||||
Key: "watercolor-hero",
|
||||
Title: "Watercolor Hero",
|
||||
Description: "Hero with two-tone watercolor blobs layered behind soft display type.",
|
||||
Source: "pastel-dream",
|
||||
Category: blocks.CategoryTheme,
|
||||
}
|
||||
|
||||
// WatercolorHeroBlock renders the hero section.
|
||||
// Content shape: {eyebrow, headline, body, image, ctaText, ctaHref}
|
||||
func WatercolorHeroBlock(ctx context.Context, content map[string]any) string {
|
||||
data := WatercolorHeroData{
|
||||
Eyebrow: getString(content, "eyebrow"),
|
||||
Headline: getStringOr(content, "headline", "Soft starts"),
|
||||
Body: getString(content, "body"),
|
||||
Image: getString(content, "image"),
|
||||
CTAText: getString(content, "ctaText"),
|
||||
CTAHref: safeLink(getString(content, "ctaHref")),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = watercolorHeroComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// WatercolorHeroData is the rendered view of the watercolor-hero block.
|
||||
type WatercolorHeroData struct {
|
||||
Eyebrow string
|
||||
Headline string
|
||||
Body string
|
||||
Image string
|
||||
CTAText string
|
||||
CTAHref string
|
||||
}
|
||||
54
watercolor_hero.templ
Normal file
54
watercolor_hero.templ
Normal file
@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
// watercolorHeroComponent renders the watercolor hero with inline SVG blobs.
|
||||
// Path fill colors come from hsl(var(--primary)) / hsl(var(--accent)) so they
|
||||
// follow whatever preset is active. A scrim under the headline keeps text
|
||||
// readable when an image is provided.
|
||||
templ watercolorHeroComponent(data WatercolorHeroData) {
|
||||
<section class="relative overflow-hidden py-20 md:py-28 bg-watercolor-blush" data-block="pastel-dream:watercolor-hero">
|
||||
// Decorative watercolor blobs. Inline SVG so fills can reference theme tokens.
|
||||
<svg class="absolute -top-12 -left-12 w-[28rem] h-[28rem] -z-0 animate-breathe" viewBox="0 0 400 400" aria-hidden="true">
|
||||
<path d="M200,40 C290,40 360,120 360,200 C360,290 280,360 200,360 C110,360 40,280 40,200 C40,110 110,40 200,40 Z" fill="hsl(var(--primary) / 0.22)"></path>
|
||||
</svg>
|
||||
<svg class="absolute -bottom-16 -right-8 w-[24rem] h-[24rem] -z-0 animate-breathe" viewBox="0 0 400 400" aria-hidden="true">
|
||||
<path d="M120,60 C200,40 320,80 360,180 C400,280 320,360 220,360 C120,360 40,300 40,220 C40,160 60,80 120,60 Z" fill="hsl(var(--accent) / 0.30)"></path>
|
||||
</svg>
|
||||
<div class="relative z-10 max-w-6xl mx-auto px-4 grid md:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
if data.Eyebrow != "" {
|
||||
<p class="font-mono text-xs uppercase tracking-[0.2em] mb-4" style="color: hsl(var(--muted-foreground));">
|
||||
{ data.Eyebrow }
|
||||
</p>
|
||||
}
|
||||
<h1 class="font-display brush-underline text-5xl md:text-6xl leading-tight mb-6" style="color: hsl(var(--foreground));">
|
||||
@templ.Raw(data.Headline)
|
||||
</h1>
|
||||
if data.Body != "" {
|
||||
<div class="font-body text-lg max-w-prose mb-8" style="color: hsl(var(--foreground) / 0.78);">
|
||||
@templ.Raw(data.Body)
|
||||
</div>
|
||||
}
|
||||
if data.CTAText != "" {
|
||||
<a href={ templ.SafeURL(data.CTAHref) } class="pastel-pill text-base animate-breathe">
|
||||
{ data.CTAText }
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
if data.Image != "" {
|
||||
<div class="relative">
|
||||
<div class="pastel-card overflow-hidden">
|
||||
<img src={ data.Image } alt="" class="w-full h-auto block"/>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
<div class="relative h-64 md:h-80" aria-hidden="true">
|
||||
<svg viewBox="0 0 300 240" class="w-full h-full">
|
||||
<ellipse cx="150" cy="120" rx="110" ry="80" fill="hsl(var(--secondary) / 0.55)"></ellipse>
|
||||
<ellipse cx="120" cy="100" rx="70" ry="50" fill="hsl(var(--accent) / 0.45)"></ellipse>
|
||||
<ellipse cx="180" cy="140" rx="60" ry="44" fill="hsl(var(--primary) / 0.40)"></ellipse>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
153
watercolor_hero_templ.go
Normal file
153
watercolor_hero_templ.go
Normal file
@ -0,0 +1,153 @@
|
||||
// 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"
|
||||
|
||||
// watercolorHeroComponent renders the watercolor hero with inline SVG blobs.
|
||||
// Path fill colors come from hsl(var(--primary)) / hsl(var(--accent)) so they
|
||||
// follow whatever preset is active. A scrim under the headline keeps text
|
||||
// readable when an image is provided.
|
||||
func watercolorHeroComponent(data WatercolorHeroData) 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 overflow-hidden py-20 md:py-28 bg-watercolor-blush\" data-block=\"pastel-dream:watercolor-hero\"><svg class=\"absolute -top-12 -left-12 w-[28rem] h-[28rem] -z-0 animate-breathe\" viewBox=\"0 0 400 400\" aria-hidden=\"true\"><path d=\"M200,40 C290,40 360,120 360,200 C360,290 280,360 200,360 C110,360 40,280 40,200 C40,110 110,40 200,40 Z\" fill=\"hsl(var(--primary) / 0.22)\"></path></svg> <svg class=\"absolute -bottom-16 -right-8 w-[24rem] h-[24rem] -z-0 animate-breathe\" viewBox=\"0 0 400 400\" aria-hidden=\"true\"><path d=\"M120,60 C200,40 320,80 360,180 C400,280 320,360 220,360 C120,360 40,300 40,220 C40,160 60,80 120,60 Z\" fill=\"hsl(var(--accent) / 0.30)\"></path></svg><div class=\"relative z-10 max-w-6xl mx-auto px-4 grid md:grid-cols-2 gap-12 items-center\"><div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Eyebrow != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"font-mono text-xs uppercase tracking-[0.2em] mb-4\" style=\"color: hsl(var(--muted-foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Eyebrow)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `watercolor_hero.templ`, Line: 20, Col: 20}
|
||||
}
|
||||
_, 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, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h1 class=\"font-display brush-underline text-5xl md:text-6xl leading-tight mb-6\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Headline).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Body != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"font-body text-lg max-w-prose mb-8\" style=\"color: hsl(var(--foreground) / 0.78);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Body).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.CTAText != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.CTAHref))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `watercolor_hero.templ`, Line: 32, Col: 42}
|
||||
}
|
||||
_, 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, 9, "\" class=\"pastel-pill text-base animate-breathe\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.CTAText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `watercolor_hero.templ`, Line: 33, Col: 20}
|
||||
}
|
||||
_, 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, 10, "</a>")
|
||||
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
|
||||
}
|
||||
if data.Image != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"relative\"><div class=\"pastel-card overflow-hidden\"><img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Image)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `watercolor_hero.templ`, Line: 40, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" alt=\"\" class=\"w-full h-auto block\"></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"relative h-64 md:h-80\" aria-hidden=\"true\"><svg viewBox=\"0 0 300 240\" class=\"w-full h-full\"><ellipse cx=\"150\" cy=\"120\" rx=\"110\" ry=\"80\" fill=\"hsl(var(--secondary) / 0.55)\"></ellipse> <ellipse cx=\"120\" cy=\"100\" rx=\"70\" ry=\"50\" fill=\"hsl(var(--accent) / 0.45)\"></ellipse> <ellipse cx=\"180\" cy=\"140\" rx=\"60\" ry=\"44\" fill=\"hsl(var(--primary) / 0.40)\"></ellipse></svg></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
Loading…
x
Reference in New Issue
Block a user