commit de55bbebd61a507fe24bb09b59a3a66e71a0ae0b Author: Alex Dunmow Date: Sat Jun 6 14:11:41 2026 +0800 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f780e6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.so +*.test +tmp/ +.idea/ +.vscode/ diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md new file mode 100644 index 0000000..79d4e32 --- /dev/null +++ b/BUILD_REPORT.md @@ -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 `.go` + `.templ` pair under repo root, one +`schemas/.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 `` 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 ` 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 +├── .go + .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 +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9b55af7 --- /dev/null +++ b/Makefile @@ -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" diff --git a/RECOMMENDED_FONTS.md b/RECOMMENDED_FONTS.md new file mode 100644 index 0000000..35e711e --- /dev/null +++ b/RECOMMENDED_FONTS.md @@ -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. diff --git a/affirmation.go b/affirmation.go new file mode 100644 index 0000000..6a43ec2 --- /dev/null +++ b/affirmation.go @@ -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 +} diff --git a/affirmation.templ b/affirmation.templ new file mode 100644 index 0000000..4d6ecc6 --- /dev/null +++ b/affirmation.templ @@ -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) { +
+
+

+ “{ data.Quote }” +

+ if data.Author != "" { +

+ { data.Author } +

+ } +
+
+} diff --git a/affirmation_templ.go b/affirmation_templ.go new file mode 100644 index 0000000..1bb8deb --- /dev/null +++ b/affirmation_templ.go @@ -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, "

