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