“") + 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, "”

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Author != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/assets/css/pastel-dream.css b/assets/css/pastel-dream.css new file mode 100644 index 0000000..dcb1c0b --- /dev/null +++ b/assets/css/pastel-dream.css @@ -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; + } +} diff --git a/assets/fonts/web/README.txt b/assets/fonts/web/README.txt new file mode 100644 index 0000000..65a45c7 --- /dev/null +++ b/assets/fonts/web/README.txt @@ -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. diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..7fee599 --- /dev/null +++ b/assets/style.css @@ -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; + } +} diff --git a/button_override.go b/button_override.go new file mode 100644 index 0000000..2f91709 --- /dev/null +++ b/button_override.go @@ -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 +} diff --git a/button_override.templ b/button_override.templ new file mode 100644 index 0000000..f90009e --- /dev/null +++ b/button_override.templ @@ -0,0 +1,8 @@ +package main + +// pastelButtonComponent renders the pill button override. +templ pastelButtonComponent(data PastelButtonData) { +
+ { data.Text } + +} diff --git a/button_override_templ.go b/button_override_templ.go new file mode 100644 index 0000000..5503f2c --- /dev/null +++ b/button_override_templ.go @@ -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, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/card_override.go b/card_override.go new file mode 100644 index 0000000..ab89c78 --- /dev/null +++ b/card_override.go @@ -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 +} diff --git a/card_override.templ b/card_override.templ new file mode 100644 index 0000000..2049079 --- /dev/null +++ b/card_override.templ @@ -0,0 +1,20 @@ +package main + +// pastelCardComponent renders the soft pastel card. +templ pastelCardComponent(data PastelCardData) { +
+ if data.Title != "" { +

{ data.Title }

+ } + if data.Body != "" { +
+ @templ.Raw(data.Body) +
+ } + if data.Footer != "" { +
+ @templ.Raw(data.Footer) +
+ } +
+} diff --git a/card_override_templ.go b/card_override_templ.go new file mode 100644 index 0000000..23b3c61 --- /dev/null +++ b/card_override_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Body != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Footer != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/cozy_footer.go b/cozy_footer.go new file mode 100644 index 0000000..24c0069 --- /dev/null +++ b/cozy_footer.go @@ -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 +} diff --git a/cozy_footer.templ b/cozy_footer.templ new file mode 100644 index 0000000..5d4a2ab --- /dev/null +++ b/cozy_footer.templ @@ -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) { +
+ // Decorative watercolor blob, top right + +
+

+ { data.Affirmation } +

+ if data.ShowSignup { +
+ + + +
+ } + if len(data.Social) > 0 { +
+ for _, href := range data.Social { + + Visit + + } +
+ } +

+ Pastel Dream · with care +

+
+
+} diff --git a/cozy_footer_templ.go b/cozy_footer_templ.go new file mode 100644 index 0000000..3d5634d --- /dev/null +++ b/cozy_footer_templ.go @@ -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, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.ShowSignup { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Social) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, href := range data.Social { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Visit") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

Pastel Dream · with care

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/email_wrapper.templ b/email_wrapper.templ new file mode 100644 index 0000000..ca8274b --- /dev/null +++ b/email_wrapper.templ @@ -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) { + + + + + + + + { emailCtx.SiteSettings.SiteName } + + + + if emailCtx.PreviewText != "" { +
+ { emailCtx.PreviewText } +
+ } + + + + +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + +} + +// 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, + ) +} diff --git a/email_wrapper_templ.go b/email_wrapper_templ.go new file mode 100644 index 0000000..d73a1da --- /dev/null +++ b/email_wrapper_templ.go @@ -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, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.PreviewText != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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 diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..101c4b3 --- /dev/null +++ b/embed.go @@ -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 +} diff --git a/feature_grid_soft.go b/feature_grid_soft.go new file mode 100644 index 0000000..6ff0625 --- /dev/null +++ b/feature_grid_soft.go @@ -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 +} diff --git a/feature_grid_soft.templ b/feature_grid_soft.templ new file mode 100644 index 0000000..bb93626 --- /dev/null +++ b/feature_grid_soft.templ @@ -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) { +
+
+ if data.Intro != "" { +
+ @templ.Raw(data.Intro) +
+ } + if len(data.Items) > 0 { +
+ for _, item := range data.Items { +
+ if item.Icon != "" { +
+ +
+ } else { + + } + if item.Title != "" { +

{ item.Title }

+ } + if item.Body != "" { +

{ item.Body }

+ } +
+ } +
+ } else { +
+

Add up to three cards to the trio.

+
+ } +
+
+} diff --git a/feature_grid_soft_templ.go b/feature_grid_soft_templ.go new file mode 100644 index 0000000..5e18d1d --- /dev/null +++ b/feature_grid_soft_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Intro != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Items) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, item := range data.Items { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if item.Icon != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
\"\"
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if item.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if item.Body != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

Add up to three cards to the trio.

") + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/fonts.json b/fonts.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/fonts.json @@ -0,0 +1 @@ +[] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7429a30 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..26aea2c --- /dev/null +++ b/go.sum @@ -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= diff --git a/heading_override.go b/heading_override.go new file mode 100644 index 0000000..21e9fea --- /dev/null +++ b/heading_override.go @@ -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 +} diff --git a/heading_override.templ b/heading_override.templ new file mode 100644 index 0000000..de42584 --- /dev/null +++ b/heading_override.templ @@ -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: +

{ text }

+ case 2: +

{ text }

+ case 3: +

{ text }

+ case 4: +

{ text }

+ case 5: +
{ text }
+ case 6: +
{ text }
+ default: +

{ text }

+ } +} diff --git a/heading_override_templ.go b/heading_override_templ.go new file mode 100644 index 0000000..a4842fc --- /dev/null +++ b/heading_override_templ.go @@ -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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + 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, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..7496164 --- /dev/null +++ b/helpers.go @@ -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 +} diff --git a/plugin.mod b/plugin.mod new file mode 100644 index 0000000..ca1308b --- /dev/null +++ b/plugin.mod @@ -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" diff --git a/presets.json b/presets.json new file mode 100644 index 0000000..a349754 --- /dev/null +++ b/presets.json @@ -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%" + } + } + } +] diff --git a/register.go b/register.go new file mode 100644 index 0000000..9c69fb9 --- /dev/null +++ b/register.go @@ -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, + }, + }, + }, + } +} diff --git a/registration.go b/registration.go new file mode 100644 index 0000000..502b30e --- /dev/null +++ b/registration.go @@ -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), + } +} diff --git a/schemas/affirmation.schema.json b/schemas/affirmation.schema.json new file mode 100644 index 0000000..15008eb --- /dev/null +++ b/schemas/affirmation.schema.json @@ -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" + } + } +} diff --git a/schemas/cozy-footer.schema.json b/schemas/cozy-footer.schema.json new file mode 100644 index 0000000..36e30d0 --- /dev/null +++ b/schemas/cozy-footer.schema.json @@ -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" + } + } + } +} diff --git a/schemas/feature-grid-soft.schema.json b/schemas/feature-grid-soft.schema.json new file mode 100644 index 0000000..4ef1cf8 --- /dev/null +++ b/schemas/feature-grid-soft.schema.json @@ -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"] + } + } + } +} diff --git a/schemas/soft-navbar.schema.json b/schemas/soft-navbar.schema.json new file mode 100644 index 0000000..64114d4 --- /dev/null +++ b/schemas/soft-navbar.schema.json @@ -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" + } + } +} diff --git a/schemas/testimonial-soft.schema.json b/schemas/testimonial-soft.schema.json new file mode 100644 index 0000000..800f917 --- /dev/null +++ b/schemas/testimonial-soft.schema.json @@ -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" + } + } +} diff --git a/schemas/watercolor-hero.schema.json b/schemas/watercolor-hero.schema.json new file mode 100644 index 0000000..9f7fc06 --- /dev/null +++ b/schemas/watercolor-hero.schema.json @@ -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" + } + } +} diff --git a/soft_navbar.go b/soft_navbar.go new file mode 100644 index 0000000..99cd595 --- /dev/null +++ b/soft_navbar.go @@ -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 +} diff --git a/soft_navbar.templ b/soft_navbar.templ new file mode 100644 index 0000000..93705af --- /dev/null +++ b/soft_navbar.templ @@ -0,0 +1,31 @@ +package main + +// softNavbarComponent renders a rounded pill navigation with watercolor wash +// behind the logo wordmark. +templ softNavbarComponent(data SoftNavbarData) { + +} diff --git a/soft_navbar_templ.go b/soft_navbar_templ.go new file mode 100644 index 0000000..d18afe7 --- /dev/null +++ b/soft_navbar_templ.go @@ -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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/template.templ b/template.templ new file mode 100644 index 0000000..f10267b --- /dev/null +++ b/template.templ @@ -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) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+

This page is still resting. Add a block to begin.

+
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// PastelLanding is the marketing landing layout — full-width hero, body, +// affirmation CTA, footer. +templ PastelLanding(data PastelPageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["hero"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { +
+ @templ.Raw(main) +
+ } +
+
+ @templ.Raw(data.Slots["cta"]) +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// PastelArticle is the narrow reading-room layout for editorial posts. +templ PastelArticle(data PastelPageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { +
+ @templ.Raw(main) +
+ } else { +
+

This page is still resting. Add a block to begin.

+
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// PastelFullWidth is the edge-to-edge layout for galleries and seasonal looks. +templ PastelFullWidth(data PastelPageData) { + + + @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, + }) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["header"]) +
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
+

This page is still resting. Add a block to begin.

+
+ } +
+
+ @templ.Raw(data.Slots["footer"]) +
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// 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)) +} diff --git a/template_templ.go b/template_templ.go new file mode 100644 index 0000000..9363d58 --- /dev/null +++ b/template_templ.go @@ -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, "") + 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, "") + 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, "
") + 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, "
") + 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, "

This page is still resting. Add a block to begin.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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, "
") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + 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, "
") + 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, "
") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

This page is still resting. Add a block to begin.

") + 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 + } + 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, "
") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "

This page is still resting. Add a block to begin.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + 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, "
") + 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, "") + 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 diff --git a/testimonial_soft.go b/testimonial_soft.go new file mode 100644 index 0000000..0304a50 --- /dev/null +++ b/testimonial_soft.go @@ -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 +} diff --git a/testimonial_soft.templ b/testimonial_soft.templ new file mode 100644 index 0000000..0741893 --- /dev/null +++ b/testimonial_soft.templ @@ -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) { +
+ if data.Rating > 0 { +
+ for i := 0; i < data.Rating; i++ { + + + + } +
+ } +
+ @templ.Raw(data.Quote) +
+ if data.Name != "" || data.Role != "" { +
+ if data.Avatar != "" { + { + } else if data.Name != "" { + + { initials(data.Name) } + + } +
+ if data.Name != "" { +

{ data.Name }

+ } + if data.Role != "" { +

{ data.Role }

+ } +
+
+ } +
+} diff --git a/testimonial_soft_templ.go b/testimonial_soft_templ.go new file mode 100644 index 0000000..1f5d635 --- /dev/null +++ b/testimonial_soft_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Rating > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Name != "" || data.Role != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Avatar != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.Name != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Name != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Role != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/text_override.go b/text_override.go new file mode 100644 index 0000000..aabd9c5 --- /dev/null +++ b/text_override.go @@ -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() +} diff --git a/text_override.templ b/text_override.templ new file mode 100644 index 0000000..d065afd --- /dev/null +++ b/text_override.templ @@ -0,0 +1,8 @@ +package main + +// pastelTextComponent renders body text with Nunito at line-height 1.75. +templ pastelTextComponent(text, class string) { +
+ @templ.Raw(text) +
+} diff --git a/text_override_templ.go b/text_override_templ.go new file mode 100644 index 0000000..f9ab8fb --- /dev/null +++ b/text_override_templ.go @@ -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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/watercolor_hero.go b/watercolor_hero.go new file mode 100644 index 0000000..3d0f67d --- /dev/null +++ b/watercolor_hero.go @@ -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 +} diff --git a/watercolor_hero.templ b/watercolor_hero.templ new file mode 100644 index 0000000..852b1af --- /dev/null +++ b/watercolor_hero.templ @@ -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) { +
+ // Decorative watercolor blobs. Inline SVG so fills can reference theme tokens. + + +
+
+ if data.Eyebrow != "" { +

+ { data.Eyebrow } +

+ } +

+ @templ.Raw(data.Headline) +

+ if data.Body != "" { +
+ @templ.Raw(data.Body) +
+ } + if data.CTAText != "" { + + { data.CTAText } + + } +
+ if data.Image != "" { +
+
+ +
+
+ } else { + + } +
+
+} diff --git a/watercolor_hero_templ.go b/watercolor_hero_templ.go new file mode 100644 index 0000000..1113c1d --- /dev/null +++ b/watercolor_hero_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Eyebrow != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Body != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.CTAText != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + 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, "") + 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 data.Image != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
\"\"
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate