Compare commits
No commits in common. "main" and "v0.1.1" have entirely different histories.
1
.gitignore
vendored
@ -3,4 +3,3 @@
|
||||
tmp/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.bnp
|
||||
|
||||
329
BUILD_REPORT.md
@ -1,85 +1,280 @@
|
||||
# Pastel Dream — Build Report (Wave A, WO-TF-023)
|
||||
# Pastel Dream — Build Report
|
||||
|
||||
Bakery / patisserie persona. Codeless `.bnp` theme (no Go, no wasm). Built and
|
||||
verified against cms main at `a0f07e2ae` (OVERRIDE-CONTRACT freeze).
|
||||
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
|
||||
- `plugin.mod`: `name = "pastel-dream"`, `kind = "theme"`, `scope = "@themes"`,
|
||||
`version = "0.2.0"`, bakery tags, `required_icon_packs = ["lucide", "simple-icons"]`.
|
||||
- `manifest.yaml`: system template `pastel-dream`, 7 page templates, email
|
||||
wrapper, 49 template overrides, `css.input_css_append` (pastel utility layer),
|
||||
icon packs, presets/fonts/master-pages references.
|
||||
### 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.
|
||||
|
||||
### Overrides (49 mandatory keys)
|
||||
- One `templates/overrides/pastel-dream/<key>.ninjatpl` per mandatory key, plus a
|
||||
`template_overrides` entry per key. Re-skinned from the field-correct `coffee`
|
||||
sibling (frozen at the same cms pin): every content field, provider var, custom
|
||||
tag, and JS block preserved verbatim; only styling and copy changed to the
|
||||
pastel bakery identity. Legacy button/card reads already reconciled per §4
|
||||
(`link`/`style`/`label`, `text`/`media`).
|
||||
### 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.
|
||||
|
||||
### Page templates (7)
|
||||
- `default`, `landing`, `article`, `full-width`, `blog-index`, `contact`, `auth`
|
||||
under `templates/pastel-dream/`. Soft watercolor-wash chrome (`pd-paper` body).
|
||||
### 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`.
|
||||
|
||||
### Persona blocks (4)
|
||||
- `pastel-dream:seasonal_specials`, `pastel-dream:cake_gallery`,
|
||||
`pastel-dream:order_ribbon`, `pastel-dream:bakers_note`. Each is a
|
||||
`blocks/blocks.yaml` definition + `<key>.schema.json` (draft-07) +
|
||||
`<key>.ninjatpl`. Media via `{% img %}`; arrays via `x-editor: collection`.
|
||||
The six old duplicate-builtin blocks (soft-navbar, watercolor-hero,
|
||||
affirmation, testimonial-soft, feature-grid-soft, cozy-footer) were deleted;
|
||||
their surfaces are now the navbar/hero/quote/testimonial/feature-grid/footer
|
||||
overrides.
|
||||
### 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.
|
||||
|
||||
### Presets (5, dual-mode)
|
||||
- `morning-haze`, `berry-cream`, `matcha-cream`, `lavender-honey`,
|
||||
`peach-blossom`. All `mode: "both"`, all 19 tokens per light + dark. Dark is a
|
||||
tuned dusk plum/berry ground (not a lightened pastel) per the WO.
|
||||
### Theme-owned blocks (6 in total)
|
||||
Each block has one `<key>.go` + `<key>.templ` pair under repo root, one
|
||||
`schemas/<key>.schema.json` (draft-07), and exactly one `br.Register` call
|
||||
in `register.go`. All blocks declare `Source: "pastel-dream"` on
|
||||
`BlockMeta`. Standalone signature `func(ctx, content) string` is used
|
||||
throughout (no `children` argument).
|
||||
|
||||
### Fonts (bundled, OFL)
|
||||
- `fonts.json`: **Fraunces** 400/600 (heading) + **Quicksand** 400/700 (body),
|
||||
woff2 in `assets/fonts/`, OFL-1.1 recorded in `assets/fonts/OFL.txt`. Every
|
||||
`font-family` resolves through `var(--font-heading|body|mono, <fallback>)`.
|
||||
| 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) |
|
||||
|
||||
### Email
|
||||
- `templates/email/pastel-dream.ninjatpl`: table-based, inline styles, pastel
|
||||
hex fallbacks for clients without CSS custom properties, scalloped divider.
|
||||
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}`.
|
||||
|
||||
### Master pages + seed
|
||||
- `master_pages.json`: default/landing/blog/auth masters, "Petal & Crumb" brand.
|
||||
- `seed/seed.json` (`demo: true`): home, about, blog index + 3 posts, contact,
|
||||
login. Contact form → `contact_submissions` data table via
|
||||
`formConfig.targetTable`. `seed/workflows.json`: `row_inserted` →
|
||||
email-admins ("Form Submission Notification", recipients admins/all). Bakery
|
||||
voice, no LLM-isms, no em-dashes, no fabricated stats.
|
||||
### 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))`).
|
||||
|
||||
### Motion tier: CSS-FIRST
|
||||
- Watercolor wash + gentle `pd-breathe` blob drift + brush draw-in only. No
|
||||
canvas, no JS showpiece. All animation disabled under
|
||||
`prefers-reduced-motion: reduce`.
|
||||
### Master pages
|
||||
`DefaultMasterPages()` returns exactly two entries:
|
||||
|
||||
## Verification (both exit 0)
|
||||
1. **`pastel-dream:default-master`** — `PageTemplates: ["default", "article"]`.
|
||||
Blocks: `pastel-dream:soft-navbar` (header), `slot` (main), `pastel-dream:cozy-footer` (footer).
|
||||
2. **`pastel-dream:landing-master`** — `PageTemplates: ["landing"]`.
|
||||
Blocks: `pastel-dream:watercolor-hero` (hero), `slot` (main),
|
||||
`pastel-dream:affirmation` (cta), `pastel-dream:cozy-footer` (footer).
|
||||
|
||||
`slot` block `Content.slotName` matches its slot name in every case.
|
||||
|
||||
### Presets (presets.json)
|
||||
Three presets present, ids exactly `blush-morning`, `mint-meadow`,
|
||||
`twilight-petal`. All 19 tokens declared per preset. Every value matches
|
||||
`^\d+ \d+% \d+%$` (HSL triple, no `hsl()` wrapper). `blush-morning` and
|
||||
`mint-meadow` carry `lightColors`; `twilight-petal` carries `darkColors`.
|
||||
Values copied verbatim from spec §"Variants (presets.json)".
|
||||
|
||||
### Fonts (wave-1 policy)
|
||||
- `fonts.json = []` (literal). Embed succeeds.
|
||||
- No woff2s bundled. `assets/fonts/web/` holds a `README.txt` placeholder
|
||||
so the directory is non-empty (required by `//go:embed assets/*`).
|
||||
- CSS variable consumer pattern in use everywhere:
|
||||
`var(--font-heading, "Caveat Brush", …)`, `var(--font-body, "Nunito", …)`,
|
||||
`var(--font-mono, "JetBrains Mono", …)`. Fallback stacks degrade
|
||||
gracefully when no admin has assigned fonts.
|
||||
- `RECOMMENDED_FONTS.md` shipped at theme root with Google Fonts picker
|
||||
recommendations for Caveat Brush, Nunito, JetBrains Mono per spec §5.
|
||||
|
||||
### CSS strategy
|
||||
- `--radius-soft: 20px;` declared once.
|
||||
- `@keyframes pastel-shimmer` and `@keyframes breathe` declared.
|
||||
- `cubic-bezier(.22,1,.36,1)` used in `.pastel-pill` and feature card
|
||||
transitions.
|
||||
- `.bg-watercolor-blush` / `.bg-watercolor-mint` utility classes provided
|
||||
for hero / footer backdrops.
|
||||
- `prefers-reduced-motion: reduce` media query disables both animations and
|
||||
the pill hover transform.
|
||||
- Theme CSS is injected via `CSSManifest.InputCSSAppend` (reads
|
||||
`assets/css/pastel-dream.css`). The asset endpoint also serves a
|
||||
mirrored `assets/style.css` so direct loads work in case the manifest is
|
||||
bypassed.
|
||||
- No hardcoded hex / rgb / named colors in `*.templ`, `*.go`, or
|
||||
`assets/css/` *for runtime UI* — every color resolves through
|
||||
`hsl(var(--token))`. The **email wrapper is an explicit exception**: it
|
||||
carries hex fallbacks because email clients cannot read CSS custom
|
||||
properties. The colors are sourced from `EmailContext.Colors` when
|
||||
present; the hex fallbacks only fire when no preset is bound to the
|
||||
email (e.g. raw previews). See "Open items / deferred" below.
|
||||
|
||||
### Email wrapper
|
||||
- `tr.RegisterEmailWrapper("pastel-dream", PastelEmailWrapper)` called once.
|
||||
- 560 px centered frame, cream paper backdrop, top-right watercolor blob
|
||||
watermark (inline SVG with `fill = hsl(var(--primary))` via
|
||||
`pastelEmailPrimary`).
|
||||
- Caveat Brush masthead (`'Caveat Brush', 'Sacramento', cursive`),
|
||||
Nunito body.
|
||||
- Footer carries an `<a>` for the unsubscribe URL token and a one-line
|
||||
Caveat Brush affirmation.
|
||||
- A reusable mint CTA pill style helper (`pastelEmailPillStyle`) is
|
||||
exported for body content to consume.
|
||||
|
||||
## Build output
|
||||
|
||||
```
|
||||
$ cd themes/pastel-dream && make
|
||||
Built pastel-dream@0.2.0 (codeless) → pastel-dream-0.2.0.bnp # EXIT 0
|
||||
|
||||
$ cd check-safety && go run . themes/pastel-dream
|
||||
32 checks: 19 ok 13 skip -> OK # EXIT 0
|
||||
$ 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
|
||||
```
|
||||
|
||||
Runtime template rendering is not exercised by these gates (codeless build +
|
||||
check-safety are static). Visual correctness stays UNVERIFIED until Wave B.
|
||||
## Safety check
|
||||
|
||||
## Known limits (platform gaps, per OVERRIDE-CONTRACT §11/§12)
|
||||
- `auth-form`, `auth-status`, `password-reset-form`, `page-suggestions` overrides
|
||||
are inert until the compiled-block override-dispatch fix lands (authored
|
||||
correctly to model).
|
||||
- Provider-backed overrides (navbar/footer menus, featured/related/popular posts,
|
||||
category-list, breadcrumbs, blog-hero, sidebar-nav, author-bio-hero) render
|
||||
themed chrome but empty lists until the codeless loader registers overrides via
|
||||
the source path. Authored faithful to the builtin provider var names.
|
||||
- `preview.png` is the old (wellness) render and must be regenerated in Wave B.
|
||||
The task brief points at
|
||||
`/home/alex/src/blockninja/backend/go.mod` / `./cmd/check-safety`. **That
|
||||
backend path does not exist on this system.** The canonical check-safety
|
||||
tool lives at `/home/alex/src/blockninja/check-safety/` and reads the CMS
|
||||
SDK version from `/home/alex/src/blockninja/cms/backend/go.mod`. I ran it
|
||||
from that location:
|
||||
|
||||
```
|
||||
$ cd /home/alex/src/blockninja/check-safety
|
||||
$ go run . --plugin-dir /home/alex/src/blockninja/themes/pastel-dream
|
||||
```
|
||||
|
||||
### Per-plugin result: PASS
|
||||
|
||||
Every check that names `pastel-dream` reports `OK`:
|
||||
|
||||
```
|
||||
=== Check 1: Secret env var reads outside config.Load() ===
|
||||
OK: No secret env var reads outside config.Load() (1 plugin roots scanned)
|
||||
OK: pastel-dream
|
||||
=== Check 2b: Plugin proto ownership ===
|
||||
OK: pastel-dream
|
||||
=== Check 2c: Standalone plugin SDK import boundaries ===
|
||||
OK: Standalone plugin imports and go.mod stay on SDK version v0.11.1
|
||||
OK: pastel-dream
|
||||
=== Check 3: Go code compiles and passes go fix, golangci-lint --fix, go vet, and strict lint ===
|
||||
OK: 1 Go module(s) cleared the Go lint pipeline
|
||||
=== Check 6: No hardcoded colors in frontend (use theme tokens) ===
|
||||
OK: No hardcoded colors in .templ files
|
||||
=== Check 11: No placeholder code; only shipped features ===
|
||||
=== Check 12: No reinvented utilities (use helpers) ===
|
||||
OK: pastel-dream
|
||||
=== Check 17: No TODO markers in production code ===
|
||||
OK: pastel-dream
|
||||
=== Check 18: Plugin segmentation (safety-rules.yml) ===
|
||||
OK: pastel-dream
|
||||
=== Check 21: Plugin presets.json validation ===
|
||||
OK: pastel-dream presets valid
|
||||
```
|
||||
|
||||
Total: 10 OK lines mentioning `pastel-dream`, zero `FAIL:` lines that
|
||||
reference any path inside `themes/pastel-dream/`.
|
||||
|
||||
### Whole-run exit code: 1 (non-zero), but not pastel-dream-related
|
||||
|
||||
The `check-safety` binary always exits 1 in this layout because, when
|
||||
invoked from inside its own source directory, several of its own internal
|
||||
files trip the same regexes the tool enforces. The three FAILs the tool
|
||||
prints all live inside the tool's own source tree, not the plugin:
|
||||
|
||||
```
|
||||
FAIL: 21 placeholder reference(s) found:
|
||||
check_placeholder.go:51, colors.go:183, comingsoon.go:163-231, main.go:19, …
|
||||
FAIL: 10 TODO marker(s) found:
|
||||
check_todos.go:34, todo.go:86, todo.go:115, frontend.go:108, main.go:25
|
||||
FAIL: 3 hand-rolled HTML sanitization pattern(s):
|
||||
check_htmlsanitize.go:37-38, htmlsanitize.go:41
|
||||
```
|
||||
|
||||
None of those paths is in `themes/pastel-dream/`. The strict
|
||||
`./cmd/check-safety . --plugin-dir <pastel-dream>` exit-0 requirement
|
||||
from the task brief cannot be satisfied from this checkout without
|
||||
patching `check-safety` itself (out of scope and outside the theme
|
||||
directory). The **plugin-specific verdict** is PASS.
|
||||
|
||||
## Open items / deferred
|
||||
|
||||
These were scoped out of this implementation pass and should be picked up
|
||||
in wave-2 / UAT sign-off:
|
||||
|
||||
- **Bundled woff2 fonts.** Per wave-1 policy (`themes/docs/FONTS.md`),
|
||||
this pass ships `fonts.json = []`. Spec §"Bundled fonts" lists Caveat
|
||||
Brush, Nunito (300/400/600/700), and JetBrains Mono — these should be
|
||||
sourced (OFL is fine for all three), placed under `assets/fonts/web/`,
|
||||
declared in `fonts.json`, and licence-attributed in `LICENSES.md`. The
|
||||
matching UAT §11 checks (woff2 magic, 200 OK on fetch, FOUT ≤200ms,
|
||||
`@font-face` rule count ≥ 3) become real then.
|
||||
- **Marketplace assets (UAT §12).** Six 1440×900 PNG screenshots, demo
|
||||
seed for "Linden & Loom" (4 posts / 3 services / 2 testimonials), and
|
||||
the launch copy line require a running container, design QA, and the
|
||||
marketplace folder at `themes/docs/marketplace/pastel-dream/`. None of
|
||||
those can land inside the theme directory.
|
||||
- **Email wrapper hex fallbacks vs UAT §5 regex.** UAT §5 reads:
|
||||
`grep -rE "#[0-9a-fA-F]{3,8}|rgb\(|rgba\(" pastel-dream/*.templ … returns
|
||||
zero matches`. The email wrapper carries hex fallbacks for clients that
|
||||
cannot resolve CSS custom properties (Gmail web, Outlook, etc.). At
|
||||
runtime `EmailContext.Colors` is populated from the active preset so
|
||||
the hex values never reach an actual recipient — they are pure
|
||||
no-active-preset fallbacks. The check-safety colors check accepts this
|
||||
pattern (gotham follows the same convention). A future pass could move
|
||||
the fallback table into a hex-free Go file via `fmt.Sprintf` from HSL
|
||||
triples; deferred.
|
||||
- **Live container deploy.** `make rebuild` and the `instance-pastel-dream`
|
||||
container live in the host CMS workflow. UAT §2 checks about
|
||||
`podman ps` showing `Up`, `make logs` showing zero ERROR / panic, and
|
||||
any tests against `https://pastel-dream.localdev.blockninjacms.com/`
|
||||
require that container, which is out of scope for this build pass.
|
||||
- **DOM-based gates (UAT §5 line 7, §6, §7, §8, §10, §13).** These need
|
||||
the running URL with Chrome DevTools / Playwright. Computed-style and
|
||||
network-request assertions are deferred to manual UAT.
|
||||
- **Regression gate (UAT §14).** Comparing rendered HTML SHA-256 across
|
||||
multi-theme installs and uninstall behaviour against the real CMS DB is
|
||||
out of scope for the build pass.
|
||||
- **Sign-off (UAT §15).** Three named reviewers (Designer, Engineer,
|
||||
Marketplace owner) must independently tick boxes. Cannot be self-applied.
|
||||
|
||||
## Where everything lives
|
||||
|
||||
```
|
||||
themes/pastel-dream/
|
||||
├── BUILD_REPORT.md ← this file
|
||||
├── Makefile ← `make` builds .so locally, `make rebuild` deploys
|
||||
├── RECOMMENDED_FONTS.md ← wave-1 Google Fonts picker recommendations
|
||||
├── plugin.mod ← TOML metadata (locked to spec)
|
||||
├── go.mod / go.sum ← block/core v0.11.1, no replace directives
|
||||
├── presets.json ← 3 presets, all 19 tokens, HSL triples
|
||||
├── fonts.json ← []
|
||||
├── embed.go ← //go:embed wiring
|
||||
├── registration.go ← exports Registration + ThemeCSSManifest
|
||||
├── register.go ← Register() + DefaultMasterPages()
|
||||
├── helpers.go ← getString / getSlice / getBoolish / safeLink
|
||||
├── template.templ ← 4 page templates (Default, Landing, Article, FullWidth)
|
||||
├── email_wrapper.templ ← PastelEmailWrapper
|
||||
├── <block>.go + <block>.templ ← one pair per block + override
|
||||
├── *_templ.go ← templ-generated; committed
|
||||
├── schemas/ ← one *.schema.json per block
|
||||
├── assets/css/pastel-dream.css← injected via CSSManifest.InputCSSAppend
|
||||
├── assets/style.css ← served at /templates/pastel-dream/style.css
|
||||
└── assets/fonts/web/ ← reserved for wave-2 bundled woff2s
|
||||
```
|
||||
|
||||
221
Makefile
@ -1,55 +1,190 @@
|
||||
# pastel-dream — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
|
||||
# Pastel Dream — build & deploy helpers (.so plugin workflow)
|
||||
#
|
||||
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
|
||||
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
|
||||
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
|
||||
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
|
||||
# classifies the repo as codeless (no Go at the root) and packs the artifact
|
||||
# with manifest.pb (codeless: true) and NO plugin.wasm.
|
||||
# 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 build # pack pastel-dream-<version>.bnp (codeless — no wasm)
|
||||
# make verify # validate the built .bnp (loader-identical checks)
|
||||
# make archive-check # prove a clean `git archive HEAD` still builds codeless
|
||||
# make publish # ninja plugin publish --bnp (to the active registry)
|
||||
# make clean # remove build artefacts
|
||||
# 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: build verify archive-check publish clean help
|
||||
.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
|
||||
|
||||
PLUGIN_NAME := pastel-dream
|
||||
NINJA := ninja
|
||||
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
|
||||
# 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)
|
||||
|
||||
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
|
||||
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
|
||||
build:
|
||||
$(NINJA) plugin build --dir .
|
||||
# Default target: build the .so locally for development.
|
||||
all: $(PLUGIN_NAME).so
|
||||
|
||||
# Validate the built .bnp exactly as the registry/loader will.
|
||||
verify:
|
||||
$(NINJA) plugin verify $(BNP)
|
||||
# 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 .
|
||||
|
||||
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
|
||||
archive-check:
|
||||
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
|
||||
trap 'rm -rf "$$T"' EXIT && \
|
||||
git archive HEAD | tar -x -C "$$T" && \
|
||||
cd "$$T" && $(NINJA) plugin build --dir . && \
|
||||
echo "archive-check OK"
|
||||
|
||||
# Publish the prebuilt .bnp to the active registry.
|
||||
# Pass --host https://my.blockninja.dev for the dev orchestrator.
|
||||
publish: build
|
||||
$(NINJA) plugin publish --bnp $(BNP)
|
||||
|
||||
# Remove build artefacts.
|
||||
# Remove the compiled .so.
|
||||
clean:
|
||||
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
|
||||
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 " build Pack the codeless .bnp (no wasm)"
|
||||
@echo " verify Validate the built .bnp (loader-identical checks)"
|
||||
@echo " archive-check Prove a clean git archive HEAD packs codeless"
|
||||
@echo " publish Publish the .bnp to the active registry"
|
||||
@echo " clean Remove build artefacts"
|
||||
@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"
|
||||
|
||||
24
README.md
@ -1,24 +0,0 @@
|
||||
# Pastel Dream
|
||||
|
||||
A soft bakery window in the morning light. Watercolor washes fade behind scalloped edges, and rounded menu cards hold the day's specials in blush, berry, and lilac. The dark mode shifts to a warm dusk with deeper berry tones, so evening pages feel cosy rather than harsh. It looks sweet and unhurried.
|
||||
|
||||
## Good for
|
||||
|
||||
We built Pastel Dream for food and hospitality with a gentle touch. Treat these as starting points, not limits. If the mood fits your brand, use it:
|
||||
|
||||
- Bakeries and patisseries
|
||||
- Cafes and dessert bars
|
||||
- Florists and gift shops
|
||||
- Wedding and party catering
|
||||
|
||||
## What you get
|
||||
|
||||
- Five color presets, each with a tuned light mode and a tuned dusk-toned dark mode: Morning Haze, Berry Cream, Matcha Cream, Lavender Honey, and Peach Blossom.
|
||||
- Fraunces for headings and Quicksand for body, both bundled. Fonts stay admin-controlled, so you can swap them from the typography settings.
|
||||
- Four persona blocks: a baker's note, a cake gallery, an order ribbon with a call to action, and a seasonal specials board.
|
||||
- A demo site that installs when you activate the theme: a home page, an about page, a short blog, and a contact page whose form emails your admins.
|
||||
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
|
||||
|
||||
## Install
|
||||
|
||||
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully built starting point.
|
||||
@ -1,45 +1,45 @@
|
||||
# Recommended fonts — Pastel Dream
|
||||
|
||||
This theme **bundles** its typographic identity as woff2 files in
|
||||
`assets/fonts/` (see `fonts.json`). Both families are licensed under the SIL
|
||||
Open Font License 1.1 (`assets/fonts/OFL.txt`). No admin setup is needed for
|
||||
the intended look.
|
||||
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.
|
||||
|
||||
## Bundled (source = template)
|
||||
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.
|
||||
|
||||
| Slot | Family | Weights | Role |
|
||||
|------|--------|---------|------|
|
||||
| Heading | **Fraunces** | 400, 600 | Sweet, soft-serif display for headings, drop-caps, and signatures |
|
||||
| Body | **Quicksand** | 400, 700 | Rounded geometric sans for body copy, menus, and accents |
|
||||
## Display / hand-lettered (assign to Heading)
|
||||
|
||||
Assign in the typography panel:
|
||||
- `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. In the **Heading** slot pick **Fraunces**; in the **Body** slot pick
|
||||
**Quicksand**. Leave **Mono** on the system default, or pick a mono if you
|
||||
use price/number strings heavily.
|
||||
3. Save. The CMS emits the `@font-face` rules from the bundled files and
|
||||
re-renders.
|
||||
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.
|
||||
|
||||
If no fonts are assigned, the theme still renders correctly through the CSS
|
||||
variable fallback stacks (`--font-heading` → Fraunces/Playfair/serif;
|
||||
`--font-body` → Quicksand/Nunito/system sans; `--font-mono` → JetBrains
|
||||
Mono/system mono). The theme never hardcodes a `font-family` — every family
|
||||
resolves through `var(--font-heading|body|mono, <fallback>)` so the admin font
|
||||
picker keeps working.
|
||||
## Why no bundled woff2 in this pass
|
||||
|
||||
## Alternates from Google Fonts
|
||||
|
||||
If you prefer a different feel, these all suit the bakery persona and are in
|
||||
the curated Google Fonts list:
|
||||
|
||||
- Heading: `google:Comfortaa`, `google:Pacifico` (script), `google:Playfair Display`.
|
||||
- Body: `google:Nunito`, `google:Comfortaa`, `google:Varela Round`.
|
||||
- Mono: `google:JetBrains Mono`, `google:Fira Code`.
|
||||
|
||||
## Licensing
|
||||
|
||||
- **Fraunces** — Copyright 2018 The Fraunces Project Authors. OFL-1.1.
|
||||
- **Quicksand** — Copyright 2011 The Quicksand Project Authors. OFL-1.1.
|
||||
|
||||
Full text and copyright notices in `assets/fonts/OFL.txt`.
|
||||
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.
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
# Recommended icons — Pastel Dream
|
||||
|
||||
This theme renders icons through the CMS sprite system only
|
||||
(`<svg><use href="/icons/<pack>.svg#<name>"/></svg>`). It declares its packs in
|
||||
`plugin.mod` `required_icon_packs`. Until the loader auto-installs declared
|
||||
packs, install them by hand before activation.
|
||||
|
||||
## Required packs
|
||||
|
||||
| Pack | Used by |
|
||||
|------|---------|
|
||||
| **lucide** (bundled) | Navigation, hero and CTA arrows, contact and hours icons, timeline markers, gallery and card chrome. This is the primary pack and is bundled with the CMS. |
|
||||
| **simple-icons** (bundled) | Brand marks in the footer and social-links override (Instagram, Facebook, etc.). |
|
||||
|
||||
Both packs ship bundled with the CMS, so in most instances nothing needs to be
|
||||
installed. If a fresh instance is missing one:
|
||||
|
||||
1. Open `/admin/icons`.
|
||||
2. Find **Lucide** and **Simple Icons** and click **Install** / **Enable**.
|
||||
3. Save. Templates that reference `lucide:*` and `simple-icons:*` now resolve.
|
||||
|
||||
## Notes
|
||||
|
||||
- Block content stores icon references as `"pack:name"` strings and the
|
||||
templates split them at render time. Do not inline raw SVG.
|
||||
- If an icon name is missing from a pack, the sprite `<use>` simply renders
|
||||
nothing rather than breaking layout. Prefer the bundled Lucide names already
|
||||
used across the overrides (`arrow-right`, `map-pin`, `navigation`,
|
||||
`croissant`, `menu`, `x`, `chevron-down`).
|
||||
46
affirmation.go
Normal file
@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// AffirmationMeta defines the affirmation strip block.
|
||||
var AffirmationMeta = blocks.BlockMeta{
|
||||
Key: "affirmation",
|
||||
Title: "Affirmation Strip",
|
||||
Description: "Slow-shimmer band carrying a short calming line in display type.",
|
||||
Source: "pastel-dream",
|
||||
Category: blocks.CategoryTheme,
|
||||
}
|
||||
|
||||
// AffirmationBlock renders the affirmation strip.
|
||||
// Content shape: {quote, author, palette}
|
||||
func AffirmationBlock(ctx context.Context, content map[string]any) string {
|
||||
palette := getStringOr(content, "palette", "blush")
|
||||
switch palette {
|
||||
case "blush", "mint", "butter", "sky":
|
||||
// valid
|
||||
default:
|
||||
palette = "blush"
|
||||
}
|
||||
|
||||
data := AffirmationData{
|
||||
Quote: getStringOr(content, "quote", "You are doing enough."),
|
||||
Author: getString(content, "author"),
|
||||
Palette: palette,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = affirmationComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// AffirmationData is the rendered view of the affirmation block.
|
||||
type AffirmationData struct {
|
||||
Quote string
|
||||
Author string
|
||||
Palette string
|
||||
}
|
||||
37
affirmation.templ
Normal file
@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
// affirmationPaletteVar maps a palette name to a token reference. All four
|
||||
// names resolve to a theme token so the strip never fails to colorize.
|
||||
func affirmationPaletteVar(palette string) string {
|
||||
switch palette {
|
||||
case "mint":
|
||||
return "hsl(var(--secondary) / 0.55)"
|
||||
case "butter":
|
||||
return "hsl(var(--accent) / 0.55)"
|
||||
case "sky":
|
||||
return "hsl(var(--secondary) / 0.40)"
|
||||
default:
|
||||
return "hsl(var(--primary) / 0.35)"
|
||||
}
|
||||
}
|
||||
|
||||
// affirmationComponent renders the shimmering affirmation strip.
|
||||
templ affirmationComponent(data AffirmationData) {
|
||||
<section
|
||||
class="relative py-16 overflow-hidden animate-pastel-shimmer"
|
||||
style={ "background-image: linear-gradient(90deg, hsl(var(--background)) 0%, " + affirmationPaletteVar(data.Palette) + " 50%, hsl(var(--background)) 100%);" }
|
||||
data-block="pastel-dream:affirmation"
|
||||
data-palette={ data.Palette }
|
||||
>
|
||||
<div class="relative z-10 max-w-3xl mx-auto px-6 text-center">
|
||||
<p class="font-display text-3xl md:text-4xl leading-snug" style="color: hsl(var(--foreground));">
|
||||
“{ data.Quote }”
|
||||
</p>
|
||||
if data.Author != "" {
|
||||
<p class="font-body mt-4 text-sm uppercase tracking-[0.2em]" style="color: hsl(var(--muted-foreground));">
|
||||
{ data.Author }
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
118
affirmation_templ.go
Normal file
@ -0,0 +1,118 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// affirmationPaletteVar maps a palette name to a token reference. All four
|
||||
// names resolve to a theme token so the strip never fails to colorize.
|
||||
func affirmationPaletteVar(palette string) string {
|
||||
switch palette {
|
||||
case "mint":
|
||||
return "hsl(var(--secondary) / 0.55)"
|
||||
case "butter":
|
||||
return "hsl(var(--accent) / 0.55)"
|
||||
case "sky":
|
||||
return "hsl(var(--secondary) / 0.40)"
|
||||
default:
|
||||
return "hsl(var(--primary) / 0.35)"
|
||||
}
|
||||
}
|
||||
|
||||
// affirmationComponent renders the shimmering affirmation strip.
|
||||
func affirmationComponent(data AffirmationData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"relative py-16 overflow-hidden animate-pastel-shimmer\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-image: linear-gradient(90deg, hsl(var(--background)) 0%, " + affirmationPaletteVar(data.Palette) + " 50%, hsl(var(--background)) 100%);")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `affirmation.templ`, Line: 22, Col: 158}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" data-block=\"pastel-dream:affirmation\" data-palette=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Palette)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `affirmation.templ`, Line: 24, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><div class=\"relative z-10 max-w-3xl mx-auto px-6 text-center\"><p class=\"font-display text-3xl md:text-4xl leading-snug\" style=\"color: hsl(var(--foreground));\">“")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Quote)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `affirmation.templ`, Line: 28, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "”</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Author != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"font-body mt-4 text-sm uppercase tracking-[0.2em]\" style=\"color: hsl(var(--muted-foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `affirmation.templ`, Line: 32, Col: 18}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
187
assets/css/pastel-dream.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,109 +0,0 @@
|
||||
Bundled fonts — SIL Open Font License 1.1
|
||||
=========================================
|
||||
|
||||
This directory bundles latin-subset woff2 files for two families, both
|
||||
licensed under the SIL Open Font License, Version 1.1 (OFL-1.1).
|
||||
|
||||
Copyright notices
|
||||
-----------------
|
||||
Fraunces: Copyright 2018 The Fraunces Project Authors (https://github.com/undercasetype/Fraunces)
|
||||
Quicksand: Copyright 2011 The Quicksand Project Authors (https://github.com/andrew-paglinawan/QuicksandFamily)
|
||||
|
||||
Files
|
||||
-----
|
||||
fraunces-latin-400.woff2, fraunces-latin-600.woff2 — Fraunces (sweet display headings)
|
||||
quicksand-400.woff2, quicksand-700.woff2 — Quicksand (rounded body)
|
||||
|
||||
Full license text (SIL OFL 1.1)
|
||||
-------------------------------
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
5
assets/fonts/web/README.txt
Normal file
@ -0,0 +1,5 @@
|
||||
This directory is reserved for bundled woff2 font files.
|
||||
|
||||
In this wave-1 implementation pass, `fonts.json = []` and no woff2s are
|
||||
bundled. See ../../../RECOMMENDED_FONTS.md for the Google Fonts the admin
|
||||
should add via the typography panel.
|
||||
318
assets/style.css
@ -1,257 +1,133 @@
|
||||
/* Pastel Dream theme styles — bakery / patisserie.
|
||||
/* Pastel Dream — theme stylesheet served at /templates/pastel-dream/style.css
|
||||
*
|
||||
* Dual-mode via the 19 shadcn-style HSL token CSS variables (--background,
|
||||
* --foreground, --primary, --accent, --border, --muted, --card, ...) consumed
|
||||
* as hsl(var(--token)). No literal colors anywhere. Font families resolve ONLY
|
||||
* through the BlockNinja font variables (--font-heading, --font-body,
|
||||
* --font-mono) with the fallback stacks below — never hardcoded, so the admin
|
||||
* font picker keeps working.
|
||||
*
|
||||
* Motion tier: CSS-FIRST. Every animation is a gentle CSS drift and is disabled
|
||||
* under prefers-reduced-motion. No canvas, no JS showpiece.
|
||||
* 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 {
|
||||
--pd-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
||||
--pd-body-fallback: "Quicksand", "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--pd-hand-fallback: "Quicksand", "Comfortaa", "Segoe Print", cursive;
|
||||
--pd-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
|
||||
--pd-radius: 1.5rem;
|
||||
--radius-soft: 20px;
|
||||
}
|
||||
|
||||
/* --- Watercolor wash background ----------------------------------------- */
|
||||
/* The signature "morning haze": soft radial pastel washes tinted from the
|
||||
* active palette, drifting slowly. Reads as watercolor on cream in light mode
|
||||
* and as a dusk-lilac glow in dark mode — all from the same tokens. */
|
||||
body.pd-paper {
|
||||
background-color: hsl(var(--background));
|
||||
background-image:
|
||||
radial-gradient(60% 55% at 15% 12%, hsl(var(--primary) / 0.14) 0%, transparent 60%),
|
||||
radial-gradient(55% 50% at 85% 20%, hsl(var(--accent) / 0.12) 0%, transparent 58%),
|
||||
radial-gradient(65% 60% at 75% 85%, hsl(var(--secondary) / 0.40) 0%, transparent 62%),
|
||||
radial-gradient(50% 50% at 25% 90%, hsl(var(--primary) / 0.08) 0%, transparent 60%);
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
/* Guard against horizontal scroll on small screens from any off-canvas
|
||||
* chrome (mobile drawer). `clip` does not create a scroll container, so
|
||||
* it will not break the sticky navbar the way `hidden` can. */
|
||||
overflow-x: clip;
|
||||
.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%);
|
||||
}
|
||||
|
||||
/* --- Type roles --------------------------------------------------------- */
|
||||
.pd-display {
|
||||
font-family: var(--font-heading, var(--pd-heading-fallback));
|
||||
font-optical-sizing: auto;
|
||||
font-feature-settings: "liga" 1, "dlig" 1;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
.pd-body {
|
||||
font-family: var(--font-body, var(--pd-body-fallback));
|
||||
line-height: 1.7;
|
||||
}
|
||||
.pd-mono {
|
||||
font-family: var(--font-mono, var(--pd-mono-fallback));
|
||||
}
|
||||
/* Sweet accent lettering (badges, captions, section titles). Routed through
|
||||
* --font-heading with a rounded fallback: unassigned it renders Quicksand; if
|
||||
* the admin sets a heading font the accents follow it (3-slot model intended). */
|
||||
.pd-hand {
|
||||
font-family: var(--font-heading, var(--pd-hand-fallback));
|
||||
letter-spacing: 0.005em;
|
||||
line-height: 1.2;
|
||||
.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%);
|
||||
}
|
||||
|
||||
/* --- Brush underline ---------------------------------------------------- */
|
||||
.pd-doodle-underline {
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8 Q 22 1 44 6 T 88 6 T 118 6' fill='none' stroke='currentColor' stroke-width='4' stroke-linecap='round' opacity='0.35'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 100%;
|
||||
background-size: 100% 0.55em;
|
||||
padding-bottom: 0.16em;
|
||||
.shadow-soft {
|
||||
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
|
||||
0 4px 12px -2px hsl(var(--primary) / 0.10);
|
||||
}
|
||||
|
||||
/* --- Drop-cap ----------------------------------------------------------- */
|
||||
.pd-dropcap > p:first-of-type::first-letter {
|
||||
font-family: var(--font-heading, var(--pd-heading-fallback));
|
||||
font-size: 3.8em;
|
||||
line-height: 0.8;
|
||||
float: left;
|
||||
padding: 0.06em 0.14em 0 0;
|
||||
color: hsl(var(--primary));
|
||||
.deckle-edge {
|
||||
border: 1px dashed hsl(var(--border));
|
||||
}
|
||||
|
||||
/* --- Soft pill button (icing-smooth, lifts on hover) -------------------- */
|
||||
.pd-tag {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.7rem 1.5rem;
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
font-family: var(--font-body, var(--pd-body-fallback));
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
box-shadow: 0 8px 22px -8px hsl(var(--primary) / 0.45);
|
||||
transition: transform 220ms cubic-bezier(.22,1,.36,1), box-shadow 220ms cubic-bezier(.22,1,.36,1), background-color 220ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pd-tag:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 14px 30px -8px hsl(var(--primary) / 0.52);
|
||||
background-color: hsl(var(--primary) / 0.92);
|
||||
}
|
||||
.pd-tag:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.pd-tag--ghost {
|
||||
background-color: transparent;
|
||||
color: hsl(var(--primary));
|
||||
box-shadow: none;
|
||||
border: 1.5px solid hsl(var(--primary) / 0.45);
|
||||
}
|
||||
.pd-tag--ghost:hover {
|
||||
background-color: hsl(var(--primary) / 0.08);
|
||||
box-shadow: none;
|
||||
.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;
|
||||
}
|
||||
|
||||
/* --- Scalloped edges (piped-icing / doily trim) ------------------------- */
|
||||
.pd-torn-top {
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 6 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 L120 12 L0 12 Z' fill='black'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 6 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 L120 12 L0 12 Z' fill='black'/%3E%3C/svg%3E");
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
.pd-torn-bottom {
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L120 0 L120 6 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 Z' fill='black'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L120 0 L120 6 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 Z' fill='black'/%3E%3C/svg%3E");
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
.pastel-pill:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px -6px hsl(var(--primary) / 0.50);
|
||||
background-color: hsl(var(--primary) / 0.92);
|
||||
}
|
||||
|
||||
/* --- Surfaces (rounded-everything) ------------------------------------- */
|
||||
.pd-card {
|
||||
background-color: hsl(var(--card));
|
||||
color: hsl(var(--card-foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--pd-radius);
|
||||
box-shadow: 0 12px 40px -14px hsl(var(--primary) / 0.22), 0 4px 12px -6px hsl(var(--primary) / 0.10);
|
||||
}
|
||||
.pd-frame {
|
||||
border: 1px solid hsl(var(--border));
|
||||
background-color: hsl(var(--card));
|
||||
border-radius: var(--pd-radius);
|
||||
.pastel-pill:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* --- Menu card (soft pastel board for the menu-list override) ---------- */
|
||||
/* Not an inverted chalkboard: a soft card with a primary wash. Text inherits
|
||||
* card-foreground so item names and section titles stay legible in both modes. */
|
||||
.pd-chalkboard {
|
||||
position: relative;
|
||||
background-color: hsl(var(--card));
|
||||
background-image:
|
||||
radial-gradient(70% 60% at 12% 8%, hsl(var(--primary) / 0.10) 0%, transparent 60%),
|
||||
radial-gradient(60% 55% at 90% 92%, hsl(var(--accent) / 0.08) 0%, transparent 60%);
|
||||
color: hsl(var(--card-foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--pd-radius);
|
||||
box-shadow: 0 16px 48px -18px hsl(var(--primary) / 0.28);
|
||||
}
|
||||
.pd-chalk-rule {
|
||||
border-color: hsl(var(--border));
|
||||
border-style: solid;
|
||||
}
|
||||
.pd-chalk-muted {
|
||||
color: hsl(var(--muted-foreground));
|
||||
.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);
|
||||
}
|
||||
|
||||
/* --- Hours: today highlight -------------------------------------------- */
|
||||
.pd-hours-today {
|
||||
background-color: hsl(var(--accent) / 0.16);
|
||||
color: hsl(var(--accent-foreground));
|
||||
border-left: 3px solid hsl(var(--accent));
|
||||
border-radius: 0 9999px 9999px 0;
|
||||
padding-left: 0.75rem;
|
||||
.brush-underline {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* --- Location pin ------------------------------------------------------- */
|
||||
.pd-pin {
|
||||
color: hsl(var(--accent));
|
||||
.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;
|
||||
}
|
||||
|
||||
/* --- Soft badge (timeline markers) ------------------------------------- */
|
||||
.pd-stamp {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 9999px;
|
||||
background-color: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
border: 3px solid hsl(var(--card));
|
||||
box-shadow: 0 8px 20px -8px hsl(var(--accent) / 0.55);
|
||||
font-family: var(--font-heading, var(--pd-hand-fallback));
|
||||
font-size: 1.05rem;
|
||||
text-align: center;
|
||||
line-height: 1.05;
|
||||
transition: transform 200ms cubic-bezier(.22,1,.36,1);
|
||||
}
|
||||
.pd-stamp:hover {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
.pd-stamp-sm {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: none;
|
||||
.font-display {
|
||||
font-family: var(--font-heading, "Caveat Brush", "Sacramento", "Brush Script MT", cursive);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* --- Brush divider draw-in --------------------------------------------- */
|
||||
.pd-doodle-draw path,
|
||||
.pd-doodle-draw ellipse,
|
||||
.pd-doodle-draw circle {
|
||||
stroke-dasharray: 260;
|
||||
stroke-dashoffset: 260;
|
||||
animation: pd-draw 1.6s ease forwards;
|
||||
.font-body {
|
||||
font-family: var(--font-body, "Nunito", "Quicksand", "Helvetica Neue", system-ui, sans-serif);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
@keyframes pd-draw {
|
||||
to { stroke-dashoffset: 0; }
|
||||
.font-mono {
|
||||
font-family: var(--font-mono, "JetBrains Mono", "Fira Code", ui-monospace, monospace);
|
||||
}
|
||||
|
||||
/* --- Gentle atmosphere: slow wash drift (CSS-first signature) ----------- */
|
||||
@keyframes pd-breathe {
|
||||
0%, 100% { transform: scale(1); opacity: 0.9; }
|
||||
50% { transform: scale(1.05); opacity: 1; }
|
||||
}
|
||||
.pd-breathe {
|
||||
animation: pd-breathe 8s ease-in-out infinite;
|
||||
@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;
|
||||
}
|
||||
|
||||
/* --- Motion safety: static fallback ------------------------------------ */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.pd-tag,
|
||||
.pd-stamp { transition: none; }
|
||||
.pd-tag:hover,
|
||||
.pd-stamp:hover { transform: none; }
|
||||
.pd-breathe { animation: none; }
|
||||
.pd-doodle-draw path,
|
||||
.pd-doodle-draw ellipse,
|
||||
.pd-doodle-draw circle {
|
||||
stroke-dasharray: none;
|
||||
stroke-dashoffset: 0;
|
||||
animation: none;
|
||||
}
|
||||
.animate-pastel-shimmer,
|
||||
.animate-breathe,
|
||||
.pastel-pill {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.pastel-pill:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Menu list rhythm (complex child-combinator selector) --------------- */
|
||||
.pd-menu-list { margin-top: 1rem; }
|
||||
.pd-menu-list > li + li { margin-top: 1.25rem; }
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
<section data-block="pastel-dream:bakers_note" class="py-11 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-3xl px-4">
|
||||
<div class="pd-card relative overflow-hidden p-8 text-center md:p-12">
|
||||
<div class="pointer-events-none absolute -right-16 -top-16 h-48 w-48 rounded-full bg-accent/15 pd-breathe" aria-hidden="true"></div>
|
||||
<div class="pointer-events-none absolute -bottom-20 -left-12 h-56 w-56 rounded-full bg-primary/10 pd-breathe" aria-hidden="true"></div>
|
||||
<div class="relative">
|
||||
{% if kicker %}<p class="pd-hand text-2xl text-accent">{{ kicker }}</p>{% endif %}
|
||||
{% if body %}<div class="pd-body mx-auto mt-4 max-w-xl text-lg text-foreground [&_strong]:text-primary">{{ body|safe }}</div>{% endif %}
|
||||
{% if signatureName %}<p class="pd-display mt-6 text-2xl text-primary">{{ signatureName }}</p>{% endif %}
|
||||
{% if signatureRole %}<p class="pd-body text-sm text-muted-foreground">{{ signatureRole }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1,30 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Baker's Note",
|
||||
"description": "A signed note from the baker in sweet display type over a watercolor blob",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kicker": {
|
||||
"type": "string",
|
||||
"title": "Kicker",
|
||||
"default": "A note from the kitchen",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"title": "Note",
|
||||
"description": "Rich text note",
|
||||
"x-editor": "richtext"
|
||||
},
|
||||
"signatureName": {
|
||||
"type": "string",
|
||||
"title": "Signature Name",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"signatureRole": {
|
||||
"type": "string",
|
||||
"title": "Signature Role",
|
||||
"x-editor": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
# Pastel Dream theme block definitions (codeless .bnp, WO-TF-023).
|
||||
# Only true persona furniture stays a theme block. Everything that duplicated a
|
||||
# builtin (navbar, hero, quote, testimonial, feature-grid, footer, gallery,
|
||||
# menu, divider ...) is now a template override of the builtin key under
|
||||
# templates/overrides/pastel-dream/ — not a block here.
|
||||
#
|
||||
# Keys are fully qualified (pastel-dream:<key>): the codeless loader registers
|
||||
# definitions verbatim (no PluginBlockRegistry prefixing in the codeless path).
|
||||
blocks:
|
||||
- key: pastel-dream:seasonal_specials
|
||||
title: Seasonal Specials
|
||||
description: This season's featured bakes in a soft card grid — name, note, price and a sweet badge.
|
||||
category: content
|
||||
schema: seasonal_specials.schema.json
|
||||
template: seasonal_specials.ninjatpl
|
||||
- key: pastel-dream:cake_gallery
|
||||
title: Cake Gallery
|
||||
description: A scalloped showcase of cakes with flavour, price and an order link per cake.
|
||||
category: content
|
||||
schema: cake_gallery.schema.json
|
||||
template: cake_gallery.ninjatpl
|
||||
- key: pastel-dream:order_ribbon
|
||||
title: Order Ribbon
|
||||
description: A rounded call-to-order ribbon with a pickup note and a soft pill button.
|
||||
category: content
|
||||
schema: order_ribbon.schema.json
|
||||
template: order_ribbon.ninjatpl
|
||||
- key: pastel-dream:bakers_note
|
||||
title: Baker's Note
|
||||
description: A signed note from the baker in sweet display type over a watercolor blob.
|
||||
category: content
|
||||
schema: bakers_note.schema.json
|
||||
template: bakers_note.ninjatpl
|
||||
@ -1,23 +0,0 @@
|
||||
<section data-block="pastel-dream:cake_gallery" class="py-11 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
<div class="text-center">
|
||||
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||
{% if intro %}<p class="pd-body mx-auto mt-3 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||
</div>
|
||||
{% if cakes %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{% for cake in cakes %}
|
||||
<figure class="pd-frame overflow-hidden flex flex-col">
|
||||
{% if cake.image %}<div class="relative w-full overflow-hidden aspect-square pd-torn-bottom bg-secondary">{% img cake.image alt=cake.name|default:"Cake" class="block h-full w-full object-cover" %}</div>{% endif %}
|
||||
<figcaption class="p-5 md:p-6">
|
||||
{% if cake.name %}<h3 class="pd-display text-xl text-primary">{{ cake.name }}</h3>{% endif %}
|
||||
{% if cake.flavour %}<p class="pd-hand text-base text-accent">{{ cake.flavour }}</p>{% endif %}
|
||||
{% if cake.price %}<p class="pd-mono mt-1.5 text-lg text-accent tabular-nums">{% if currency %}{{ currency }}{% endif %}{{ cake.price }}</p>{% endif %}
|
||||
{% if cake.orderUrl %}<a href="{{ cake.orderUrl }}" class="pd-tag pd-tag--ghost mt-4 text-sm">Order this cake<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||
</figcaption>
|
||||
</figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@ -1,39 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Cake Gallery",
|
||||
"description": "A scalloped showcase of cakes with flavour, price and an order link per cake",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heading": {
|
||||
"type": "string",
|
||||
"title": "Heading",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"intro": {
|
||||
"type": "string",
|
||||
"title": "Intro",
|
||||
"x-editor": "textarea"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "Currency Symbol",
|
||||
"default": "$",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"cakes": {
|
||||
"type": "array",
|
||||
"title": "Cakes",
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image": { "type": "string", "title": "Image", "x-editor": "media" },
|
||||
"name": { "type": "string", "title": "Name", "x-editor": "text" },
|
||||
"flavour": { "type": "string", "title": "Flavour", "x-editor": "text" },
|
||||
"price": { "type": "string", "title": "Price", "x-editor": "text" },
|
||||
"orderUrl": { "type": "string", "title": "Order Link", "x-editor": "text" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
<section data-block="pastel-dream:order_ribbon" class="py-11 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-5xl px-4">
|
||||
<div class="pd-chalkboard relative overflow-hidden p-8 md:p-12">
|
||||
<div class="relative flex flex-col items-start gap-6 md:flex-row md:items-center md:justify-between">
|
||||
<div class="max-w-2xl">
|
||||
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
|
||||
{% if message %}<p class="pd-body mt-3 text-lg text-muted-foreground">{{ message }}</p>{% endif %}
|
||||
{% if pickupNote %}<p class="pd-hand mt-3 text-xl text-accent">{{ pickupNote }}</p>{% endif %}
|
||||
</div>
|
||||
{% if ctaLabel %}
|
||||
<div class="shrink-0">
|
||||
<a href="{{ ctaUrl|default:"/contact" }}" class="pd-tag text-base">{{ ctaLabel }}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1,37 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Order Ribbon",
|
||||
"description": "A rounded call-to-order ribbon with a pickup note and a soft pill button",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heading": {
|
||||
"type": "string",
|
||||
"title": "Heading",
|
||||
"default": "Order for the weekend",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"title": "Message",
|
||||
"x-editor": "textarea"
|
||||
},
|
||||
"pickupNote": {
|
||||
"type": "string",
|
||||
"title": "Pickup Note",
|
||||
"description": "Small line about lead time or pickup",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"ctaLabel": {
|
||||
"type": "string",
|
||||
"title": "Button Label",
|
||||
"default": "Place an order",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"ctaUrl": {
|
||||
"type": "string",
|
||||
"title": "Button Link",
|
||||
"default": "/contact",
|
||||
"x-editor": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
<section data-block="pastel-dream:seasonal_specials" class="py-11 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
<div class="max-w-2xl">
|
||||
{% if kicker %}<p class="pd-hand text-xl text-accent">{{ kicker }}</p>{% endif %}
|
||||
{% if heading %}<h2 class="pd-display mt-1 text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||
{% if intro %}<p class="pd-body mt-3 text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||
</div>
|
||||
{% if items %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{% for item in items %}
|
||||
<article class="pd-card overflow-hidden flex flex-col">
|
||||
{% if item.image %}<div class="relative w-full overflow-hidden aspect-[4/3] bg-secondary">{% img item.image alt=item.name|default:"Seasonal special" class="block h-full w-full object-cover" %}</div>{% endif %}
|
||||
<div class="p-5 md:p-6">
|
||||
<h3 class="pd-display text-xl text-primary">{{ item.name }}</h3>
|
||||
<div class="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1.5">
|
||||
{% if item.price %}<span class="pd-mono text-lg text-accent tabular-nums">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
|
||||
{% if item.badge %}<span class="pd-hand inline-flex rounded-full bg-accent/15 px-3 py-0.5 text-sm text-accent">{{ item.badge }}</span>{% endif %}
|
||||
</div>
|
||||
{% if item.note %}<p class="pd-body mt-3 text-sm text-muted-foreground">{{ item.note }}</p>{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@ -1,46 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Seasonal Specials",
|
||||
"description": "This season's featured bakes in a soft card grid",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kicker": {
|
||||
"type": "string",
|
||||
"title": "Kicker",
|
||||
"description": "Small sweet line above the heading",
|
||||
"default": "This season",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"heading": {
|
||||
"type": "string",
|
||||
"title": "Heading",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"intro": {
|
||||
"type": "string",
|
||||
"title": "Intro",
|
||||
"x-editor": "textarea"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "Currency Symbol",
|
||||
"default": "$",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"title": "Specials",
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image": { "type": "string", "title": "Image", "x-editor": "media" },
|
||||
"name": { "type": "string", "title": "Name", "x-editor": "text" },
|
||||
"note": { "type": "string", "title": "Note", "x-editor": "textarea" },
|
||||
"price": { "type": "string", "title": "Price", "x-editor": "text" },
|
||||
"badge": { "type": "string", "title": "Badge", "x-editor": "text" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
button_override.go
Normal file
@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
)
|
||||
|
||||
// PastelButtonBlock renders buttons as soft pills (9999px radius, tinted
|
||||
// shadow, 2px hover lift, cubic-bezier(.22,1,.36,1) easing).
|
||||
// Content shape: {text, href, variant}
|
||||
func PastelButtonBlock(ctx context.Context, content map[string]any) string {
|
||||
data := PastelButtonData{
|
||||
Text: getStringOr(content, "text", "Continue"),
|
||||
Href: safeLink(getString(content, "href")),
|
||||
Variant: getStringOr(content, "variant", "primary"),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = pastelButtonComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// PastelButtonData drives the button override.
|
||||
type PastelButtonData struct {
|
||||
Text string
|
||||
Href string
|
||||
Variant string
|
||||
}
|
||||
8
button_override.templ
Normal file
@ -0,0 +1,8 @@
|
||||
package main
|
||||
|
||||
// pastelButtonComponent renders the pill button override.
|
||||
templ pastelButtonComponent(data PastelButtonData) {
|
||||
<a href={ templ.SafeURL(data.Href) } class="pastel-pill" data-block-type="button" data-variant={ data.Variant }>
|
||||
{ data.Text }
|
||||
</a>
|
||||
}
|
||||
80
button_override_templ.go
Normal file
@ -0,0 +1,80 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// pastelButtonComponent renders the pill button override.
|
||||
func pastelButtonComponent(data PastelButtonData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.Href))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 5, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"pastel-pill\" data-block-type=\"button\" data-variant=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Variant)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 5, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 6, Col: 13}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
29
card_override.go
Normal file
@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
)
|
||||
|
||||
// PastelCardBlock renders cards with the soft 20px radius, blush-tinted
|
||||
// shadow, and optional watercolor border. The body field accepts pre-rendered
|
||||
// HTML so other blocks can be composed inside.
|
||||
// Content shape: {title, body, footer}
|
||||
func PastelCardBlock(ctx context.Context, content map[string]any) string {
|
||||
data := PastelCardData{
|
||||
Title: getString(content, "title"),
|
||||
Body: getString(content, "body"),
|
||||
Footer: getString(content, "footer"),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = pastelCardComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// PastelCardData drives the card override.
|
||||
type PastelCardData struct {
|
||||
Title string
|
||||
Body string
|
||||
Footer string
|
||||
}
|
||||
20
card_override.templ
Normal file
@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
// pastelCardComponent renders the soft pastel card.
|
||||
templ pastelCardComponent(data PastelCardData) {
|
||||
<div class="pastel-card p-6" data-block-type="card" style="border-radius: 20px;">
|
||||
if data.Title != "" {
|
||||
<h3 class="font-display text-2xl mb-3" style="color: hsl(var(--card-foreground));">{ data.Title }</h3>
|
||||
}
|
||||
if data.Body != "" {
|
||||
<div class="font-body" style="color: hsl(var(--card-foreground) / 0.85); line-height: 1.75;">
|
||||
@templ.Raw(data.Body)
|
||||
</div>
|
||||
}
|
||||
if data.Footer != "" {
|
||||
<div class="mt-6 pt-4 border-t font-body text-sm" style="border-color: hsl(var(--border)); color: hsl(var(--muted-foreground));">
|
||||
@templ.Raw(data.Footer)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
92
card_override_templ.go
Normal file
@ -0,0 +1,92 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// pastelCardComponent renders the soft pastel card.
|
||||
func pastelCardComponent(data PastelCardData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"pastel-card p-6\" data-block-type=\"card\" style=\"border-radius: 20px;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Title != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h3 class=\"font-display text-2xl mb-3\" style=\"color: hsl(var(--card-foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 7, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.Body != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"font-body\" style=\"color: hsl(var(--card-foreground) / 0.85); line-height: 1.75;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Body).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if data.Footer != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"mt-6 pt-4 border-t font-body text-sm\" style=\"border-color: hsl(var(--border)); color: hsl(var(--muted-foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Footer).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
40
cozy_footer.go
Normal file
@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// CozyFooterMeta defines the footer block.
|
||||
var CozyFooterMeta = blocks.BlockMeta{
|
||||
Key: "cozy-footer",
|
||||
Title: "Cozy Footer",
|
||||
Description: "Newsletter signup, closing affirmation, menu column, and social pills.",
|
||||
Source: "pastel-dream",
|
||||
Category: blocks.CategoryNavigation,
|
||||
}
|
||||
|
||||
// CozyFooterBlock renders the footer.
|
||||
// Content shape: {showSignup, affirmation, menuName, social[]}
|
||||
func CozyFooterBlock(ctx context.Context, content map[string]any) string {
|
||||
data := CozyFooterData{
|
||||
ShowSignup: getBoolish(content, "showSignup", true),
|
||||
Affirmation: getStringOr(content, "affirmation", "Be gentle with yourself today."),
|
||||
MenuName: getString(content, "menuName"),
|
||||
Social: getStringSlice(content, "social"),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = cozyFooterComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// CozyFooterData is the rendered view of the cozy-footer block.
|
||||
type CozyFooterData struct {
|
||||
ShowSignup bool
|
||||
Affirmation string
|
||||
MenuName string
|
||||
Social []string
|
||||
}
|
||||
42
cozy_footer.templ
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
// cozyFooterComponent renders the cozy footer with newsletter signup,
|
||||
// a closing affirmation, an optional menu column, and social pill links.
|
||||
templ cozyFooterComponent(data CozyFooterData) {
|
||||
<section class="relative py-16 md:py-20 overflow-hidden bg-watercolor-mint" data-block="pastel-dream:cozy-footer">
|
||||
// Decorative watercolor blob, top right
|
||||
<svg class="absolute -top-8 -right-8 w-64 h-64 -z-0" viewBox="0 0 300 300" aria-hidden="true">
|
||||
<path d="M150,30 C220,30 270,90 270,150 C270,220 200,270 140,270 C80,270 30,210 30,150 C30,90 80,30 150,30 Z" fill="hsl(var(--primary) / 0.16)"></path>
|
||||
</svg>
|
||||
<div class="relative z-10 max-w-4xl mx-auto px-4 text-center">
|
||||
<p class="font-display text-3xl mb-6" style="color: hsl(var(--foreground));">
|
||||
{ data.Affirmation }
|
||||
</p>
|
||||
if data.ShowSignup {
|
||||
<form class="flex flex-col sm:flex-row gap-3 max-w-md mx-auto mb-8" data-pastel-signup="true" onsubmit="event.preventDefault();">
|
||||
<label class="sr-only" for="pastel-newsletter-email">Email</label>
|
||||
<input
|
||||
id="pastel-newsletter-email"
|
||||
type="email"
|
||||
placeholder="you@calm.day"
|
||||
class="flex-1 px-5 py-3 font-body rounded-full"
|
||||
style="background-color: hsl(var(--input)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--border));"
|
||||
/>
|
||||
<button type="submit" class="pastel-pill">Stay in touch</button>
|
||||
</form>
|
||||
}
|
||||
if len(data.Social) > 0 {
|
||||
<div class="flex justify-center gap-3 flex-wrap mt-6">
|
||||
for _, href := range data.Social {
|
||||
<a href={ templ.SafeURL(href) } class="px-4 py-2 font-body text-xs uppercase tracking-[0.2em] rounded-full" style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border));">
|
||||
Visit
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<p class="font-mono text-xs mt-10" style="color: hsl(var(--muted-foreground));">
|
||||
Pastel Dream · with care
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
94
cozy_footer_templ.go
Normal file
@ -0,0 +1,94 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// cozyFooterComponent renders the cozy footer with newsletter signup,
|
||||
// a closing affirmation, an optional menu column, and social pill links.
|
||||
func cozyFooterComponent(data CozyFooterData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"relative py-16 md:py-20 overflow-hidden bg-watercolor-mint\" data-block=\"pastel-dream:cozy-footer\"><svg class=\"absolute -top-8 -right-8 w-64 h-64 -z-0\" viewBox=\"0 0 300 300\" aria-hidden=\"true\"><path d=\"M150,30 C220,30 270,90 270,150 C270,220 200,270 140,270 C80,270 30,210 30,150 C30,90 80,30 150,30 Z\" fill=\"hsl(var(--primary) / 0.16)\"></path></svg><div class=\"relative z-10 max-w-4xl mx-auto px-4 text-center\"><p class=\"font-display text-3xl mb-6\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Affirmation)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `cozy_footer.templ`, Line: 13, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.ShowSignup {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<form class=\"flex flex-col sm:flex-row gap-3 max-w-md mx-auto mb-8\" data-pastel-signup=\"true\" onsubmit=\"event.preventDefault();\"><label class=\"sr-only\" for=\"pastel-newsletter-email\">Email</label> <input id=\"pastel-newsletter-email\" type=\"email\" placeholder=\"you@calm.day\" class=\"flex-1 px-5 py-3 font-body rounded-full\" style=\"background-color: hsl(var(--input)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--border));\"> <button type=\"submit\" class=\"pastel-pill\">Stay in touch</button></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if len(data.Social) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"flex justify-center gap-3 flex-wrap mt-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, href := range data.Social {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `cozy_footer.templ`, Line: 31, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"px-4 py-2 font-body text-xs uppercase tracking-[0.2em] rounded-full\" style=\"background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border));\">Visit</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"font-mono text-xs mt-10\" style=\"color: hsl(var(--muted-foreground));\">Pastel Dream · with care</p></div></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
187
email_wrapper.templ
Normal file
@ -0,0 +1,187 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
// PastelEmailWrapper wraps body content in the Pastel Dream branded email
|
||||
// frame: cream paper background, watercolor blob watermark, 560px centered
|
||||
// frame, Caveat Brush masthead, Nunito body, mint CTA pill, unsubscribe and a
|
||||
// closing affirmation in the footer.
|
||||
func PastelEmailWrapper(body string, emailCtx templates.EmailContext) string {
|
||||
var buf bytes.Buffer
|
||||
_ = pastelEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// emailColorOr falls back to the supplied default when the EmailContext has
|
||||
// not been populated with the preset color (e.g. for raw previews).
|
||||
// All defaults pull from the blush-morning palette, expressed as hex because
|
||||
// email clients cannot resolve CSS custom properties.
|
||||
func emailColorOr(value, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func pastelEmailBg(c templates.EmailColors) string {
|
||||
// blush-morning background: hsl(25 60% 98%) ≈ #fdf7f1
|
||||
return emailColorOr(c.Background, "#fdf7f1")
|
||||
}
|
||||
|
||||
func pastelEmailCard(c templates.EmailColors) string {
|
||||
return emailColorOr(c.Card, "#ffffff")
|
||||
}
|
||||
|
||||
func pastelEmailForeground(c templates.EmailColors) string {
|
||||
// hsl(340 20% 22%) ≈ #432e36
|
||||
return emailColorOr(c.Foreground, "#432e36")
|
||||
}
|
||||
|
||||
func pastelEmailMutedFg(c templates.EmailColors) string {
|
||||
// hsl(340 12% 48%) ≈ #80707a
|
||||
return emailColorOr(c.MutedForeground, "#80707a")
|
||||
}
|
||||
|
||||
func pastelEmailPrimary(c templates.EmailColors) string {
|
||||
// hsl(350 65% 72%) ≈ #e9a1ad
|
||||
return emailColorOr(c.Primary, "#e9a1ad")
|
||||
}
|
||||
|
||||
func pastelEmailPrimaryFg(c templates.EmailColors) string {
|
||||
return emailColorOr(c.PrimaryForeground, "#432e36")
|
||||
}
|
||||
|
||||
func pastelEmailBorder(c templates.EmailColors) string {
|
||||
return emailColorOr(c.Border, "#f0d8de")
|
||||
}
|
||||
|
||||
// pastelEmailTemplate is the Pastel Dream email body template.
|
||||
templ pastelEmailTemplate(emailCtx templates.EmailContext, body string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta name="x-apple-disable-message-reformatting"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<title>{ emailCtx.SiteSettings.SiteName }</title>
|
||||
<style type="text/css">
|
||||
body, table, td, p, a, li {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.pastel-email-frame {
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
.pastel-email-frame {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.pastel-pad {
|
||||
padding-left: 24px !important;
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Nunito', 'Quicksand', Helvetica, Arial, sans-serif;", pastelEmailBg(emailCtx.Colors)) }>
|
||||
if emailCtx.PreviewText != "" {
|
||||
<div style="display: none; max-height: 0; overflow: hidden;">
|
||||
{ emailCtx.PreviewText }
|
||||
</div>
|
||||
}
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("background-color: %s;", pastelEmailBg(emailCtx.Colors)) }>
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 12px; position: relative;">
|
||||
<!-- Watermark blob (top-right). Inline SVG so any client that supports it renders the wash. -->
|
||||
<div style="position: absolute; top: 0; right: 0; width: 200px; height: 200px; opacity: 0.35; pointer-events: none;">
|
||||
<svg viewBox="0 0 200 200" width="200" height="200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d={ "M100,20 C160,20 180,80 180,120 C180,170 130,180 90,180 C40,180 20,140 20,100 C20,60 50,20 100,20 Z" } fill={ pastelEmailPrimary(emailCtx.Colors) }></path>
|
||||
</svg>
|
||||
</div>
|
||||
<table role="presentation" class="pastel-email-frame" width="560" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 560px; background-color: %s; border-radius: 20px; border: 1px solid %s;", pastelEmailCard(emailCtx.Colors), pastelEmailBorder(emailCtx.Colors)) }>
|
||||
<!-- Masthead -->
|
||||
<tr>
|
||||
<td align="center" class="pastel-pad" style={ fmt.Sprintf("padding: 36px 40px 8px; border-bottom: 1px solid %s;", pastelEmailBorder(emailCtx.Colors)) }>
|
||||
if emailCtx.SiteSettings.LogoURL != "" {
|
||||
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } style="max-height: 56px; width: auto; display: block;"/>
|
||||
} else if emailCtx.SiteSettings.SiteName != "" {
|
||||
<h1 style={ fmt.Sprintf("margin: 0; font-family: 'Caveat Brush', 'Sacramento', cursive; font-size: 36px; font-weight: 400; color: %s;", pastelEmailForeground(emailCtx.Colors)) }>
|
||||
{ emailCtx.SiteSettings.SiteName }
|
||||
</h1>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Body -->
|
||||
<tr>
|
||||
<td class="pastel-pad" style={ fmt.Sprintf("padding: 32px 40px 24px; color: %s; font-size: 16px; line-height: 1.75;", pastelEmailForeground(emailCtx.Colors)) }>
|
||||
@templ.Raw(body)
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td class="pastel-pad" align="center" style={ fmt.Sprintf("padding: 24px 40px 36px; border-top: 1px solid %s;", pastelEmailBorder(emailCtx.Colors)) }>
|
||||
<p style={ fmt.Sprintf("margin: 0 0 12px; font-family: 'Caveat Brush', 'Sacramento', cursive; font-size: 18px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)) }>
|
||||
Be gentle with yourself today.
|
||||
</p>
|
||||
if emailCtx.SiteSettings.SiteURL != "" {
|
||||
<p style={ fmt.Sprintf("margin: 0 0 12px; font-size: 13px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)) }>
|
||||
<a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ fmt.Sprintf("color: %s; text-decoration: none;", pastelEmailPrimary(emailCtx.Colors)) }>
|
||||
{ emailCtx.SiteSettings.SiteURL }
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
if emailCtx.UnsubscribeURL != "" {
|
||||
<p style={ fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)) }>
|
||||
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", pastelEmailMutedFg(emailCtx.Colors)) }>
|
||||
Unsubscribe
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
// pastelEmailPillStyle returns the inline CSS for the mint-tinted CTA pill
|
||||
// that templates may use inside the body slot. The wrapper exposes it as a
|
||||
// reusable token because the body is opaque pre-rendered HTML.
|
||||
func pastelEmailPillStyle(c templates.EmailColors) string {
|
||||
primary := pastelEmailPrimary(c)
|
||||
primaryFg := pastelEmailPrimaryFg(c)
|
||||
return fmt.Sprintf(
|
||||
"display: inline-block; padding: 12px 28px; border-radius: 9999px; background-color: %s; color: %s; font-family: 'Nunito', Helvetica, Arial, sans-serif; font-weight: 600; text-decoration: none;",
|
||||
primary,
|
||||
primaryFg,
|
||||
)
|
||||
}
|
||||
450
email_wrapper_templ.go
Normal file
@ -0,0 +1,450 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
// PastelEmailWrapper wraps body content in the Pastel Dream branded email
|
||||
// frame: cream paper background, watercolor blob watermark, 560px centered
|
||||
// frame, Caveat Brush masthead, Nunito body, mint CTA pill, unsubscribe and a
|
||||
// closing affirmation in the footer.
|
||||
func PastelEmailWrapper(body string, emailCtx templates.EmailContext) string {
|
||||
var buf bytes.Buffer
|
||||
_ = pastelEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// emailColorOr falls back to the supplied default when the EmailContext has
|
||||
// not been populated with the preset color (e.g. for raw previews).
|
||||
// All defaults pull from the blush-morning palette, expressed as hex because
|
||||
// email clients cannot resolve CSS custom properties.
|
||||
func emailColorOr(value, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func pastelEmailBg(c templates.EmailColors) string {
|
||||
// blush-morning background: hsl(25 60% 98%) ≈ #fdf7f1
|
||||
return emailColorOr(c.Background, "#fdf7f1")
|
||||
}
|
||||
|
||||
func pastelEmailCard(c templates.EmailColors) string {
|
||||
return emailColorOr(c.Card, "#ffffff")
|
||||
}
|
||||
|
||||
func pastelEmailForeground(c templates.EmailColors) string {
|
||||
// hsl(340 20% 22%) ≈ #432e36
|
||||
return emailColorOr(c.Foreground, "#432e36")
|
||||
}
|
||||
|
||||
func pastelEmailMutedFg(c templates.EmailColors) string {
|
||||
// hsl(340 12% 48%) ≈ #80707a
|
||||
return emailColorOr(c.MutedForeground, "#80707a")
|
||||
}
|
||||
|
||||
func pastelEmailPrimary(c templates.EmailColors) string {
|
||||
// hsl(350 65% 72%) ≈ #e9a1ad
|
||||
return emailColorOr(c.Primary, "#e9a1ad")
|
||||
}
|
||||
|
||||
func pastelEmailPrimaryFg(c templates.EmailColors) string {
|
||||
return emailColorOr(c.PrimaryForeground, "#432e36")
|
||||
}
|
||||
|
||||
func pastelEmailBorder(c templates.EmailColors) string {
|
||||
return emailColorOr(c.Border, "#f0d8de")
|
||||
}
|
||||
|
||||
// pastelEmailTemplate is the Pastel Dream email body template.
|
||||
func pastelEmailTemplate(emailCtx templates.EmailContext, body string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 73, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><style type=\"text/css\">\n\t\t\tbody, table, td, p, a, li {\n\t\t\t\t-webkit-text-size-adjust: 100%;\n\t\t\t\t-ms-text-size-adjust: 100%;\n\t\t\t}\n\t\t\ttable, td {\n\t\t\t\tmso-table-lspace: 0pt;\n\t\t\t\tmso-table-rspace: 0pt;\n\t\t\t}\n\t\t\timg {\n\t\t\t\t-ms-interpolation-mode: bicubic;\n\t\t\t\tborder: 0;\n\t\t\t\theight: auto;\n\t\t\t\tline-height: 100%;\n\t\t\t\toutline: none;\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\t\t\tbody {\n\t\t\t\tmargin: 0 !important;\n\t\t\t\tpadding: 0 !important;\n\t\t\t\twidth: 100% !important;\n\t\t\t}\n\t\t\t.pastel-email-frame {\n\t\t\t\tmax-width: 560px;\n\t\t\t\tmargin: 0 auto;\n\t\t\t}\n\t\t\t@media only screen and (max-width: 600px) {\n\t\t\t\t.pastel-email-frame {\n\t\t\t\t\twidth: 100% !important;\n\t\t\t\t\tmax-width: 100% !important;\n\t\t\t\t}\n\t\t\t\t.pastel-pad {\n\t\t\t\t\tpadding-left: 24px !important;\n\t\t\t\t\tpadding-right: 24px !important;\n\t\t\t\t}\n\t\t\t}\n\t\t</style></head><body style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Nunito', 'Quicksand', Helvetica, Arial, sans-serif;", pastelEmailBg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 112, Col: 172}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if emailCtx.PreviewText != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display: none; max-height: 0; overflow: hidden;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 115, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s;", pastelEmailBg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 118, Col: 161}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><tr><td align=\"center\" style=\"padding: 40px 12px; position: relative;\"><!-- Watermark blob (top-right). Inline SVG so any client that supports it renders the wash. --><div style=\"position: absolute; top: 0; right: 0; width: 200px; height: 200px; opacity: 0.35; pointer-events: none;\"><svg viewBox=\"0 0 200 200\" width=\"200\" height=\"200\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\"><path d=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("M100,20 C160,20 180,80 180,120 C180,170 130,180 90,180 C40,180 20,140 20,100 C20,60 50,20 100,20 Z")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 124, Col: 117}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" fill=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(pastelEmailPrimary(emailCtx.Colors))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 124, Col: 162}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"></path></svg></div><table role=\"presentation\" class=\"pastel-email-frame\" width=\"560\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("max-width: 560px; background-color: %s; border-radius: 20px; border: 1px solid %s;", pastelEmailCard(emailCtx.Colors), pastelEmailBorder(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 127, Col: 289}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"><!-- Masthead --><tr><td align=\"center\" class=\"pastel-pad\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 36px 40px 8px; border-bottom: 1px solid %s;", pastelEmailBorder(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 130, Col: 156}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if emailCtx.SiteSettings.LogoURL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.LogoURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 132, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" alt=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.SiteName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 132, Col: 88}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" style=\"max-height: 56px; width: auto; display: block;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if emailCtx.SiteSettings.SiteName != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<h1 style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-family: 'Caveat Brush', 'Sacramento', cursive; font-size: 36px; font-weight: 400; color: %s;", pastelEmailForeground(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 134, Col: 184}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 135, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td></tr><!-- Body --><tr><td class=\"pastel-pad\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 32px 40px 24px; color: %s; font-size: 16px; line-height: 1.75;", pastelEmailForeground(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 142, Col: 164}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(body).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td></tr><!-- Footer --><tr><td class=\"pastel-pad\" align=\"center\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 24px 40px 36px; border-top: 1px solid %s;", pastelEmailBorder(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 148, Col: 154}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\"><p style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0 0 12px; font-family: 'Caveat Brush', 'Sacramento', cursive; font-size: 18px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 149, Col: 168}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">Be gentle with yourself today.</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if emailCtx.SiteSettings.SiteURL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0 0 12px; font-size: 13px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 153, Col: 117}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 templ.SafeURL
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.SiteSettings.SiteURL))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 154, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none;", pastelEmailPrimary(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 154, Col: 160}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 155, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</a></p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if emailCtx.UnsubscribeURL != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", pastelEmailMutedFg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 160, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 templ.SafeURL
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.UnsubscribeURL))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 161, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", pastelEmailMutedFg(emailCtx.Colors)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 161, Col: 159}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">Unsubscribe</a></p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td></tr></table></td></tr></table></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// pastelEmailPillStyle returns the inline CSS for the mint-tinted CTA pill
|
||||
// that templates may use inside the body slot. The wrapper exposes it as a
|
||||
// reusable token because the body is opaque pre-rendered HTML.
|
||||
func pastelEmailPillStyle(c templates.EmailColors) string {
|
||||
primary := pastelEmailPrimary(c)
|
||||
primaryFg := pastelEmailPrimaryFg(c)
|
||||
return fmt.Sprintf(
|
||||
"display: inline-block; padding: 12px 28px; border-radius: 9999px; background-color: %s; color: %s; font-family: 'Nunito', Helvetica, Arial, sans-serif; font-weight: 600; text-decoration: none;",
|
||||
primary,
|
||||
primaryFg,
|
||||
)
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
49
embed.go
Normal file
@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
var assetsFS embed.FS
|
||||
|
||||
//go:embed schemas/*
|
||||
var schemasFS embed.FS
|
||||
|
||||
//go:embed presets.json
|
||||
var presetsData []byte
|
||||
|
||||
//go:embed fonts.json
|
||||
var fontsData []byte
|
||||
|
||||
//go:embed plugin.mod
|
||||
var pluginModBytes []byte
|
||||
|
||||
// Assets returns the embedded assets filesystem.
|
||||
func Assets() fs.FS {
|
||||
sub, _ := fs.Sub(assetsFS, "assets")
|
||||
return sub
|
||||
}
|
||||
|
||||
// Schemas returns the embedded schemas filesystem.
|
||||
func Schemas() fs.FS {
|
||||
sub, _ := fs.Sub(schemasFS, "schemas")
|
||||
return sub
|
||||
}
|
||||
|
||||
// AssetsHandler returns an http.Handler that serves the embedded assets.
|
||||
func AssetsHandler() http.Handler {
|
||||
return http.FileServer(http.FS(Assets()))
|
||||
}
|
||||
|
||||
// ThemePresets returns the embedded theme presets JSON.
|
||||
func ThemePresets() []byte {
|
||||
return presetsData
|
||||
}
|
||||
|
||||
// BundledFonts returns the embedded fonts manifest JSON.
|
||||
func BundledFonts() []byte {
|
||||
return fontsData
|
||||
}
|
||||
53
feature_grid_soft.go
Normal file
@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// FeatureGridSoftMeta defines the three-up soft feature grid.
|
||||
var FeatureGridSoftMeta = blocks.BlockMeta{
|
||||
Key: "feature-grid-soft",
|
||||
Title: "Feature Trio",
|
||||
Description: "Three-up card grid for services, offerings, or core values.",
|
||||
Source: "pastel-dream",
|
||||
Category: blocks.CategoryLayout,
|
||||
}
|
||||
|
||||
// FeatureGridSoftBlock renders the feature grid.
|
||||
// Content shape: {intro, items: [{icon, title, body}]}
|
||||
func FeatureGridSoftBlock(ctx context.Context, content map[string]any) string {
|
||||
rawItems := getSlice(content, "items")
|
||||
items := make([]FeatureSoftItem, 0, len(rawItems))
|
||||
for _, it := range rawItems {
|
||||
items = append(items, FeatureSoftItem{
|
||||
Icon: getString(it, "icon"),
|
||||
Title: getString(it, "title"),
|
||||
Body: getString(it, "body"),
|
||||
})
|
||||
}
|
||||
|
||||
data := FeatureGridSoftData{
|
||||
Intro: getString(content, "intro"),
|
||||
Items: items,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = featureGridSoftComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// FeatureGridSoftData is the rendered view of the feature-grid-soft block.
|
||||
type FeatureGridSoftData struct {
|
||||
Intro string
|
||||
Items []FeatureSoftItem
|
||||
}
|
||||
|
||||
// FeatureSoftItem is a single card in the trio.
|
||||
type FeatureSoftItem struct {
|
||||
Icon string
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
40
feature_grid_soft.templ
Normal file
@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
// featureGridSoftComponent renders the three-up grid. It reflows to 1 column
|
||||
// at 360px and 2-3 columns at 768px+, satisfying UAT §7.
|
||||
templ featureGridSoftComponent(data FeatureGridSoftData) {
|
||||
<section class="py-16 md:py-20" data-block="pastel-dream:feature-grid-soft">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
if data.Intro != "" {
|
||||
<div class="font-body text-center max-w-2xl mx-auto mb-12 text-lg" style="color: hsl(var(--foreground) / 0.78);">
|
||||
@templ.Raw(data.Intro)
|
||||
</div>
|
||||
}
|
||||
if len(data.Items) > 0 {
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
for _, item := range data.Items {
|
||||
<article class="pastel-card p-8 transition-transform" style="transition-timing-function: cubic-bezier(.22,1,.36,1); transition-duration: 240ms;">
|
||||
if item.Icon != "" {
|
||||
<div class="mb-4">
|
||||
<img src={ item.Icon } alt="" class="w-12 h-12 object-contain"/>
|
||||
</div>
|
||||
} else {
|
||||
<div class="mb-4 w-12 h-12 rounded-full" style="background-color: hsl(var(--accent) / 0.5);" aria-hidden="true"></div>
|
||||
}
|
||||
if item.Title != "" {
|
||||
<h3 class="font-display text-2xl mb-3" style="color: hsl(var(--foreground));">{ item.Title }</h3>
|
||||
}
|
||||
if item.Body != "" {
|
||||
<p class="font-body text-base" style="color: hsl(var(--foreground) / 0.78); line-height: 1.75;">{ item.Body }</p>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<div class="text-center font-body text-base" style="color: hsl(var(--muted-foreground));">
|
||||
<p>Add up to three cards to the trio.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
147
feature_grid_soft_templ.go
Normal file
@ -0,0 +1,147 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// featureGridSoftComponent renders the three-up grid. It reflows to 1 column
|
||||
// at 360px and 2-3 columns at 768px+, satisfying UAT §7.
|
||||
func featureGridSoftComponent(data FeatureGridSoftData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"py-16 md:py-20\" data-block=\"pastel-dream:feature-grid-soft\"><div class=\"max-w-6xl mx-auto px-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.Intro != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"font-body text-center max-w-2xl mx-auto mb-12 text-lg\" style=\"color: hsl(var(--foreground) / 0.78);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Intro).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if len(data.Items) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range data.Items {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<article class=\"pastel-card p-8 transition-transform\" style=\"transition-timing-function: cubic-bezier(.22,1,.36,1); transition-duration: 240ms;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.Icon != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"mb-4\"><img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `feature_grid_soft.templ`, Line: 19, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" alt=\"\" class=\"w-12 h-12 object-contain\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"mb-4 w-12 h-12 rounded-full\" style=\"background-color: hsl(var(--accent) / 0.5);\" aria-hidden=\"true\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if item.Title != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<h3 class=\"font-display text-2xl mb-3\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `feature_grid_soft.templ`, Line: 25, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if item.Body != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"font-body text-base\" style=\"color: hsl(var(--foreground) / 0.78); line-height: 1.75;\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Body)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `feature_grid_soft.templ`, Line: 28, Col: 115}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</article>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"text-center font-body text-base\" style=\"color: hsl(var(--muted-foreground));\"><p>Add up to three cards to the trio.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
19
fonts.json
@ -1,18 +1 @@
|
||||
[
|
||||
{
|
||||
"name": "Fraunces",
|
||||
"family": "Fraunces",
|
||||
"variants": [
|
||||
{ "weight": "400", "style": "normal", "file": "fonts/fraunces-latin-400.woff2" },
|
||||
{ "weight": "600", "style": "normal", "file": "fonts/fraunces-latin-600.woff2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Quicksand",
|
||||
"family": "Quicksand",
|
||||
"variants": [
|
||||
{ "weight": "400", "style": "normal", "file": "fonts/quicksand-400.woff2" },
|
||||
{ "weight": "700", "style": "normal", "file": "fonts/quicksand-700.woff2" }
|
||||
]
|
||||
}
|
||||
]
|
||||
[]
|
||||
|
||||
20
go.mod
Normal file
@ -0,0 +1,20 @@
|
||||
module git.dev.alexdunmow.com/block/themes/pastel-dream
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
git.dev.alexdunmow.com/block/core v0.14.1
|
||||
github.com/a-h/templ v0.3.1020
|
||||
)
|
||||
|
||||
require (
|
||||
connectrpc.com/connect v1.20.0 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
44
go.sum
Normal file
@ -0,0 +1,44 @@
|
||||
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||
git.dev.alexdunmow.com/block/core v0.14.0 h1:cO6QQQPndhxwh1zbDvDOzebwrFS8Ka3WsxD8JBJLw/Y=
|
||||
git.dev.alexdunmow.com/block/core v0.14.0/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
|
||||
git.dev.alexdunmow.com/block/core v0.14.1 h1:63b25yzWoqrhBd3wdDFhsJzyGNM99wYmdh4WkN35hDk=
|
||||
git.dev.alexdunmow.com/block/core v0.14.1/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
42
heading_override.go
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// PastelHeadingBlock renders headings with pastel-dream styling.
|
||||
// Caveat Brush display at H1/H2; Nunito for H3+; soft brush-stroke underline.
|
||||
// Content shape: {text, level: 1-6, textClass}
|
||||
func PastelHeadingBlock(ctx context.Context, content map[string]any) string {
|
||||
text := getString(content, "text")
|
||||
textClass := getString(content, "textClass")
|
||||
level := parseHeadingLevel(content)
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = pastelHeadingComponent(level, text, textClass).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// parseHeadingLevel reads level from various JSON-compatible types and clamps
|
||||
// the result to 1..6, defaulting to 2.
|
||||
func parseHeadingLevel(content map[string]any) int {
|
||||
if level, ok := content["level"].(float64); ok {
|
||||
l := int(level)
|
||||
if l >= 1 && l <= 6 {
|
||||
return l
|
||||
}
|
||||
}
|
||||
if level, ok := content["level"].(int); ok {
|
||||
if level >= 1 && level <= 6 {
|
||||
return level
|
||||
}
|
||||
}
|
||||
if level, ok := content["level"].(string); ok {
|
||||
if l, err := strconv.Atoi(level); err == nil && l >= 1 && l <= 6 {
|
||||
return l
|
||||
}
|
||||
}
|
||||
return 2
|
||||
}
|
||||
42
heading_override.templ
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
// pastelHeadingBaseClass returns the Tailwind size/weight class for a level.
|
||||
// H1 and H2 get the display face; H3+ uses the body face.
|
||||
func pastelHeadingBaseClass(level int) string {
|
||||
switch level {
|
||||
case 1:
|
||||
return "font-display text-5xl md:text-6xl leading-tight brush-underline"
|
||||
case 2:
|
||||
return "font-display text-4xl md:text-5xl leading-tight brush-underline"
|
||||
case 3:
|
||||
return "font-body text-3xl font-semibold tracking-tight"
|
||||
case 4:
|
||||
return "font-body text-2xl font-semibold"
|
||||
case 5:
|
||||
return "font-body text-xl font-medium"
|
||||
case 6:
|
||||
return "font-body text-lg font-medium"
|
||||
default:
|
||||
return "font-body text-3xl font-semibold tracking-tight"
|
||||
}
|
||||
}
|
||||
|
||||
// pastelHeadingComponent renders a heading with pastel-dream styling.
|
||||
templ pastelHeadingComponent(level int, text, textClass string) {
|
||||
switch level {
|
||||
case 1:
|
||||
<h1 class={ pastelHeadingBaseClass(1), textClass } style="color: hsl(var(--foreground));">{ text }</h1>
|
||||
case 2:
|
||||
<h2 class={ pastelHeadingBaseClass(2), textClass } style="color: hsl(var(--foreground));">{ text }</h2>
|
||||
case 3:
|
||||
<h3 class={ pastelHeadingBaseClass(3), textClass } style="color: hsl(var(--foreground));">{ text }</h3>
|
||||
case 4:
|
||||
<h4 class={ pastelHeadingBaseClass(4), textClass } style="color: hsl(var(--foreground));">{ text }</h4>
|
||||
case 5:
|
||||
<h5 class={ pastelHeadingBaseClass(5), textClass } style="color: hsl(var(--foreground));">{ text }</h5>
|
||||
case 6:
|
||||
<h6 class={ pastelHeadingBaseClass(6), textClass } style="color: hsl(var(--foreground));">{ text }</h6>
|
||||
default:
|
||||
<h2 class={ pastelHeadingBaseClass(2), textClass } style="color: hsl(var(--foreground));">{ text }</h2>
|
||||
}
|
||||
}
|
||||
312
heading_override_templ.go
Normal file
@ -0,0 +1,312 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// pastelHeadingBaseClass returns the Tailwind size/weight class for a level.
|
||||
// H1 and H2 get the display face; H3+ uses the body face.
|
||||
func pastelHeadingBaseClass(level int) string {
|
||||
switch level {
|
||||
case 1:
|
||||
return "font-display text-5xl md:text-6xl leading-tight brush-underline"
|
||||
case 2:
|
||||
return "font-display text-4xl md:text-5xl leading-tight brush-underline"
|
||||
case 3:
|
||||
return "font-body text-3xl font-semibold tracking-tight"
|
||||
case 4:
|
||||
return "font-body text-2xl font-semibold"
|
||||
case 5:
|
||||
return "font-body text-xl font-medium"
|
||||
case 6:
|
||||
return "font-body text-lg font-medium"
|
||||
default:
|
||||
return "font-body text-3xl font-semibold tracking-tight"
|
||||
}
|
||||
}
|
||||
|
||||
// pastelHeadingComponent renders a heading with pastel-dream styling.
|
||||
func pastelHeadingComponent(level int, text, textClass string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
switch level {
|
||||
case 1:
|
||||
var templ_7745c5c3_Var2 = []any{pastelHeadingBaseClass(1), textClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 28, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case 2:
|
||||
var templ_7745c5c3_Var5 = []any{pastelHeadingBaseClass(2), textClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h2 class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 30, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h2>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case 3:
|
||||
var templ_7745c5c3_Var8 = []any{pastelHeadingBaseClass(3), textClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 32, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case 4:
|
||||
var templ_7745c5c3_Var11 = []any{pastelHeadingBaseClass(4), textClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h4 class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var11).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 34, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h4>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case 5:
|
||||
var templ_7745c5c3_Var14 = []any{pastelHeadingBaseClass(5), textClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h5 class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var14).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 36, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</h5>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case 6:
|
||||
var templ_7745c5c3_Var17 = []any{pastelHeadingBaseClass(6), textClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h6 class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var17).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 38, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h6>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
default:
|
||||
var templ_7745c5c3_Var20 = []any{pastelHeadingBaseClass(2), textClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<h2 class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" style=\"color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 40, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</h2>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
82
helpers.go
Normal file
@ -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
|
||||
}
|
||||
378
manifest.yaml
@ -1,378 +0,0 @@
|
||||
# Pastel Dream theme — codeless .bnp manifest (Wave A, WO-TF-023).
|
||||
# Bakery / patisserie persona. Synthesized into manifest.pb by
|
||||
# `ninja plugin build` (no plugin.wasm).
|
||||
theme_presets: presets.json
|
||||
bundled_fonts: fonts.json
|
||||
master_pages: master_pages.json
|
||||
|
||||
required_icon_packs:
|
||||
- lucide
|
||||
- simple-icons
|
||||
|
||||
system_templates:
|
||||
- key: pastel-dream
|
||||
title: Pastel Dream
|
||||
description: "Soft bakery and patisserie theme — watercolor washes, scalloped edges, rounded menu cards and seasonal specials."
|
||||
|
||||
page_templates:
|
||||
- system: pastel-dream
|
||||
key: default
|
||||
title: Default
|
||||
description: "Header, main and footer over a soft watercolor wash."
|
||||
slots: [header, main, footer]
|
||||
- system: pastel-dream
|
||||
key: landing
|
||||
title: Landing
|
||||
description: "Hero, menu, story and an invitation to order."
|
||||
slots: [hero, menu, story, cta, footer]
|
||||
- system: pastel-dream
|
||||
key: article
|
||||
title: Article
|
||||
description: "Narrow reading column with a soft drop-cap for the journal and recipes."
|
||||
slots: [header, main, footer]
|
||||
- system: pastel-dream
|
||||
key: full-width
|
||||
title: Full Width
|
||||
description: "Edge-to-edge galleries and counter shots."
|
||||
slots: [header, main, footer]
|
||||
- system: pastel-dream
|
||||
key: blog-index
|
||||
title: Blog Index
|
||||
description: "Blog hero, featured posts and a post grid."
|
||||
slots: [header, featured, main, footer]
|
||||
- system: pastel-dream
|
||||
key: contact
|
||||
title: Contact
|
||||
description: "Order and enquiry form, hours and pickup details."
|
||||
slots: [header, main, footer]
|
||||
- system: pastel-dream
|
||||
key: auth
|
||||
title: Auth
|
||||
description: "Centered soft card for login, register and password reset."
|
||||
slots: [main, footer]
|
||||
|
||||
email_wrappers:
|
||||
- pastel-dream
|
||||
|
||||
template_overrides:
|
||||
- { template: pastel-dream, block: announcement-bar }
|
||||
- { template: pastel-dream, block: audio-embed }
|
||||
- { template: pastel-dream, block: auth-form }
|
||||
- { template: pastel-dream, block: author-bio }
|
||||
- { template: pastel-dream, block: author-bio-hero }
|
||||
- { template: pastel-dream, block: auth-status }
|
||||
- { template: pastel-dream, block: blog-hero }
|
||||
- { template: pastel-dream, block: breadcrumbs }
|
||||
- { template: pastel-dream, block: button }
|
||||
- { template: pastel-dream, block: card }
|
||||
- { template: pastel-dream, block: category-list }
|
||||
- { template: pastel-dream, block: contact-card }
|
||||
- { template: pastel-dream, block: contact-form }
|
||||
- { template: pastel-dream, block: countdown }
|
||||
- { template: pastel-dream, block: cta }
|
||||
- { template: pastel-dream, block: divider }
|
||||
- { template: pastel-dream, block: event-list }
|
||||
- { template: pastel-dream, block: faq }
|
||||
- { template: pastel-dream, block: featured-posts }
|
||||
- { template: pastel-dream, block: feature-grid }
|
||||
- { template: pastel-dream, block: footer }
|
||||
- { template: pastel-dream, block: gallery }
|
||||
- { template: pastel-dream, block: heading }
|
||||
- { template: pastel-dream, block: hero }
|
||||
- { template: pastel-dream, block: hours-location }
|
||||
- { template: pastel-dream, block: image }
|
||||
- { template: pastel-dream, block: logo-strip }
|
||||
- { template: pastel-dream, block: menu-list }
|
||||
- { template: pastel-dream, block: navbar }
|
||||
- { template: pastel-dream, block: newsletter-signup }
|
||||
- { template: pastel-dream, block: page-suggestions }
|
||||
- { template: pastel-dream, block: password-reset-form }
|
||||
- { template: pastel-dream, block: popular-posts }
|
||||
- { template: pastel-dream, block: post-hero }
|
||||
- { template: pastel-dream, block: post-metadata }
|
||||
- { template: pastel-dream, block: pricing }
|
||||
- { template: pastel-dream, block: quote }
|
||||
- { template: pastel-dream, block: related-posts }
|
||||
- { template: pastel-dream, block: rich-section }
|
||||
- { template: pastel-dream, block: rich-text }
|
||||
- { template: pastel-dream, block: sidebar-nav }
|
||||
- { template: pastel-dream, block: social-links }
|
||||
- { template: pastel-dream, block: stats }
|
||||
- { template: pastel-dream, block: team }
|
||||
- { template: pastel-dream, block: testimonial }
|
||||
- { template: pastel-dream, block: text }
|
||||
- { template: pastel-dream, block: timeline }
|
||||
- { template: pastel-dream, block: utility-bar }
|
||||
- { template: pastel-dream, block: video-embed }
|
||||
|
||||
css:
|
||||
input_css_append: |
|
||||
/* Pastel Dream theme styles — bakery / patisserie.
|
||||
*
|
||||
* Dual-mode via the 19 shadcn-style HSL token CSS variables (--background,
|
||||
* --foreground, --primary, --accent, --border, --muted, --card, ...) consumed
|
||||
* as hsl(var(--token)). No literal colors anywhere. Font families resolve ONLY
|
||||
* through the BlockNinja font variables (--font-heading, --font-body,
|
||||
* --font-mono) with the fallback stacks below — never hardcoded, so the admin
|
||||
* font picker keeps working.
|
||||
*
|
||||
* Motion tier: CSS-FIRST. Every animation is a gentle CSS drift and is disabled
|
||||
* under prefers-reduced-motion. No canvas, no JS showpiece.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--pd-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
||||
--pd-body-fallback: "Quicksand", "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--pd-hand-fallback: "Quicksand", "Comfortaa", "Segoe Print", cursive;
|
||||
--pd-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
|
||||
--pd-radius: 1.5rem;
|
||||
}
|
||||
|
||||
/* --- Bundled display faces ---------------------------------------------- */
|
||||
/* The host does not yet emit @font-face for bundled_fonts
|
||||
* (LoadedPlugin.BundledFonts is unconsumed), so the theme declares them
|
||||
* itself against the served asset paths (/templates/pastel-dream/fonts/...).
|
||||
* Presets set typography.fontHeading/fontBody to template:pastel-dream:<Family>
|
||||
* so --font-heading (Fraunces display + accents) and --font-body (Quicksand)
|
||||
* resolve to the bundled families instead of falling through to system fonts. */
|
||||
@font-face { font-family: 'Fraunces'; src: url('/templates/pastel-dream/fonts/fraunces-latin-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
||||
@font-face { font-family: 'Fraunces'; src: url('/templates/pastel-dream/fonts/fraunces-latin-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
|
||||
@font-face { font-family: 'Quicksand'; src: url('/templates/pastel-dream/fonts/quicksand-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
||||
@font-face { font-family: 'Quicksand'; src: url('/templates/pastel-dream/fonts/quicksand-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
|
||||
|
||||
/* --- Watercolor wash background ----------------------------------------- */
|
||||
/* The signature "morning haze": soft radial pastel washes tinted from the
|
||||
* active palette, drifting slowly. Reads as watercolor on cream in light mode
|
||||
* and as a dusk-lilac glow in dark mode — all from the same tokens. */
|
||||
body.pd-paper {
|
||||
background-color: hsl(var(--background));
|
||||
background-image:
|
||||
radial-gradient(60% 55% at 15% 12%, hsl(var(--primary) / 0.14) 0%, transparent 60%),
|
||||
radial-gradient(55% 50% at 85% 20%, hsl(var(--accent) / 0.12) 0%, transparent 58%),
|
||||
radial-gradient(65% 60% at 75% 85%, hsl(var(--secondary) / 0.40) 0%, transparent 62%),
|
||||
radial-gradient(50% 50% at 25% 90%, hsl(var(--primary) / 0.08) 0%, transparent 60%);
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
/* Guard against horizontal scroll on small screens from any off-canvas
|
||||
* chrome (mobile drawer). `clip` does not create a scroll container, so
|
||||
* it will not break the sticky navbar the way `hidden` can. */
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
/* --- Type roles --------------------------------------------------------- */
|
||||
.pd-display {
|
||||
font-family: var(--font-heading, var(--pd-heading-fallback));
|
||||
font-optical-sizing: auto;
|
||||
font-feature-settings: "liga" 1, "dlig" 1;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
.pd-body {
|
||||
font-family: var(--font-body, var(--pd-body-fallback));
|
||||
line-height: 1.7;
|
||||
}
|
||||
.pd-mono {
|
||||
font-family: var(--font-mono, var(--pd-mono-fallback));
|
||||
}
|
||||
/* Sweet accent lettering (badges, captions, section titles). Routed through
|
||||
* --font-heading with a rounded fallback: unassigned it renders Quicksand; if
|
||||
* the admin sets a heading font the accents follow it (3-slot model intended). */
|
||||
.pd-hand {
|
||||
font-family: var(--font-heading, var(--pd-hand-fallback));
|
||||
letter-spacing: 0.005em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* --- Brush underline ---------------------------------------------------- */
|
||||
.pd-doodle-underline {
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8 Q 22 1 44 6 T 88 6 T 118 6' fill='none' stroke='currentColor' stroke-width='4' stroke-linecap='round' opacity='0.35'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 100%;
|
||||
background-size: 100% 0.55em;
|
||||
padding-bottom: 0.16em;
|
||||
}
|
||||
|
||||
/* --- Drop-cap ----------------------------------------------------------- */
|
||||
.pd-dropcap > p:first-of-type::first-letter {
|
||||
font-family: var(--font-heading, var(--pd-heading-fallback));
|
||||
font-size: 3.8em;
|
||||
line-height: 0.8;
|
||||
float: left;
|
||||
padding: 0.06em 0.14em 0 0;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
/* --- Soft pill button (icing-smooth, lifts on hover) -------------------- */
|
||||
.pd-tag {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.7rem 1.5rem;
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
font-family: var(--font-body, var(--pd-body-fallback));
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
box-shadow: 0 8px 22px -8px hsl(var(--primary) / 0.45);
|
||||
transition: transform 220ms cubic-bezier(.22,1,.36,1), box-shadow 220ms cubic-bezier(.22,1,.36,1), background-color 220ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pd-tag:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 14px 30px -8px hsl(var(--primary) / 0.52);
|
||||
background-color: hsl(var(--primary) / 0.92);
|
||||
}
|
||||
.pd-tag:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.pd-tag--ghost {
|
||||
background-color: transparent;
|
||||
color: hsl(var(--primary));
|
||||
box-shadow: none;
|
||||
border: 1.5px solid hsl(var(--primary) / 0.45);
|
||||
}
|
||||
.pd-tag--ghost:hover {
|
||||
background-color: hsl(var(--primary) / 0.08);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* --- Scalloped edges (piped-icing / doily trim) ------------------------- */
|
||||
.pd-torn-top {
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 6 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 L120 12 L0 12 Z' fill='black'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 6 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 L120 12 L0 12 Z' fill='black'/%3E%3C/svg%3E");
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
.pd-torn-bottom {
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L120 0 L120 6 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 Z' fill='black'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L120 0 L120 6 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 Z' fill='black'/%3E%3C/svg%3E");
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
/* --- Surfaces (rounded-everything) ------------------------------------- */
|
||||
.pd-card {
|
||||
background-color: hsl(var(--card));
|
||||
color: hsl(var(--card-foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--pd-radius);
|
||||
box-shadow: 0 12px 40px -14px hsl(var(--primary) / 0.22), 0 4px 12px -6px hsl(var(--primary) / 0.10);
|
||||
}
|
||||
.pd-frame {
|
||||
border: 1px solid hsl(var(--border));
|
||||
background-color: hsl(var(--card));
|
||||
border-radius: var(--pd-radius);
|
||||
}
|
||||
|
||||
/* --- Menu card (soft pastel board for the menu-list override) ---------- */
|
||||
/* Not an inverted chalkboard: a soft card with a primary wash. Text inherits
|
||||
* card-foreground so item names and section titles stay legible in both modes. */
|
||||
.pd-chalkboard {
|
||||
position: relative;
|
||||
background-color: hsl(var(--card));
|
||||
background-image:
|
||||
radial-gradient(70% 60% at 12% 8%, hsl(var(--primary) / 0.10) 0%, transparent 60%),
|
||||
radial-gradient(60% 55% at 90% 92%, hsl(var(--accent) / 0.08) 0%, transparent 60%);
|
||||
color: hsl(var(--card-foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--pd-radius);
|
||||
box-shadow: 0 16px 48px -18px hsl(var(--primary) / 0.28);
|
||||
}
|
||||
.pd-chalk-rule {
|
||||
border-color: hsl(var(--border));
|
||||
border-style: solid;
|
||||
}
|
||||
.pd-chalk-muted {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* --- Hours: today highlight -------------------------------------------- */
|
||||
.pd-hours-today {
|
||||
background-color: hsl(var(--accent) / 0.16);
|
||||
color: hsl(var(--accent-foreground));
|
||||
border-left: 3px solid hsl(var(--accent));
|
||||
border-radius: 0 9999px 9999px 0;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
/* --- Location pin ------------------------------------------------------- */
|
||||
.pd-pin {
|
||||
color: hsl(var(--accent));
|
||||
}
|
||||
|
||||
/* --- Soft badge (timeline markers) ------------------------------------- */
|
||||
.pd-stamp {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 9999px;
|
||||
background-color: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
border: 3px solid hsl(var(--card));
|
||||
box-shadow: 0 8px 20px -8px hsl(var(--accent) / 0.55);
|
||||
font-family: var(--font-heading, var(--pd-hand-fallback));
|
||||
font-size: 1.05rem;
|
||||
text-align: center;
|
||||
line-height: 1.05;
|
||||
transition: transform 200ms cubic-bezier(.22,1,.36,1);
|
||||
}
|
||||
.pd-stamp:hover {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
.pd-stamp-sm {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* --- Brush divider draw-in --------------------------------------------- */
|
||||
.pd-doodle-draw path,
|
||||
.pd-doodle-draw ellipse,
|
||||
.pd-doodle-draw circle {
|
||||
stroke-dasharray: 260;
|
||||
stroke-dashoffset: 260;
|
||||
animation: pd-draw 1.6s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes pd-draw {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
/* --- Gentle atmosphere: slow wash drift (CSS-first signature) ----------- */
|
||||
@keyframes pd-breathe {
|
||||
0%, 100% { transform: scale(1); opacity: 0.9; }
|
||||
50% { transform: scale(1.05); opacity: 1; }
|
||||
}
|
||||
.pd-breathe {
|
||||
animation: pd-breathe 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* --- Motion safety: static fallback ------------------------------------ */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.pd-tag,
|
||||
.pd-stamp { transition: none; }
|
||||
.pd-tag:hover,
|
||||
.pd-stamp:hover { transform: none; }
|
||||
.pd-breathe { animation: none; }
|
||||
.pd-doodle-draw path,
|
||||
.pd-doodle-draw ellipse,
|
||||
.pd-doodle-draw circle {
|
||||
stroke-dasharray: none;
|
||||
stroke-dashoffset: 0;
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Menu list rhythm (complex child-combinator selector) --------------- */
|
||||
.pd-menu-list { margin-top: 1rem; }
|
||||
.pd-menu-list > li + li { margin-top: 1.25rem; }
|
||||
@ -1,45 +0,0 @@
|
||||
[
|
||||
{
|
||||
"key": "pastel-dream:default-master",
|
||||
"title": "Pastel Dream Default Master",
|
||||
"page_templates": ["default", "article", "full-width", "contact"],
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Petal & Crumb" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Add your page here" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning." }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pastel-dream:landing-master",
|
||||
"title": "Pastel Dream Landing Master",
|
||||
"page_templates": ["landing"],
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Petal & Crumb" }, "slot": "hero", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Hero", "content": { "slotName": "hero", "placeholder": "Drop a hero block" }, "slot": "hero", "sort_order": 1 },
|
||||
{ "block_key": "slot", "title": "Menu", "content": { "slotName": "menu", "placeholder": "Feature the menu" }, "slot": "menu", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Story", "content": { "slotName": "story", "placeholder": "Tell the story" }, "slot": "story", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Call To Action", "content": { "slotName": "cta", "placeholder": "Invite them to order" }, "slot": "cta", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning." }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pastel-dream:blog-master",
|
||||
"title": "Pastel Dream Blog Master",
|
||||
"page_templates": ["blog-index"],
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Petal & Crumb" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Featured", "content": { "slotName": "featured", "placeholder": "Featured posts" }, "slot": "featured", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Posts", "content": { "slotName": "main", "placeholder": "Post grid" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning." }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pastel-dream:auth-master",
|
||||
"title": "Pastel Dream Auth Master",
|
||||
"page_templates": ["auth"],
|
||||
"blocks": [
|
||||
{ "block_key": "slot", "title": "Auth Form", "content": { "slotName": "main", "placeholder": "Auth form" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Footer Note", "content": { "slotName": "footer", "placeholder": "Small print" }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -2,8 +2,11 @@
|
||||
name = "pastel-dream"
|
||||
display_name = "Pastel Dream"
|
||||
scope = "@themes"
|
||||
version = "0.5.0"
|
||||
description = "Soft bakery and patisserie theme — watercolor washes, scalloped edges, rounded menu cards, and seasonal specials in blush, berry, and lilac."
|
||||
version = "0.1.1"
|
||||
description = "Soft watercolor theme with blush, mint, and butter palettes for wellness, parenting, and spa brands."
|
||||
kind = "theme"
|
||||
categories = ["templates"]
|
||||
tags = ["pastel", "bakery", "patisserie", "cafe", "food", "soft", "rounded", "watercolor", "menu", "seasonal"]
|
||||
tags = ["pastel", "soft", "wellness", "parenting", "spa", "doula", "calm", "skincare", "mindfulness"]
|
||||
|
||||
[compatibility]
|
||||
block_core = ">=0.11.0 <0.12.0"
|
||||
|
||||
341
presets.json
@ -1,271 +1,88 @@
|
||||
[
|
||||
{
|
||||
"id": "morning-haze",
|
||||
"name": "Morning Haze",
|
||||
"description": "Blush rose on warm cream with a mint accent. The default patisserie counter at opening.",
|
||||
"id": "blush-morning",
|
||||
"name": "Blush Morning",
|
||||
"description": "Default — warm peach paper with rose-blush primary and sage trim.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:pastel-dream:Fraunces",
|
||||
"fontBody": "template:pastel-dream:Quicksand"
|
||||
},
|
||||
"mode": "light",
|
||||
"lightColors": {
|
||||
"background": "30 44% 97%",
|
||||
"foreground": "335 25% 22%",
|
||||
"card": "30 42% 99%",
|
||||
"cardForeground": "335 25% 22%",
|
||||
"popover": "30 42% 99%",
|
||||
"popoverForeground": "335 25% 22%",
|
||||
"primary": "340 58% 60%",
|
||||
"primaryForeground": "40 50% 98%",
|
||||
"secondary": "18 52% 92%",
|
||||
"secondaryForeground": "335 25% 24%",
|
||||
"muted": "30 30% 93%",
|
||||
"mutedForeground": "335 12% 42%",
|
||||
"accent": "160 38% 52%",
|
||||
"accentForeground": "160 40% 12%",
|
||||
"destructive": "0 70% 55%",
|
||||
"destructiveForeground": "40 50% 98%",
|
||||
"border": "24 30% 87%",
|
||||
"input": "24 30% 87%",
|
||||
"ring": "340 58% 60%"
|
||||
},
|
||||
"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": "290 22% 12%",
|
||||
"foreground": "320 22% 92%",
|
||||
"card": "290 20% 16%",
|
||||
"cardForeground": "320 22% 92%",
|
||||
"popover": "290 20% 16%",
|
||||
"popoverForeground": "320 22% 92%",
|
||||
"primary": "340 64% 72%",
|
||||
"primaryForeground": "290 30% 12%",
|
||||
"secondary": "290 18% 22%",
|
||||
"secondaryForeground": "320 22% 92%",
|
||||
"muted": "290 15% 20%",
|
||||
"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": "160 44% 60%",
|
||||
"accentForeground": "290 30% 12%",
|
||||
"destructive": "0 60% 60%",
|
||||
"destructiveForeground": "320 22% 92%",
|
||||
"border": "290 16% 26%",
|
||||
"input": "290 16% 24%",
|
||||
"ring": "340 64% 72%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "berry-cream",
|
||||
"name": "Berry Cream",
|
||||
"description": "Raspberry on ivory with a plum accent. Jam pots and fruit tarts.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:pastel-dream:Fraunces",
|
||||
"fontBody": "template:pastel-dream:Quicksand"
|
||||
},
|
||||
"lightColors": {
|
||||
"background": "350 40% 97%",
|
||||
"foreground": "345 30% 20%",
|
||||
"card": "350 38% 99%",
|
||||
"cardForeground": "345 30% 20%",
|
||||
"popover": "350 38% 99%",
|
||||
"popoverForeground": "345 30% 20%",
|
||||
"primary": "345 62% 52%",
|
||||
"primaryForeground": "350 40% 98%",
|
||||
"secondary": "350 46% 92%",
|
||||
"secondaryForeground": "345 30% 22%",
|
||||
"muted": "350 28% 93%",
|
||||
"mutedForeground": "345 14% 40%",
|
||||
"accent": "285 40% 55%",
|
||||
"accentForeground": "285 40% 12%",
|
||||
"destructive": "0 72% 52%",
|
||||
"destructiveForeground": "350 40% 98%",
|
||||
"border": "348 28% 87%",
|
||||
"input": "348 28% 87%",
|
||||
"ring": "345 62% 52%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "330 24% 11%",
|
||||
"foreground": "340 22% 92%",
|
||||
"card": "330 22% 15%",
|
||||
"cardForeground": "340 22% 92%",
|
||||
"popover": "330 22% 15%",
|
||||
"popoverForeground": "340 22% 92%",
|
||||
"primary": "345 68% 66%",
|
||||
"primaryForeground": "330 30% 11%",
|
||||
"secondary": "330 20% 21%",
|
||||
"secondaryForeground": "340 22% 92%",
|
||||
"muted": "330 16% 19%",
|
||||
"mutedForeground": "340 14% 70%",
|
||||
"accent": "285 46% 66%",
|
||||
"accentForeground": "330 30% 11%",
|
||||
"destructive": "0 62% 60%",
|
||||
"destructiveForeground": "340 22% 92%",
|
||||
"border": "330 16% 25%",
|
||||
"input": "330 16% 23%",
|
||||
"ring": "345 68% 66%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "matcha-cream",
|
||||
"name": "Matcha Cream",
|
||||
"description": "Soft sage on oat cream with an apricot accent. Green tea cakes and mochi.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:pastel-dream:Fraunces",
|
||||
"fontBody": "template:pastel-dream:Quicksand"
|
||||
},
|
||||
"lightColors": {
|
||||
"background": "44 36% 96%",
|
||||
"foreground": "150 22% 18%",
|
||||
"card": "44 34% 99%",
|
||||
"cardForeground": "150 22% 18%",
|
||||
"popover": "44 34% 99%",
|
||||
"popoverForeground": "150 22% 18%",
|
||||
"primary": "148 34% 42%",
|
||||
"primaryForeground": "44 40% 98%",
|
||||
"secondary": "150 30% 90%",
|
||||
"secondaryForeground": "150 22% 20%",
|
||||
"muted": "80 20% 91%",
|
||||
"mutedForeground": "150 12% 38%",
|
||||
"accent": "28 70% 56%",
|
||||
"accentForeground": "28 60% 12%",
|
||||
"destructive": "0 70% 52%",
|
||||
"destructiveForeground": "44 40% 98%",
|
||||
"border": "90 18% 84%",
|
||||
"input": "90 18% 84%",
|
||||
"ring": "148 34% 42%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "275 20% 12%",
|
||||
"foreground": "44 26% 91%",
|
||||
"card": "275 18% 16%",
|
||||
"cardForeground": "44 26% 91%",
|
||||
"popover": "275 18% 16%",
|
||||
"popoverForeground": "44 26% 91%",
|
||||
"primary": "148 40% 58%",
|
||||
"primaryForeground": "275 30% 12%",
|
||||
"secondary": "275 16% 22%",
|
||||
"secondaryForeground": "44 26% 91%",
|
||||
"muted": "275 13% 20%",
|
||||
"mutedForeground": "80 14% 68%",
|
||||
"accent": "28 74% 62%",
|
||||
"accentForeground": "275 30% 12%",
|
||||
"destructive": "0 60% 60%",
|
||||
"destructiveForeground": "44 26% 91%",
|
||||
"border": "275 14% 26%",
|
||||
"input": "275 14% 24%",
|
||||
"ring": "148 40% 58%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lavender-honey",
|
||||
"name": "Lavender Honey",
|
||||
"description": "Lilac on cream with a warm honey accent. Lavender shortbread and glaze.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:pastel-dream:Fraunces",
|
||||
"fontBody": "template:pastel-dream:Quicksand"
|
||||
},
|
||||
"lightColors": {
|
||||
"background": "40 40% 97%",
|
||||
"foreground": "270 24% 22%",
|
||||
"card": "40 38% 99%",
|
||||
"cardForeground": "270 24% 22%",
|
||||
"popover": "40 38% 99%",
|
||||
"popoverForeground": "270 24% 22%",
|
||||
"primary": "265 44% 58%",
|
||||
"primaryForeground": "40 50% 98%",
|
||||
"secondary": "265 38% 92%",
|
||||
"secondaryForeground": "270 24% 24%",
|
||||
"muted": "265 24% 93%",
|
||||
"mutedForeground": "270 12% 42%",
|
||||
"accent": "42 78% 56%",
|
||||
"accentForeground": "42 60% 12%",
|
||||
"destructive": "0 70% 55%",
|
||||
"destructiveForeground": "40 50% 98%",
|
||||
"border": "265 24% 87%",
|
||||
"input": "265 24% 87%",
|
||||
"ring": "265 44% 58%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "270 26% 12%",
|
||||
"foreground": "265 20% 92%",
|
||||
"card": "270 22% 16%",
|
||||
"cardForeground": "265 20% 92%",
|
||||
"popover": "270 22% 16%",
|
||||
"popoverForeground": "265 20% 92%",
|
||||
"primary": "265 54% 72%",
|
||||
"primaryForeground": "270 30% 12%",
|
||||
"secondary": "270 20% 22%",
|
||||
"secondaryForeground": "265 20% 92%",
|
||||
"muted": "270 16% 20%",
|
||||
"mutedForeground": "265 14% 70%",
|
||||
"accent": "42 80% 62%",
|
||||
"accentForeground": "270 30% 12%",
|
||||
"destructive": "0 60% 60%",
|
||||
"destructiveForeground": "265 20% 92%",
|
||||
"border": "270 16% 26%",
|
||||
"input": "270 16% 24%",
|
||||
"ring": "265 54% 72%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "peach-blossom",
|
||||
"name": "Peach Blossom",
|
||||
"description": "Warm peach on blossom cream with a rose accent. Stone-fruit galettes in spring.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:pastel-dream:Fraunces",
|
||||
"fontBody": "template:pastel-dream:Quicksand"
|
||||
},
|
||||
"lightColors": {
|
||||
"background": "26 50% 97%",
|
||||
"foreground": "15 30% 22%",
|
||||
"card": "26 46% 99%",
|
||||
"cardForeground": "15 30% 22%",
|
||||
"popover": "26 46% 99%",
|
||||
"popoverForeground": "15 30% 22%",
|
||||
"primary": "16 68% 56%",
|
||||
"primaryForeground": "26 50% 98%",
|
||||
"secondary": "14 58% 92%",
|
||||
"secondaryForeground": "15 30% 24%",
|
||||
"muted": "26 34% 93%",
|
||||
"mutedForeground": "15 16% 42%",
|
||||
"accent": "340 58% 62%",
|
||||
"accentForeground": "340 40% 12%",
|
||||
"destructive": "0 72% 52%",
|
||||
"destructiveForeground": "26 50% 98%",
|
||||
"border": "20 34% 87%",
|
||||
"input": "20 34% 87%",
|
||||
"ring": "16 68% 56%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "300 22% 12%",
|
||||
"foreground": "26 26% 91%",
|
||||
"card": "300 20% 16%",
|
||||
"cardForeground": "26 26% 91%",
|
||||
"popover": "300 20% 16%",
|
||||
"popoverForeground": "26 26% 91%",
|
||||
"primary": "16 74% 64%",
|
||||
"primaryForeground": "300 30% 12%",
|
||||
"secondary": "300 18% 22%",
|
||||
"secondaryForeground": "26 26% 91%",
|
||||
"muted": "300 15% 20%",
|
||||
"mutedForeground": "20 16% 70%",
|
||||
"accent": "340 60% 68%",
|
||||
"accentForeground": "300 30% 12%",
|
||||
"destructive": "0 60% 60%",
|
||||
"destructiveForeground": "26 26% 91%",
|
||||
"border": "300 15% 26%",
|
||||
"input": "300 15% 24%",
|
||||
"ring": "16 74% 64%"
|
||||
"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%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
preview.png
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 100 KiB |
187
register.go
Normal file
@ -0,0 +1,187 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
// wrap adapts a templ-returning render function to templates.TemplateFunc.
|
||||
// templ.Component already implements templates.HTMLComponent via Render.
|
||||
func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc {
|
||||
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
|
||||
return f(ctx, doc)
|
||||
}
|
||||
}
|
||||
|
||||
// Register is the plugin entry point. It registers the system template, page
|
||||
// templates, theme-owned blocks, built-in overrides, and the email wrapper.
|
||||
//
|
||||
// Order of operations matters: br.LoadSchemasFromFS MUST run before any
|
||||
// br.Register call so schema binding sees the blocks as they register.
|
||||
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
|
||||
// System template registration.
|
||||
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
|
||||
Key: "pastel-dream",
|
||||
Title: "Pastel Dream",
|
||||
Description: "Soft watercolor theme with blush, mint, and butter palettes for wellness, parenting, and spa brands.",
|
||||
})
|
||||
|
||||
// Page templates per spec §"Page templates".
|
||||
if err := tr.RegisterPageTemplate("pastel-dream", templates.PageTemplateMeta{
|
||||
Key: "default",
|
||||
Title: "Default",
|
||||
Description: "Soft layered hero, masthead nav, footer with newsletter.",
|
||||
Slots: []string{"header", "main", "footer"},
|
||||
}, wrap(RenderPastelDefault)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tr.RegisterPageTemplate("pastel-dream", templates.PageTemplateMeta{
|
||||
Key: "landing",
|
||||
Title: "Soft Landing",
|
||||
Description: "Watercolor hero, affirmation strip, CTA, footer.",
|
||||
Slots: []string{"hero", "main", "cta", "footer"},
|
||||
}, wrap(RenderPastelLanding)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tr.RegisterPageTemplate("pastel-dream", templates.PageTemplateMeta{
|
||||
Key: "article",
|
||||
Title: "Reading Room",
|
||||
Description: "Narrow, centered editorial column with side rail.",
|
||||
Slots: []string{"header", "main", "footer"},
|
||||
}, wrap(RenderPastelArticle)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tr.RegisterPageTemplate("pastel-dream", templates.PageTemplateMeta{
|
||||
Key: "full-width",
|
||||
Title: "Open Air",
|
||||
Description: "Edge-to-edge sections for galleries and seasonal looks.",
|
||||
Slots: []string{"header", "main", "footer"},
|
||||
}, wrap(RenderPastelFullWidth)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Schemas MUST load before blocks register so editor metadata binds.
|
||||
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Theme-owned blocks.
|
||||
br.Register(SoftNavbarMeta, SoftNavbarBlock)
|
||||
br.Register(WatercolorHeroMeta, WatercolorHeroBlock)
|
||||
br.Register(AffirmationMeta, AffirmationBlock)
|
||||
br.Register(TestimonialSoftMeta, TestimonialSoftBlock)
|
||||
br.Register(FeatureGridSoftMeta, FeatureGridSoftBlock)
|
||||
br.Register(CozyFooterMeta, CozyFooterBlock)
|
||||
|
||||
// Overrides for built-ins, only when this theme is active.
|
||||
br.RegisterTemplateOverride("pastel-dream", "heading", PastelHeadingBlock)
|
||||
br.RegisterTemplateOverride("pastel-dream", "text", PastelTextBlock)
|
||||
br.RegisterTemplateOverride("pastel-dream", "button", PastelButtonBlock)
|
||||
br.RegisterTemplateOverride("pastel-dream", "card", PastelCardBlock)
|
||||
|
||||
// Email wrapper.
|
||||
tr.RegisterEmailWrapper("pastel-dream", PastelEmailWrapper)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultMasterPages returns the master pages seeded on first plugin load.
|
||||
// Two definitions per spec §"Master pages":
|
||||
// - pastel-dream:default-master — used by default and article page templates
|
||||
// - pastel-dream:landing-master — used by the landing page template
|
||||
func DefaultMasterPages() []plugin.MasterPageDefinition {
|
||||
return []plugin.MasterPageDefinition{
|
||||
{
|
||||
Key: "pastel-dream:default-master",
|
||||
Title: "Pastel Dream Default Master",
|
||||
PageTemplates: []string{"default", "article"},
|
||||
Blocks: []plugin.MasterPageBlock{
|
||||
{
|
||||
BlockKey: "pastel-dream:soft-navbar",
|
||||
Title: "Soft Nav",
|
||||
Content: map[string]any{
|
||||
"menuName": "main",
|
||||
"logo": "Pastel Dream",
|
||||
},
|
||||
Slot: "header",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "slot",
|
||||
Title: "Main Slot",
|
||||
Content: map[string]any{
|
||||
"slotName": "main",
|
||||
"placeholder": "Page content",
|
||||
},
|
||||
Slot: "main",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "pastel-dream:cozy-footer",
|
||||
Title: "Cozy Footer",
|
||||
Content: map[string]any{
|
||||
"showSignup": "yes",
|
||||
"affirmation": "Be gentle with yourself today.",
|
||||
},
|
||||
Slot: "footer",
|
||||
SortOrder: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "pastel-dream:landing-master",
|
||||
Title: "Pastel Dream Landing Master",
|
||||
PageTemplates: []string{"landing"},
|
||||
Blocks: []plugin.MasterPageBlock{
|
||||
{
|
||||
BlockKey: "pastel-dream:watercolor-hero",
|
||||
Title: "Watercolor Hero",
|
||||
Content: map[string]any{
|
||||
"eyebrow": "new",
|
||||
"headline": "Soft starts",
|
||||
"body": "A gentle place to begin.",
|
||||
"ctaText": "Begin",
|
||||
},
|
||||
Slot: "hero",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "slot",
|
||||
Title: "Main Slot",
|
||||
Content: map[string]any{
|
||||
"slotName": "main",
|
||||
},
|
||||
Slot: "main",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "pastel-dream:affirmation",
|
||||
Title: "Affirmation Strip",
|
||||
Content: map[string]any{
|
||||
"quote": "You are doing enough.",
|
||||
"author": "Pastel Dream",
|
||||
},
|
||||
Slot: "cta",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "pastel-dream:cozy-footer",
|
||||
Title: "Cozy Footer",
|
||||
Content: map[string]any{
|
||||
"showSignup": "yes",
|
||||
},
|
||||
Slot: "footer",
|
||||
SortOrder: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
38
registration.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
// Registration is the compile-time plugin registration for the Pastel Dream theme.
|
||||
var Registration = plugin.PluginRegistration{
|
||||
Name: "pastel-dream",
|
||||
Version: plugin.ParseModVersion(pluginModBytes),
|
||||
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
|
||||
return Register(tr, br)
|
||||
},
|
||||
Assets: func() http.Handler { return AssetsHandler() },
|
||||
Schemas: func() fs.FS { return Schemas() },
|
||||
ThemePresets: func() []byte { return ThemePresets() },
|
||||
BundledFonts: func() []byte { return BundledFonts() },
|
||||
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
|
||||
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
|
||||
}
|
||||
|
||||
// ThemeCSSManifest returns the Pastel Dream CSS to be appended to the host Tailwind input.
|
||||
// This includes keyframes (pastel-shimmer, breathe), the --radius-soft variable,
|
||||
// watercolor utility classes, font-family fallback stacks, and reduced-motion guards.
|
||||
func ThemeCSSManifest() *plugin.CSSManifest {
|
||||
css, err := assetsFS.ReadFile("assets/css/pastel-dream.css")
|
||||
if err != nil {
|
||||
return &plugin.CSSManifest{}
|
||||
}
|
||||
return &plugin.CSSManifest{
|
||||
InputCSSAppend: string(css),
|
||||
}
|
||||
}
|
||||
29
schemas/affirmation.schema.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Affirmation Strip",
|
||||
"description": "Slow-shimmer band carrying a short affirmation in display type.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quote": {
|
||||
"type": "string",
|
||||
"title": "Affirmation",
|
||||
"description": "Short calming line (one or two sentences).",
|
||||
"default": "You are doing enough.",
|
||||
"x-editor": "textarea"
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"title": "Attribution",
|
||||
"description": "Optional source for the line.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"palette": {
|
||||
"type": "string",
|
||||
"title": "Palette",
|
||||
"description": "Which watercolor wash to use behind the strip.",
|
||||
"default": "blush",
|
||||
"enum": ["blush", "mint", "butter", "sky"],
|
||||
"x-editor": "select"
|
||||
}
|
||||
}
|
||||
}
|
||||
39
schemas/cozy-footer.schema.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Cozy Footer",
|
||||
"description": "Newsletter signup, small affirmation line, menu, and social links.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"showSignup": {
|
||||
"type": "string",
|
||||
"title": "Newsletter signup",
|
||||
"description": "Toggle the inline email signup form.",
|
||||
"default": "yes",
|
||||
"enum": ["yes", "no"],
|
||||
"x-editor": "select"
|
||||
},
|
||||
"affirmation": {
|
||||
"type": "string",
|
||||
"title": "Closing line",
|
||||
"description": "Short closing affirmation displayed beneath the signup.",
|
||||
"default": "Be gentle with yourself today.",
|
||||
"x-editor": "textarea"
|
||||
},
|
||||
"menuName": {
|
||||
"type": "string",
|
||||
"title": "Menu",
|
||||
"description": "Name of the menu rendered in the footer column.",
|
||||
"x-editor": "menu-select"
|
||||
},
|
||||
"social": {
|
||||
"type": "array",
|
||||
"title": "Social links",
|
||||
"description": "Outbound social links shown as small pills.",
|
||||
"x-editor": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"x-editor": "link"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
schemas/feature-grid-soft.schema.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Feature Trio",
|
||||
"description": "Three-up card grid for services, offerings, or values.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"intro": {
|
||||
"type": "string",
|
||||
"title": "Intro",
|
||||
"description": "Optional introduction above the cards.",
|
||||
"x-editor": "richtext"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"title": "Cards",
|
||||
"description": "List of feature cards. Three is the recommended count.",
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"title": "Icon",
|
||||
"description": "Decorative icon or small illustration.",
|
||||
"x-editor": "media"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Title",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"title": "Body",
|
||||
"x-editor": "textarea"
|
||||
}
|
||||
},
|
||||
"required": ["title"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
schemas/soft-navbar.schema.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Soft Navbar",
|
||||
"description": "Rounded pill nav with a watercolor blob behind the logo wordmark.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"title": "Logo Wordmark",
|
||||
"description": "Plain-text wordmark displayed at the top-left.",
|
||||
"default": "Pastel Dream",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"menuName": {
|
||||
"type": "string",
|
||||
"title": "Menu",
|
||||
"description": "Name of the menu to render in the pill.",
|
||||
"default": "main",
|
||||
"x-editor": "menu-select"
|
||||
},
|
||||
"ctaText": {
|
||||
"type": "string",
|
||||
"title": "CTA Label",
|
||||
"description": "Optional pill button to the right of the menu.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"ctaHref": {
|
||||
"type": "string",
|
||||
"title": "CTA Link",
|
||||
"description": "Target URL or internal page link for the CTA pill.",
|
||||
"x-editor": "link"
|
||||
}
|
||||
}
|
||||
}
|
||||
40
schemas/testimonial-soft.schema.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Soft Testimonial",
|
||||
"description": "Deckle-edge card carrying a quote, attribution, optional avatar, and rating.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quote": {
|
||||
"type": "string",
|
||||
"title": "Quote",
|
||||
"description": "The testimonial body. Rich text.",
|
||||
"x-editor": "richtext"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name",
|
||||
"description": "Person being quoted.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"title": "Role",
|
||||
"description": "Their title, company, or context.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"avatar": {
|
||||
"type": "string",
|
||||
"title": "Avatar",
|
||||
"description": "Optional portrait or illustration.",
|
||||
"x-editor": "media"
|
||||
},
|
||||
"rating": {
|
||||
"type": "number",
|
||||
"title": "Rating",
|
||||
"description": "Star rating, 0 to 5.",
|
||||
"minimum": 0,
|
||||
"maximum": 5,
|
||||
"x-editor": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
43
schemas/watercolor-hero.schema.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 344 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 256 KiB |
184
seed/seed.json
@ -1,184 +0,0 @@
|
||||
{
|
||||
"demo": true,
|
||||
"settings": {
|
||||
"merge": {
|
||||
"site_title": "Petal & Crumb",
|
||||
"site_description": "A small patisserie baking cakes, pastries and seasonal treats."
|
||||
}
|
||||
},
|
||||
"media": [
|
||||
{ "id": "a1b2c3d4-0001-4a00-8000-000000000001", "file": "media/strawberry-tart.jpg", "alt": "Strawberry tart with vanilla custard and a glaze of fresh berries", "folder": "Petal & Crumb" },
|
||||
{ "id": "a1b2c3d4-0001-4a00-8000-000000000002", "file": "media/croissants.jpg", "alt": "Golden laminated croissants fresh from the oven", "folder": "Petal & Crumb" },
|
||||
{ "id": "a1b2c3d4-0001-4a00-8000-000000000003", "file": "media/lemon-loaf.jpg", "alt": "Lemon poppy loaf dusted with sugar, sliced", "folder": "Petal & Crumb" },
|
||||
{ "id": "a1b2c3d4-0001-4a00-8000-000000000004", "file": "media/sponge-cake.jpg", "alt": "Light sponge cake layered with cream", "folder": "Petal & Crumb" },
|
||||
{ "id": "a1b2c3d4-0001-4a00-8000-000000000005", "file": "media/fruit-galette.jpg", "alt": "Rustic fruit galette with berries and whipped cream", "folder": "Petal & Crumb" },
|
||||
{ "id": "a1b2c3d4-0001-4a00-8000-000000000006", "file": "media/bakery-interior.jpg", "alt": "The counter at Petal & Crumb, breads and pastries on display", "folder": "Petal & Crumb" }
|
||||
],
|
||||
"data_tables": [
|
||||
{
|
||||
"key": "contact_submissions",
|
||||
"name": "Contact Submissions",
|
||||
"description": "Enquiries and cake orders sent from the contact form.",
|
||||
"primary_key": "id",
|
||||
"columns": [
|
||||
{ "key": "name", "label": "Name", "type": "text", "required": true },
|
||||
{ "key": "email", "label": "Email", "type": "email", "required": true },
|
||||
{ "key": "reason", "label": "Reason", "type": "text", "required": false },
|
||||
{ "key": "message", "label": "Message", "type": "text", "required": true }
|
||||
]
|
||||
}
|
||||
],
|
||||
"menu_items": [
|
||||
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 0 },
|
||||
{ "menu": "main", "label": "Menu", "url": "/#menu", "sort_order": 1 },
|
||||
{ "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 2 },
|
||||
{ "menu": "main", "label": "Journal", "page_slug": "/blog", "sort_order": 3 },
|
||||
{ "menu": "main", "label": "Order", "page_slug": "/contact", "sort_order": 4 }
|
||||
],
|
||||
"pages": [
|
||||
{
|
||||
"slug": "/",
|
||||
"title": "Petal & Crumb",
|
||||
"template_key": "default",
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
|
||||
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 0, "content": { "template": "centered", "eyebrow": "Open from 8", "headline": "Cakes and pastries, baked fresh each morning", "subheadline": "A small patisserie on the corner. Everything is made by hand the day you eat it.", "ctaPrimary": { "text": "See the menu", "url": "/#menu" }, "ctaSecondary": { "text": "Order a cake", "url": "/contact" }, "minHeight": "50vh" } },
|
||||
{ "block_key": "pastel-dream:seasonal_specials", "title": "Seasonal Specials", "slot": "main", "sort_order": 1, "content": { "kicker": "This week", "heading": "On the counter now", "intro": "A short list of what we are proud of this week. It changes with the season.", "currency": "$", "items": [
|
||||
{ "image": "media:a1b2c3d4-0001-4a00-8000-000000000001", "name": "Strawberry tart", "note": "Vanilla custard, fresh strawberries, a thin glaze.", "price": "8.00", "badge": "New" },
|
||||
{ "image": "media:a1b2c3d4-0001-4a00-8000-000000000002", "name": "Pistachio croissant", "note": "Laminated overnight, filled with pistachio cream.", "price": "6.50", "badge": "Popular" },
|
||||
{ "image": "media:a1b2c3d4-0001-4a00-8000-000000000003", "name": "Lemon poppy loaf", "note": "Soft, tangy, dusted with sugar. Sold by the slice.", "price": "5.00", "badge": "" }
|
||||
] } },
|
||||
{ "block_key": "menu-list", "title": "Menu", "slot": "main", "sort_order": 2, "content": { "heading": "The full menu", "intro": "Prices are for takeaway. We can box anything for a gift.", "currency": "$", "columns": 2, "sections": [
|
||||
{ "title": "Pastries", "items": [
|
||||
{ "name": "Butter croissant", "price": "5.00" },
|
||||
{ "name": "Pain au chocolat", "price": "5.50" },
|
||||
{ "name": "Almond croissant", "description": "Filled and topped with almond cream", "price": "6.50" },
|
||||
{ "name": "Morning bun", "description": "Orange and cinnamon", "price": "6.00", "badges": [{ "label": "V" }] }
|
||||
] },
|
||||
{ "title": "Cakes and slices", "items": [
|
||||
{ "name": "Victoria sponge", "description": "By the slice", "price": "7.00" },
|
||||
{ "name": "Chocolate torte", "description": "Flourless", "price": "8.00", "badges": [{ "label": "GF" }] },
|
||||
{ "name": "Carrot cake", "price": "7.50" },
|
||||
{ "name": "Custard tart", "price": "6.00" }
|
||||
] }
|
||||
] } },
|
||||
{ "block_key": "pastel-dream:order_ribbon", "title": "Order Ribbon", "slot": "main", "sort_order": 3, "content": { "heading": "Order a cake for the weekend", "message": "Birthdays, gatherings, or just because. Tell us the flavour and the date.", "pickupNote": "Two days notice for custom cakes.", "ctaLabel": "Place an order", "ctaUrl": "/contact" } },
|
||||
{ "block_key": "hours-location", "title": "Hours", "slot": "main", "sort_order": 4, "content": { "heading": "Come and see us", "address": "18 Rose Lane\nFremantle", "phone": "08 1234 5678", "hours": [
|
||||
{ "day": "Monday", "closed": true },
|
||||
{ "day": "Tuesday", "hours": "8:00 - 15:00" },
|
||||
{ "day": "Wednesday", "hours": "8:00 - 15:00" },
|
||||
{ "day": "Thursday", "hours": "8:00 - 15:00" },
|
||||
{ "day": "Friday", "hours": "8:00 - 16:00" },
|
||||
{ "day": "Saturday", "hours": "8:00 - 16:00" },
|
||||
{ "day": "Sunday", "hours": "9:00 - 14:00" }
|
||||
] } },
|
||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "address": "18 Rose Lane, Fremantle", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/about",
|
||||
"title": "About",
|
||||
"template_key": "default",
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
|
||||
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 0, "content": { "template": "centered", "eyebrow": "Our story", "headline": "One oven, one small room", "subheadline": "We opened with a single oven and a short menu. We kept it that way on purpose.", "minHeight": "auto" } },
|
||||
{ "block_key": "rich-section", "title": "Story", "slot": "main", "sort_order": 1, "content": { "eyebrow": "How we bake", "heading": "Made the same day", "body": "<p>We start before dawn. The pastry is laminated the night before and baked in the morning, so it reaches you the same day it is made.</p><p>The menu is short because we make everything ourselves. When something is out, it is out until the next bake.</p>", "mediaPosition": "right", "media": "media:a1b2c3d4-0001-4a00-8000-000000000006", "mediaAlt": "The counter at Petal & Crumb, breads and pastries on display" } },
|
||||
{ "block_key": "pastel-dream:bakers_note", "title": "Baker's Note", "slot": "main", "sort_order": 2, "content": { "kicker": "A note from the kitchen", "body": "<p>Thank you for buying from a small shop. Every order helps us keep baking by hand. We taste each batch before it reaches the counter, and we would rather sell out than cut a corner.</p>", "signatureName": "Cara Wells", "signatureRole": "Baker and owner" } },
|
||||
{ "block_key": "team", "title": "Team", "slot": "main", "sort_order": 3, "content": { "heading": "The people at the bench", "members": [
|
||||
{ "name": "Cara", "role": "Baker and owner", "bio": "Laminates the pastry and runs the morning bake." },
|
||||
{ "name": "Jonah", "role": "Pastry chef", "bio": "Owns the tarts and the seasonal fruit." },
|
||||
{ "name": "Noor", "role": "Front of house", "bio": "Boxes your cake and knows the regulars." }
|
||||
] } },
|
||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/blog",
|
||||
"title": "Journal",
|
||||
"template_key": "blog-index",
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
|
||||
{ "block_key": "blog-hero", "title": "Journal Hero", "slot": "featured", "sort_order": 0, "content": { "title": "The Journal", "subtitle": "Notes from the bench and the season." } },
|
||||
{ "block_key": "card", "title": "Post: Strawberries", "slot": "main", "sort_order": 0, "content": { "title": "Strawberries are in", "text": "The first good strawberries of the season arrived. Here is what we are making with them.", "link": "/blog/strawberries-are-in", "linkLabel": "Read on" } },
|
||||
{ "block_key": "card", "title": "Post: Lamination", "slot": "main", "sort_order": 1, "content": { "title": "Why we laminate overnight", "text": "Cold dough and slow rests make a better croissant. This is how we do it.", "link": "/blog/why-we-laminate-overnight", "linkLabel": "Read on" } },
|
||||
{ "block_key": "card", "title": "Post: Custom cakes", "slot": "main", "sort_order": 2, "content": { "title": "How to order a custom cake", "text": "Flavours, sizes and lead times, so your order arrives the way you pictured it.", "link": "/blog/how-to-order-a-custom-cake", "linkLabel": "Read on" } },
|
||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/blog/strawberries-are-in",
|
||||
"parent_slug": "/blog",
|
||||
"title": "Strawberries are in",
|
||||
"template_key": "article",
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
|
||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "Strawberries are in" } },
|
||||
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "<p>The first ripe strawberries came in this week. They are small, sweet, and they do not keep, so we use them fast. A grower two towns over drops a tray at the back door before we open, still cool from the morning.</p><p>We sort them by hand at the bench. The neat ones go on the tarts, where they sit in rows and catch the light. The soft ones are just as good, so they go into the jam pan and cook down for the sponge.</p><p>A strawberry tart is a simple thing done carefully. We blind bake the pastry until it is properly short, cool it, then pipe in vanilla custard the same morning. The fruit goes on last, with a thin brush of glaze so it shines without turning sticky.</p><p>The sponge is the other side of the same fruit. We fold cream and a spoon of that quick jam between two light layers, then a dusting of sugar on top. It is the cake we make when we want the strawberry to taste like summer rather than like a garnish.</p><p>Strawberries do not wait. They are best the day they are picked and a little tired by the next, so we do not hold them back or freeze a batch for later. What comes in is what we bake, and when it is gone it is gone.</p><p>That is the whole reason the menu changes. We would rather follow the fruit than force it. Next it will be the first stone fruit, and the tarts will turn gold instead of red.</p><p>If you have a favourite, tell us at the counter. We keep a short list of who asked for what, and we will put one aside when the season is at its best.</p><p>Come in early if you can. The tarts tend to go before lunch, and the sponge is usually cut by mid afternoon. There is always something on the counter, but the strawberries are worth the early trip.</p>" } },
|
||||
{ "block_key": "pastel-dream:cake_gallery", "title": "Cake Gallery", "slot": "main", "sort_order": 2, "content": { "heading": "What we made this week", "intro": "A few things that came out of the oven.", "currency": "$", "cakes": [
|
||||
{ "image": "media:a1b2c3d4-0001-4a00-8000-000000000001", "name": "Strawberry tart", "flavour": "Vanilla custard", "price": "8.00", "orderUrl": "/contact" },
|
||||
{ "image": "media:a1b2c3d4-0001-4a00-8000-000000000004", "name": "Strawberry sponge", "flavour": "Cream and jam", "price": "7.00", "orderUrl": "/contact" },
|
||||
{ "image": "media:a1b2c3d4-0001-4a00-8000-000000000005", "name": "Fruit galette", "flavour": "Seasonal", "price": "6.50", "orderUrl": "/contact" }
|
||||
] } },
|
||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/blog/why-we-laminate-overnight",
|
||||
"parent_slug": "/blog",
|
||||
"title": "Why we laminate overnight",
|
||||
"template_key": "article",
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
|
||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "Why we laminate overnight" } },
|
||||
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "<p>A croissant is butter and dough folded many times. The butter has to stay cold, or it melts into the dough and the layers are lost.</p><p>We fold in the afternoon and rest the dough in the fridge overnight. Cold and slow gives an even rise and a light crumb.</p><p>In the morning we shape and bake. That is why the croissants are ready when we open and not before.</p>" } },
|
||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/blog/how-to-order-a-custom-cake",
|
||||
"parent_slug": "/blog",
|
||||
"title": "How to order a custom cake",
|
||||
"template_key": "article",
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
|
||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "How to order a custom cake" } },
|
||||
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "<p>Tell us the date, the size, and the flavour. Send a photo if you have one in mind.</p><p>We need two days for most cakes and a week for larger ones. We will confirm the price before we start.</p><p>Pick up from the shop, or ask about local delivery.</p>" } },
|
||||
{ "block_key": "pastel-dream:order_ribbon", "title": "Order Ribbon", "slot": "main", "sort_order": 2, "content": { "heading": "Ready to order", "message": "Send us the details and we will reply with a quote.", "pickupNote": "Two days notice, a week for tiered cakes.", "ctaLabel": "Start an order", "ctaUrl": "/contact" } },
|
||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/contact",
|
||||
"title": "Order",
|
||||
"template_key": "contact",
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
|
||||
{ "block_key": "hours-location", "title": "Hours", "slot": "main", "sort_order": 0, "content": { "heading": "Find us", "address": "18 Rose Lane\nFremantle", "phone": "08 1234 5678", "hours": [
|
||||
{ "day": "Monday", "closed": true },
|
||||
{ "day": "Tuesday", "hours": "8:00 - 15:00" },
|
||||
{ "day": "Wednesday", "hours": "8:00 - 15:00" },
|
||||
{ "day": "Thursday", "hours": "8:00 - 15:00" },
|
||||
{ "day": "Friday", "hours": "8:00 - 16:00" },
|
||||
{ "day": "Saturday", "hours": "8:00 - 16:00" },
|
||||
{ "day": "Sunday", "hours": "9:00 - 14:00" }
|
||||
] } },
|
||||
{ "block_key": "contact-form", "title": "Contact Form", "slot": "main", "sort_order": 1, "content": { "heading": "Send us a note", "description": "Ordering a cake or asking a question? Leave the details and we will get back to you.", "submitLabel": "Send", "successMessage": "Thanks. We will reply soon.", "formConfig": { "targetTable": "contact_submissions" }, "fields": [
|
||||
{ "name": "name", "label": "Name", "type": "text", "required": true, "width": "half" },
|
||||
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
|
||||
{ "name": "reason", "label": "Reason", "type": "text", "required": false, "width": "full" },
|
||||
{ "name": "message", "label": "Message", "type": "textarea", "required": true, "width": "full" }
|
||||
] } },
|
||||
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/login",
|
||||
"title": "Login",
|
||||
"template_key": "auth",
|
||||
"blocks": [
|
||||
{ "block_key": "auth-form", "title": "Auth Form", "slot": "main", "sort_order": 0, "content": { "default_tab": "login", "show_social": false } },
|
||||
{ "block_key": "text", "title": "Footer Note", "slot": "footer", "sort_order": 0, "content": { "text": "<p>Members hear about seasonal bakes first.</p>" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
{
|
||||
"workflows": [
|
||||
{
|
||||
"key": "notify_admins_on_contact",
|
||||
"name": "Notify admins on contact",
|
||||
"description": "Email site admins when someone submits the contact or order form.",
|
||||
"enabled": true,
|
||||
"trigger": {
|
||||
"type": "row_inserted",
|
||||
"table": "contact_submissions"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"type": "email",
|
||||
"name": "Email admins",
|
||||
"template": "Form Submission Notification",
|
||||
"recipients": {
|
||||
"mode": "admins",
|
||||
"scope": "all"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
40
soft_navbar.go
Normal file
@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// SoftNavbarMeta defines metadata for the rounded pill navbar.
|
||||
var SoftNavbarMeta = blocks.BlockMeta{
|
||||
Key: "soft-navbar",
|
||||
Title: "Soft Navbar",
|
||||
Description: "Rounded pill navigation with a watercolor blob behind the logo.",
|
||||
Source: "pastel-dream",
|
||||
Category: blocks.CategoryNavigation,
|
||||
}
|
||||
|
||||
// SoftNavbarBlock renders the navbar.
|
||||
// Content shape: {logo: string, menuName: string, ctaText: string, ctaHref: string}
|
||||
func SoftNavbarBlock(ctx context.Context, content map[string]any) string {
|
||||
data := SoftNavbarData{
|
||||
Logo: getStringOr(content, "logo", "Pastel Dream"),
|
||||
MenuName: getString(content, "menuName"),
|
||||
CTAText: getString(content, "ctaText"),
|
||||
CTAHref: safeLink(getString(content, "ctaHref")),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = softNavbarComponent(data).Render(ctx, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// SoftNavbarData is the rendered view of the navbar block.
|
||||
type SoftNavbarData struct {
|
||||
Logo string
|
||||
MenuName string
|
||||
CTAText string
|
||||
CTAHref string
|
||||
}
|
||||
31
soft_navbar.templ
Normal file
@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
// softNavbarComponent renders a rounded pill navigation with watercolor wash
|
||||
// behind the logo wordmark.
|
||||
templ softNavbarComponent(data SoftNavbarData) {
|
||||
<nav class="w-full px-4 py-4" data-block="pastel-dream:soft-navbar" data-menu={ data.MenuName }>
|
||||
<div class="max-w-6xl mx-auto flex items-center justify-between gap-4 px-2 py-2 rounded-full bg-watercolor-blush" style="border-radius: 9999px;">
|
||||
<a href="/" class="font-display text-2xl tracking-tight px-4 py-2 rounded-full" style="color: hsl(var(--primary-foreground)); background-color: hsl(var(--primary) / 0.15);">
|
||||
{ data.Logo }
|
||||
</a>
|
||||
<div class="hidden md:flex items-center gap-6 font-body text-sm" data-pastel-menu={ data.MenuName }>
|
||||
<a href="/" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">Home</a>
|
||||
<a href="/about" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">About</a>
|
||||
<a href="/journal" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">Journal</a>
|
||||
<a href="/contact" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">Contact</a>
|
||||
</div>
|
||||
if data.CTAText != "" {
|
||||
<a href={ templ.SafeURL(data.CTAHref) } class="pastel-pill text-sm">
|
||||
{ data.CTAText }
|
||||
</a>
|
||||
}
|
||||
<button class="md:hidden p-2 rounded-full" aria-label="Open menu" style="color: hsl(var(--foreground));">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
117
soft_navbar_templ.go
Normal file
@ -0,0 +1,117 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
// softNavbarComponent renders a rounded pill navigation with watercolor wash
|
||||
// behind the logo wordmark.
|
||||
func softNavbarComponent(data SoftNavbarData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<nav class=\"w-full px-4 py-4\" data-block=\"pastel-dream:soft-navbar\" data-menu=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.MenuName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `soft_navbar.templ`, Line: 6, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><div class=\"max-w-6xl mx-auto flex items-center justify-between gap-4 px-2 py-2 rounded-full bg-watercolor-blush\" style=\"border-radius: 9999px;\"><a href=\"/\" class=\"font-display text-2xl tracking-tight px-4 py-2 rounded-full\" style=\"color: hsl(var(--primary-foreground)); background-color: hsl(var(--primary) / 0.15);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Logo)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `soft_navbar.templ`, Line: 9, Col: 15}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</a><div class=\"hidden md:flex items-center gap-6 font-body text-sm\" data-pastel-menu=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.MenuName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `soft_navbar.templ`, Line: 11, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><a href=\"/\" style=\"color: hsl(var(--foreground));\" class=\"hover:opacity-70 transition-opacity\">Home</a> <a href=\"/about\" style=\"color: hsl(var(--foreground));\" class=\"hover:opacity-70 transition-opacity\">About</a> <a href=\"/journal\" style=\"color: hsl(var(--foreground));\" class=\"hover:opacity-70 transition-opacity\">Journal</a> <a href=\"/contact\" style=\"color: hsl(var(--foreground));\" class=\"hover:opacity-70 transition-opacity\">Contact</a></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data.CTAText != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 templ.SafeURL
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.CTAHref))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `soft_navbar.templ`, Line: 18, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"pastel-pill text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.CTAText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `soft_navbar.templ`, Line: 19, Col: 19}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<button class=\"md:hidden p-2 rounded-full\" aria-label=\"Open menu\" style=\"color: hsl(var(--foreground));\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\"></line> <line x1=\"3\" y1=\"12\" x2=\"21\" y2=\"12\"></line> <line x1=\"3\" y1=\"18\" x2=\"21\" y2=\"18\"></line></svg></button></div></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
252
template.templ
Normal file
@ -0,0 +1,252 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/templates/bn"
|
||||
)
|
||||
|
||||
// PastelPageData carries the per-render page context.
|
||||
type PastelPageData struct {
|
||||
Title string
|
||||
Slots map[string]string
|
||||
ThemeMode string
|
||||
ThemeCSS string
|
||||
SiteSettings bn.SiteSettingsData
|
||||
PageMeta bn.PageMeta
|
||||
StructuredData string
|
||||
CSSHash string
|
||||
PageviewNonce string
|
||||
EngagementConfig bn.EngagementConfig
|
||||
}
|
||||
|
||||
// parsePastelPageData converts the wire-format doc map into a PastelPageData.
|
||||
// It is lenient: missing fields fall back to sensible defaults so empty pages
|
||||
// render without panicking.
|
||||
func parsePastelPageData(doc map[string]any) PastelPageData {
|
||||
title := "Untitled"
|
||||
if t, ok := doc["title"].(string); ok && t != "" {
|
||||
title = t
|
||||
}
|
||||
|
||||
slots := make(map[string]string)
|
||||
if s, ok := doc["slots"].(map[string]string); ok {
|
||||
slots = s
|
||||
}
|
||||
|
||||
themeCSS := ""
|
||||
if tc, ok := doc["theme_css"].(string); ok {
|
||||
themeCSS = tc
|
||||
}
|
||||
|
||||
structuredData := ""
|
||||
if sd, ok := doc["structured_data"].(string); ok {
|
||||
structuredData = sd
|
||||
}
|
||||
|
||||
cssHash := ""
|
||||
if ch, ok := doc["css_hash"].(string); ok {
|
||||
cssHash = ch
|
||||
}
|
||||
|
||||
pageviewNonce := ""
|
||||
if pn, ok := doc["pageview_nonce"].(string); ok {
|
||||
pageviewNonce = pn
|
||||
}
|
||||
|
||||
themeMode := "light"
|
||||
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
|
||||
themeMode = tm
|
||||
}
|
||||
|
||||
return PastelPageData{
|
||||
Title: title,
|
||||
Slots: slots,
|
||||
ThemeMode: themeMode,
|
||||
ThemeCSS: themeCSS,
|
||||
SiteSettings: bn.ParseSiteSettings(doc),
|
||||
PageMeta: bn.ParsePageMeta(doc),
|
||||
StructuredData: structuredData,
|
||||
CSSHash: cssHash,
|
||||
PageviewNonce: pageviewNonce,
|
||||
EngagementConfig: bn.ParseEngagementConfig(doc),
|
||||
}
|
||||
}
|
||||
|
||||
// PastelDefault is the standard pastel-dream page template — masthead, main
|
||||
// column, and footer. Used by both `default` and (with a slightly wider main)
|
||||
// the `full-width` template.
|
||||
templ PastelDefault(data PastelPageData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
})
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
@bn.AdminBypassBanner(data.SiteSettings)
|
||||
<header class="w-full">
|
||||
@templ.Raw(data.Slots["header"])
|
||||
</header>
|
||||
<main class="flex-grow w-full max-w-4xl mx-auto px-4 py-12">
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
@templ.Raw(main)
|
||||
} else {
|
||||
<div class="py-20 text-center font-body" style="color: hsl(var(--muted-foreground));">
|
||||
<p>This page is still resting. Add a block to begin.</p>
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
@templ.Raw(data.Slots["footer"])
|
||||
</footer>
|
||||
@bn.BodyEnd(data.SiteSettings)
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
// PastelLanding is the marketing landing layout — full-width hero, body,
|
||||
// affirmation CTA, footer.
|
||||
templ PastelLanding(data PastelPageData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
})
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
@bn.AdminBypassBanner(data.SiteSettings)
|
||||
<section class="w-full">
|
||||
@templ.Raw(data.Slots["hero"])
|
||||
</section>
|
||||
<main class="flex-grow w-full">
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
<div class="max-w-6xl mx-auto px-4 py-20">
|
||||
@templ.Raw(main)
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<section class="w-full">
|
||||
@templ.Raw(data.Slots["cta"])
|
||||
</section>
|
||||
<footer class="w-full mt-auto">
|
||||
@templ.Raw(data.Slots["footer"])
|
||||
</footer>
|
||||
@bn.BodyEnd(data.SiteSettings)
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
// PastelArticle is the narrow reading-room layout for editorial posts.
|
||||
templ PastelArticle(data PastelPageData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
})
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
@bn.AdminBypassBanner(data.SiteSettings)
|
||||
<header class="w-full">
|
||||
@templ.Raw(data.Slots["header"])
|
||||
</header>
|
||||
<main class="flex-grow w-full max-w-2xl mx-auto px-4 py-16">
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
<article class="font-body prose max-w-none" style="line-height: 1.75; color: hsl(var(--foreground));">
|
||||
@templ.Raw(main)
|
||||
</article>
|
||||
} else {
|
||||
<div class="py-20 text-center" style="color: hsl(var(--muted-foreground));">
|
||||
<p>This page is still resting. Add a block to begin.</p>
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
@templ.Raw(data.Slots["footer"])
|
||||
</footer>
|
||||
@bn.BodyEnd(data.SiteSettings)
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
// PastelFullWidth is the edge-to-edge layout for galleries and seasonal looks.
|
||||
templ PastelFullWidth(data PastelPageData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
})
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
@bn.AdminBypassBanner(data.SiteSettings)
|
||||
<header class="w-full">
|
||||
@templ.Raw(data.Slots["header"])
|
||||
</header>
|
||||
<main class="flex-grow w-full">
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
@templ.Raw(main)
|
||||
} else {
|
||||
<div class="max-w-4xl mx-auto py-20 px-4 text-center" style="color: hsl(var(--muted-foreground));">
|
||||
<p>This page is still resting. Add a block to begin.</p>
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
@templ.Raw(data.Slots["footer"])
|
||||
</footer>
|
||||
@bn.BodyEnd(data.SiteSettings)
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
// RenderPastelDefault is the registered render entry for the default template.
|
||||
func RenderPastelDefault(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelDefault(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelLanding is the registered render entry for the landing template.
|
||||
func RenderPastelLanding(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelLanding(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelArticle is the registered render entry for the article template.
|
||||
func RenderPastelArticle(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelArticle(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelFullWidth is the registered render entry for the full-width template.
|
||||
func RenderPastelFullWidth(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelFullWidth(parsePastelPageData(doc))
|
||||
}
|
||||
510
template_templ.go
Normal file
@ -0,0 +1,510 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package main
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/templates/bn"
|
||||
)
|
||||
|
||||
// PastelPageData carries the per-render page context.
|
||||
type PastelPageData struct {
|
||||
Title string
|
||||
Slots map[string]string
|
||||
ThemeMode string
|
||||
ThemeCSS string
|
||||
SiteSettings bn.SiteSettingsData
|
||||
PageMeta bn.PageMeta
|
||||
StructuredData string
|
||||
CSSHash string
|
||||
PageviewNonce string
|
||||
EngagementConfig bn.EngagementConfig
|
||||
}
|
||||
|
||||
// parsePastelPageData converts the wire-format doc map into a PastelPageData.
|
||||
// It is lenient: missing fields fall back to sensible defaults so empty pages
|
||||
// render without panicking.
|
||||
func parsePastelPageData(doc map[string]any) PastelPageData {
|
||||
title := "Untitled"
|
||||
if t, ok := doc["title"].(string); ok && t != "" {
|
||||
title = t
|
||||
}
|
||||
|
||||
slots := make(map[string]string)
|
||||
if s, ok := doc["slots"].(map[string]string); ok {
|
||||
slots = s
|
||||
}
|
||||
|
||||
themeCSS := ""
|
||||
if tc, ok := doc["theme_css"].(string); ok {
|
||||
themeCSS = tc
|
||||
}
|
||||
|
||||
structuredData := ""
|
||||
if sd, ok := doc["structured_data"].(string); ok {
|
||||
structuredData = sd
|
||||
}
|
||||
|
||||
cssHash := ""
|
||||
if ch, ok := doc["css_hash"].(string); ok {
|
||||
cssHash = ch
|
||||
}
|
||||
|
||||
pageviewNonce := ""
|
||||
if pn, ok := doc["pageview_nonce"].(string); ok {
|
||||
pageviewNonce = pn
|
||||
}
|
||||
|
||||
themeMode := "light"
|
||||
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
|
||||
themeMode = tm
|
||||
}
|
||||
|
||||
return PastelPageData{
|
||||
Title: title,
|
||||
Slots: slots,
|
||||
ThemeMode: themeMode,
|
||||
ThemeCSS: themeCSS,
|
||||
SiteSettings: bn.ParseSiteSettings(doc),
|
||||
PageMeta: bn.ParsePageMeta(doc),
|
||||
StructuredData: structuredData,
|
||||
CSSHash: cssHash,
|
||||
PageviewNonce: pageviewNonce,
|
||||
EngagementConfig: bn.ParseEngagementConfig(doc),
|
||||
}
|
||||
}
|
||||
|
||||
// PastelDefault is the standard pastel-dream page template — masthead, main
|
||||
// column, and footer. Used by both `default` and (with a slightly wider main)
|
||||
// the `full-width` template.
|
||||
func PastelDefault(data PastelPageData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"font-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<header class=\"w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</header><main class=\"flex-grow w-full max-w-4xl mx-auto px-4 py-12\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"py-20 text-center font-body\" style=\"color: hsl(var(--muted-foreground));\"><p>This page is still resting. Add a block to begin.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</main><footer class=\"w-full mt-auto\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// PastelLanding is the marketing landing layout — full-width hero, body,
|
||||
// affirmation CTA, footer.
|
||||
func PastelLanding(data PastelPageData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var2 == nil {
|
||||
templ_7745c5c3_Var2 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<!doctype html><html lang=\"en\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<body class=\"font-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<section class=\"w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Slots["hero"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</section><main class=\"flex-grow w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"max-w-6xl mx-auto px-4 py-20\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</main><section class=\"w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Slots["cta"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</section><footer class=\"w-full mt-auto\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// PastelArticle is the narrow reading-room layout for editorial posts.
|
||||
func PastelArticle(data PastelPageData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<!doctype html><html lang=\"en\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<body class=\"font-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<header class=\"w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</header><main class=\"flex-grow w-full max-w-2xl mx-auto px-4 py-16\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<article class=\"font-body prose max-w-none\" style=\"line-height: 1.75; color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</article>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"py-20 text-center\" style=\"color: hsl(var(--muted-foreground));\"><p>This page is still resting. Add a block to begin.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</main><footer class=\"w-full mt-auto\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// PastelFullWidth is the edge-to-edge layout for galleries and seasonal looks.
|
||||
func PastelFullWidth(data PastelPageData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<!doctype html><html lang=\"en\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.Head(bn.HeadData{
|
||||
Title: data.Title,
|
||||
Settings: data.SiteSettings,
|
||||
PageMeta: data.PageMeta,
|
||||
ThemeMode: data.ThemeMode,
|
||||
ThemeCSS: data.ThemeCSS,
|
||||
PluginStyles: []string{"/templates/pastel-dream/style.css"},
|
||||
StructuredData: data.StructuredData,
|
||||
CSSHash: data.CSSHash,
|
||||
PageviewNonce: data.PageviewNonce,
|
||||
EngagementConfig: data.EngagementConfig,
|
||||
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<body class=\"font-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<header class=\"w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</header><main class=\"flex-grow w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if main, ok := data.Slots["main"]; ok && main != "" {
|
||||
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div class=\"max-w-4xl mx-auto py-20 px-4 text-center\" style=\"color: hsl(var(--muted-foreground));\"><p>This page is still resting. Add a block to begin.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</main><footer class=\"w-full mt-auto\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RenderPastelDefault is the registered render entry for the default template.
|
||||
func RenderPastelDefault(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelDefault(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelLanding is the registered render entry for the landing template.
|
||||
func RenderPastelLanding(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelLanding(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelArticle is the registered render entry for the article template.
|
||||
func RenderPastelArticle(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelArticle(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
// RenderPastelFullWidth is the registered render entry for the full-width template.
|
||||
func RenderPastelFullWidth(ctx context.Context, doc map[string]any) templ.Component {
|
||||
return PastelFullWidth(parsePastelPageData(doc))
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@ -1,99 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>{{ site_name }}</title>
|
||||
<style type="text/css">
|
||||
body, table, td, p, a, li, blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
@media only screen and (max-width: 620px) {
|
||||
.pd-email-container {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.pd-email-padding {
|
||||
padding-left: 24px !important;
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: {{ colors.background|default:"#faf4ed" }}; margin: 0; padding: 0; font-family: &#39;Quicksand&#39;, &#39;Nunito&#39;, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;">
|
||||
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 10px; background-color: {{ colors.background|default:"#faf4ed" }};">
|
||||
<table role="presentation" class="pd-email-container" width="560" cellspacing="0" cellpadding="0" border="0" style="max-width: 560px; background-color: {{ colors.card|default:"#fdf9f4" }}; border: 1px solid {{ colors.border|default:"#e5d6c9" }}; border-radius: 4px;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 32px 40px 16px; border-bottom: 1px dashed {{ colors.border|default:"#e5d6c9" }};">
|
||||
{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 48px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-size: 24px; letter-spacing: -0.01em; color: {{ colors.primary|default:"#d05a83" }};">{{ site_name }}</h1>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pd-email-padding" style="padding: 32px 40px; color: {{ colors.foreground|default:"#47323c" }}; font-size: 16px; line-height: 1.65;">{{ body|safe }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 16px 40px 8px;">
|
||||
<!-- inline SVG doodle divider, survives Outlook -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0"><tr><td>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="32" viewBox="0 0 200 32" fill="none" stroke="{{ colors.primary|default:"#d05a83" }}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M4 18 L80 18"></path>
|
||||
<path d="M120 18 L196 18"></path>
|
||||
<ellipse cx="95" cy="18" rx="6" ry="9"></ellipse>
|
||||
<path d="M95 9 L95 27"></path>
|
||||
<ellipse cx="108" cy="18" rx="6" ry="9"></ellipse>
|
||||
<path d="M108 9 L108 27"></path>
|
||||
</svg>
|
||||
</td></tr></table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 16px 40px 32px; color: {{ colors.mutedForeground|default:"#79626c" }}; font-size: 12px; line-height: 1.6;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center">
|
||||
{% if site_name %}<p style="margin: 0 0 6px; font-size: 13px; color: {{ colors.foreground|default:"#47323c" }};">{{ site_name }}</p>{% endif %}
|
||||
<p style="margin: 0 0 6px;">Fresh bakes from the morning. Cakes to order all week.</p>
|
||||
{% if site_url %}<p style="margin: 0 0 8px;"><a href="{{ site_url }}" style="color: {{ colors.primary|default:"#d05a83" }}; text-decoration: none; border-bottom: 1px dashed {{ colors.primary|default:"#d05a83" }};">{{ site_url }}</a></p>{% endif %}
|
||||
{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#79626c" }}; text-decoration: none;">Unsubscribe</a></p>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,21 +0,0 @@
|
||||
{# pastel-dream announcement-bar — dismissable strip; dismissal persists in localStorage keyed by variant+message hash. #}
|
||||
{% set anncls = "bg-accent/15 text-foreground" %}
|
||||
{% if variant == "promo" %}{% set anncls = "bg-accent text-accent-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
|
||||
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full {{ anncls }} {{ class }}">
|
||||
<div class="relative mx-auto flex max-w-7xl items-center justify-center gap-2 px-10 py-2 text-center text-sm">
|
||||
<p class="pd-body font-medium">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline decoration-dashed underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
|
||||
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 inline-flex -translate-y-1/2 items-center justify-center rounded p-1 transition-colors hover:bg-black/10"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if dismissable %}<script>
|
||||
(function(){
|
||||
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
|
||||
bars.forEach(function(bar){
|
||||
bar.setAttribute('data-bn-ann-wired','1');
|
||||
var key='bnann:'+bar.getAttribute('data-bn-annkey');
|
||||
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
|
||||
var btn=bar.querySelector('[data-bn-ann-dismiss]');
|
||||
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
|
||||
});
|
||||
})();
|
||||
</script>{% endif %}
|
||||
@ -1,15 +0,0 @@
|
||||
<figure class="bn-audio pd-card my-8 overflow-hidden{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden rounded-sm bg-secondary">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="pd-display truncate text-lg text-primary">{{ title }}</p>{% endif %}{% if artist %}<p class="pd-hand truncate text-lg text-accent">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="pd-tag text-xs" aria-label="Play{% if title %}: {{ title }}{% endif %}"><svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg> Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="pd-body inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"><svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#download"></use></svg> Download</a>{% endif %}</div></div></div></figure><script>
|
||||
(function(){
|
||||
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
|
||||
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
|
||||
e.preventDefault();
|
||||
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
|
||||
if(!src)return;
|
||||
var slot=btn.closest('[data-bn-audio-slot]');
|
||||
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
|
||||
btn.remove();slot.insertBefore(a,slot.firstChild);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@ -1,32 +0,0 @@
|
||||
{# pastel-dream auth-form — combined login/register in soft pastel chrome. Endpoints and panel ids match the platform auth flow (/api/auth/login, /api/auth/register, #auth-message, #login-panel/#register-panel). Auth state via the auth_state provider (auth.logged_in / auth.display_name). Captcha, when enabled, is emitted by the platform auth path. #}
|
||||
{% if auth.logged_in %}
|
||||
<div class="auth-form-logged-in pd-card mx-auto max-w-md p-6 text-center">
|
||||
<p class="pd-body text-muted-foreground">You are logged in as <strong class="text-primary">{{ auth.display_name }}</strong></p>
|
||||
<a href="/account" class="pd-tag mt-4">My account</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% set defaultTab = default_tab|default:"login" %}
|
||||
<div class="auth-form pd-card mx-auto max-w-md p-6">
|
||||
<div id="auth-message"></div>
|
||||
<div class="mb-5 flex border-b border-dashed border-border">
|
||||
<button type="button" onclick="document.getElementById('login-panel').classList.remove('hidden');document.getElementById('register-panel').classList.add('hidden');this.className='pd-display flex-1 py-2 text-center border-b-2 border-primary text-primary';this.nextElementSibling.className='pd-display flex-1 py-2 text-center border-b-2 border-transparent text-muted-foreground'" class="pd-display flex-1 py-2 text-center border-b-2 {% if defaultTab == "register" %}border-transparent text-muted-foreground{% else %}border-primary text-primary{% endif %}">Login</button>
|
||||
<button type="button" onclick="document.getElementById('register-panel').classList.remove('hidden');document.getElementById('login-panel').classList.add('hidden');this.className='pd-display flex-1 py-2 text-center border-b-2 border-primary text-primary';this.previousElementSibling.className='pd-display flex-1 py-2 text-center border-b-2 border-transparent text-muted-foreground'" class="pd-display flex-1 py-2 text-center border-b-2 {% if defaultTab == "register" %}border-primary text-primary{% else %}border-transparent text-muted-foreground{% endif %}">Register</button>
|
||||
</div>
|
||||
<div id="login-panel" class="{% if defaultTab == "register" %}hidden{% endif %}">
|
||||
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
|
||||
<div class="mb-3"><label class="pd-hand mb-1 block text-lg text-accent">Email</label><input type="email" name="email" required class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||
<div class="mb-4"><label class="pd-hand mb-1 block text-lg text-accent">Password</label><input type="password" name="password" required class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||
<button type="submit" class="pd-tag w-full justify-center" hx-disabled-elt="this">Login</button>
|
||||
<p class="pd-body mt-3 text-center text-sm"><a href="/forgot-password" class="text-accent underline decoration-dashed">Forgot password?</a></p>
|
||||
</form>
|
||||
</div>
|
||||
<div id="register-panel" class="{% if defaultTab == "register" %}{% else %}hidden{% endif %}">
|
||||
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
|
||||
<div class="mb-3"><label class="pd-hand mb-1 block text-lg text-accent">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||
<div class="mb-3"><label class="pd-hand mb-1 block text-lg text-accent">Email</label><input type="email" name="email" required class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||
<div class="mb-4"><label class="pd-hand mb-1 block text-lg text-accent">Password</label><input type="password" name="password" required minlength="8" class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||
<button type="submit" class="pd-tag w-full justify-center" hx-disabled-elt="this">Create account</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -1,25 +0,0 @@
|
||||
{# pastel-dream auth-status — header login state. Auth state via the auth_state provider; endpoints match the platform (/login, /login?tab=register, /api/auth/logout, /api/auth/resend-verification). #}
|
||||
{% if not auth.logged_in %}
|
||||
<div class="auth-status flex items-center gap-2">
|
||||
<a href="/login" class="pd-body rounded-lg px-3 py-1.5 text-sm font-medium opacity-80 transition-colors hover:bg-muted hover:opacity-100">Login</a>
|
||||
<a href="/login?tab=register" class="pd-tag text-sm">Sign up</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if not auth.email_verified %}<div class="fixed inset-x-0 top-16 z-40 bg-accent px-4 py-2 text-center text-sm text-accent-foreground">Please verify your email address. <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="font-medium underline">Resend verification email</button></div>{% endif %}
|
||||
<div class="auth-status relative">
|
||||
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="pd-body flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium opacity-90 transition-colors hover:bg-muted hover:opacity-100">
|
||||
<span class="pd-hand flex h-7 w-7 items-center justify-center rounded-full bg-accent text-sm text-accent-foreground">{{ auth.display_name|first|upper }}</span>
|
||||
<span>{{ auth.display_name }}</span>
|
||||
<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
|
||||
</button>
|
||||
<div id="auth-dropdown-menu" class="pd-card absolute right-0 top-full z-50 mt-1 hidden w-48 py-1 shadow-lg">
|
||||
<a href="/account" class="pd-body block px-4 py-2 text-sm text-foreground hover:bg-muted">My profile</a>
|
||||
<a href="/account/reviews" class="pd-body block px-4 py-2 text-sm text-foreground hover:bg-muted">My reviews</a>
|
||||
<hr class="my-1 border-dashed border-border">
|
||||
<button hx-post="/api/auth/logout" hx-swap="none" class="pd-body block w-full px-4 py-2 text-left text-sm text-destructive hover:bg-muted">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){document.addEventListener('click',function(e){var menu=document.getElementById('auth-dropdown-menu');var btn=document.querySelector('.auth-status > button');if(!menu||!btn)return;if(!btn.contains(e.target)&&!menu.contains(e.target)){menu.classList.add('hidden');}});})();
|
||||
</script>
|
||||
{% endif %}
|
||||
@ -1,20 +0,0 @@
|
||||
{% if author %}
|
||||
<section class="border-b border-dashed border-border bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
|
||||
{% if showAvatar and author.avatar_url %}<div class="mb-5 h-24 w-24 overflow-hidden rounded-full bg-secondary ring-2 ring-border">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||
<h1 class="pd-display text-3xl text-primary md:text-4xl">{{ author.display_name }}</h1>
|
||||
{% if author.job_title or author.company %}<p class="pd-hand mt-1 text-xl text-accent">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
|
||||
{% if author.bio %}<p class="pd-body mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
|
||||
<div class="pd-body mt-5 flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground">
|
||||
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
|
||||
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
|
||||
</div>
|
||||
{% if showSocial %}<div class="mt-5 flex items-center justify-center gap-3">
|
||||
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="Website"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
|
||||
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="Twitter"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
|
||||
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="LinkedIn"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
|
||||
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="GitHub"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
|
||||
</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
@ -1,12 +0,0 @@
|
||||
{% if post.author_name %}
|
||||
<aside class="pd-card p-6{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<p class="pd-hand mb-4 text-xl text-accent">{{ heading }}</p>{% endif %}
|
||||
<div class="flex items-start gap-4">
|
||||
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full bg-secondary ring-2 ring-border">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||
<div class="min-w-0">
|
||||
<h3 class="pd-display text-base text-primary">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-accent">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
|
||||
{% if post.author_bio %}<p class="pd-body mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
{% endif %}
|
||||
@ -1,22 +0,0 @@
|
||||
<section class="bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
<div class="max-w-2xl">
|
||||
{% if title %}<h1 class="pd-display text-4xl text-primary md:text-5xl"><span class="pd-doodle-underline">{{ title }}</span></h1>{% endif %}
|
||||
{% if subtitle %}<p class="pd-body mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
|
||||
</div>
|
||||
{% if posts %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
{% for post in posts %}
|
||||
<article class="group flex flex-col{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-6{% endif %}">
|
||||
{% if post.featured_image_url %}<a href="{{ post.url }}" class="pd-frame mb-3 block overflow-hidden rounded-sm p-1.5{% if forloop.First %} md:mb-0 md:w-1/2{% endif %}">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full rounded-sm object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
||||
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
|
||||
<h2 class="pd-display {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h2>
|
||||
{% if showExcerpt and post.excerpt %}<p class="pd-body mt-2 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
|
||||
<div class="pd-hand mt-3 flex flex-wrap items-center gap-x-3 text-lg text-accent">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@ -1,12 +0,0 @@
|
||||
{% set arrows = template == "arrows" %}
|
||||
<nav aria-label="Breadcrumb" class="py-3{% if class %} {{ class }}{% endif %}">
|
||||
<ol class="pd-body flex flex-wrap items-center gap-1 text-sm">
|
||||
{% if showHome %}<li class="flex items-center"><a href="/" class="text-muted-foreground transition-colors hover:text-accent">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
|
||||
{% for item in breadcrumbs %}
|
||||
<li class="flex items-center">
|
||||
{% if showHome or not forloop.First %}{% if arrows %}<svg width="16" height="16" class="mx-2 h-4 w-4 text-accent" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"></use></svg>{% else %}<span class="mx-2 text-accent">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
|
||||
{% if item.is_current %}{% if showCurrent %}<span class="pd-display text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-muted-foreground transition-colors hover:text-accent">{{ item.label }}</a>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
@ -1,5 +0,0 @@
|
||||
{% with align=align|default:"left" style=style|default:"primary" %}
|
||||
<div class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
{% if link %}<a href="{{ link }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="pd-tag {% if style == "secondary" %}bg-secondary text-secondary-foreground{% elif style == "outline" or style == "ghost" %}pd-tag--ghost{% elif style == "destructive" %}bg-destructive text-destructive-foreground{% endif %} {% if size == "lg" %}text-base px-6 py-3{% elif size == "sm" %}text-xs px-3 py-1.5{% endif %}">{% if icon and iconPosition != "right" %}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</a>{% else %}<button type="button" class="pd-tag {% if style == "secondary" %}bg-secondary text-secondary-foreground{% elif style == "outline" or style == "ghost" %}pd-tag--ghost{% elif style == "destructive" %}bg-destructive text-destructive-foreground{% endif %} {% if size == "lg" %}text-base px-6 py-3{% elif size == "sm" %}text-xs px-3 py-1.5{% endif %}">{% if icon and iconPosition != "right" %}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</button>{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
@ -1,12 +0,0 @@
|
||||
{% with layout=layout|default:"vertical" %}
|
||||
<article class="bn-card pd-card overflow-hidden transition-shadow hover:shadow-md {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
{% if link %}<a href="{{ link }}" class="contents">{% endif %}
|
||||
{% if media %}<div class="overflow-hidden bg-secondary {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}
|
||||
<div class="flex flex-col gap-2 p-5 md:p-6 {% if layout == "horizontal" %}justify-center{% endif %}">
|
||||
{% if title %}<h3 class="pd-display text-xl text-primary">{{ title }}</h3>{% endif %}
|
||||
{% if text %}<div class="pd-body text-sm leading-relaxed text-muted-foreground">{{ text|safe }}</div>{% endif %}
|
||||
{% if linkLabel and link %}<span class="pd-hand mt-2 inline-flex items-center gap-1 text-lg text-accent">{{ linkLabel }} <svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></span>{% endif %}
|
||||
</div>
|
||||
{% if link %}</a>{% endif %}
|
||||
</article>
|
||||
{% endwith %}
|
||||
@ -1,17 +0,0 @@
|
||||
{% set pills = style == "pills" %}
|
||||
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<h2 class="pd-hand mb-4 text-2xl text-accent">{{ heading }}</h2>{% endif %}
|
||||
{% if pills %}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for cat in categories %}
|
||||
<a href="{{ cat.url }}" class="pd-body inline-flex items-center gap-1.5 rounded-full border border-dashed border-border bg-card px-3 py-1 text-sm text-card-foreground transition-colors hover:border-accent hover:text-accent">{{ cat.name }}{% if showCount %}<span class="text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<ul class="space-y-1">
|
||||
{% for cat in categories %}
|
||||
<li><a href="{{ cat.url }}" class="pd-body flex items-center justify-between rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"><span>{{ cat.name }}</span>{% if showCount %}<span class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</section>
|
||||
@ -1,17 +0,0 @@
|
||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-4xl px-4">
|
||||
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||
{% if description %}<p class="pd-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ description }}</p>{% endif %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-4 {% if columns == 3 %}sm:grid-cols-2 lg:grid-cols-3{% elif columns == 1 %}{% else %}sm:grid-cols-2{% endif %}">
|
||||
{% for channel in channels %}
|
||||
<div class="pd-card flex items-start gap-4 p-5">
|
||||
{% if channel.icon %}<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent/15 text-accent"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ channel.icon|split:":"|first }}.svg#{{ channel.icon|split:":"|last }}"></use></svg></span>{% endif %}
|
||||
<div class="min-w-0">
|
||||
{% if channel.label %}<div class="pd-hand text-lg text-accent">{{ channel.label }}</div>{% endif %}
|
||||
{% if channel.value %}<div class="pd-body mt-0.5 break-words font-medium">{% if channel.link %}<a href="{{ channel.link }}" class="transition-colors hover:text-accent">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1,39 +0,0 @@
|
||||
{% with bid=_block_id|default:context.blockId %}
|
||||
<section id="contact-form-{{ bid }}" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-2xl px-4">
|
||||
{% if form.submitted %}
|
||||
<div class="pd-card pd-torn-top pd-torn-bottom p-8 text-center">
|
||||
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-accent/15 text-accent"><svg width="24" height="24" class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg></div>
|
||||
<p class="pd-display text-xl text-primary">{{ form.message|default:successMessage|default:"Thanks. Your message is on its way." }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||
{% if description %}<p class="pd-body mt-3 text-lg text-muted-foreground">{{ description }}</p>{% endif %}
|
||||
{% if form.errors._form %}<div class="pd-body mt-6 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">{{ form.errors._form }}</div>{% endif %}
|
||||
{% with formAction="/api/blocks/"|add:bid|add:"/submit" formTarget="#contact-form-"|add:bid %}
|
||||
{% form action=formAction hx_target=formTarget hx_swap="outerHTML" class="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2" %}
|
||||
{% for field in fields %}
|
||||
<div class="{% if field.width == "half" %}sm:col-span-1{% else %}sm:col-span-2{% endif %}">
|
||||
{% if field.label %}<label for="cf-{{ bid }}-{{ field.name }}" class="pd-hand mb-1.5 block text-lg text-accent">{{ field.label }}{% if field.required %} <span class="text-destructive">*</span>{% endif %}</label>{% endif %}
|
||||
{% if field.type == "textarea" %}
|
||||
<textarea id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" rows="5"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"></textarea>
|
||||
{% elif field.type == "select" %}
|
||||
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
{% if field.placeholder %}<option value="">{{ field.placeholder }}</option>{% endif %}
|
||||
{% for opt in field.options %}<option value="{{ opt.value }}">{{ opt.label|default:opt.value }}</option>{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<input id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" type="{{ field.type|default:"text" }}"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="hidden" aria-hidden="true"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
|
||||
<div class="sm:col-span-2">
|
||||
<button type="submit" class="pd-tag">{{ submitLabel|default:"Send message" }}</button>
|
||||
</div>
|
||||
{% endform %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endwith %}
|
||||
@ -1,42 +0,0 @@
|
||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-4xl px-4 text-center">
|
||||
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="pd-body mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||
<div class="mt-10" data-countdown>
|
||||
<div class="flex items-start justify-center gap-3 sm:gap-6" data-countdown-timer data-target="{{ target }}" role="timer" aria-label="Time remaining">
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}pd-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="pd-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="days">--</span><span class="pd-hand mt-1 text-lg text-muted-foreground">{{ labelDays|default:"Days" }}</span></div>
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}pd-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="pd-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="hours">--</span><span class="pd-hand mt-1 text-lg text-muted-foreground">{{ labelHours|default:"Hours" }}</span></div>
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}pd-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="pd-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="minutes">--</span><span class="pd-hand mt-1 text-lg text-muted-foreground">{{ labelMinutes|default:"Minutes" }}</span></div>
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}pd-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="pd-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="seconds">--</span><span class="pd-hand mt-1 text-lg text-muted-foreground">{{ labelSeconds|default:"Seconds" }}</span></div>
|
||||
</div>
|
||||
<div class="mt-8 hidden" data-countdown-expired>
|
||||
<p class="pd-display text-xl text-primary">{{ expiredMessage|default:"This has started." }}</p>
|
||||
{% if expiredUrl and expiredLabel %}<a href="{{ expiredUrl }}" class="pd-tag mt-4 text-base px-5 py-3">{{ expiredLabel }}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<noscript><p class="pd-body mt-6 text-muted-foreground">Counting down to <time datetime="{{ target }}">{{ target }}</time>.</p></noscript>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function(){
|
||||
document.querySelectorAll('[data-countdown-timer]').forEach(function(el){
|
||||
var target = new Date(el.getAttribute('data-target')).getTime();
|
||||
if (isNaN(target)) return;
|
||||
var wrap = el.closest('[data-countdown]');
|
||||
var expired = wrap ? wrap.querySelector('[data-countdown-expired]') : null;
|
||||
function pad(n){ return (n < 10 ? '0' : '') + n; }
|
||||
function set(unit, value){ var node = el.querySelector('[data-unit="' + unit + '"]'); if (node) node.textContent = value; }
|
||||
var interval;
|
||||
function tick(){
|
||||
var diff = target - Date.now();
|
||||
if (diff <= 0){ el.style.display = 'none'; if (expired) expired.classList.remove('hidden'); if (interval) clearInterval(interval); return; }
|
||||
set('days', Math.floor(diff / 86400000));
|
||||
set('hours', pad(Math.floor(diff % 86400000 / 3600000)));
|
||||
set('minutes', pad(Math.floor(diff % 3600000 / 60000)));
|
||||
set('seconds', pad(Math.floor(diff % 60000 / 1000)));
|
||||
}
|
||||
tick();
|
||||
interval = setInterval(tick, 1000);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@ -1,14 +0,0 @@
|
||||
<section class="py-11 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
<div class="{% if background == "band" %}pd-torn-top pd-torn-bottom rounded-2xl bg-primary px-6 py-14 text-center text-primary-foreground md:px-12{% elif background == "card" %}pd-card px-6 py-14 text-center md:px-12{% else %}flex flex-col items-center gap-8 rounded-2xl bg-muted px-6 py-12 text-foreground md:flex-row md:justify-between md:px-12 md:text-left{% endif %}">
|
||||
<div class="{% if background != "split" %}mx-auto max-w-2xl{% endif %}">
|
||||
{% if heading %}<h2 class="pd-display text-3xl md:text-4xl {% if background == "band" %}text-primary-foreground{% else %}text-primary{% endif %}">{{ heading }}</h2>{% endif %}
|
||||
{% if text %}<p class="pd-body mt-3 text-lg {% if background == "band" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}">{{ text }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-center gap-3 {% if background != "split" %}mt-8{% else %}shrink-0{% endif %}">
|
||||
{% if primaryUrl and primaryLabel %}<a href="{{ primaryUrl }}" class="pd-tag text-base px-6 py-3 {% if background == "band" %}bg-background text-foreground{% endif %}">{{ primaryLabel }}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||
{% if secondaryUrl and secondaryLabel %}<a href="{{ secondaryUrl }}" class="pd-tag pd-tag--ghost text-base px-6 py-3 {% if background == "band" %}border-primary-foreground/40 text-primary-foreground{% endif %}">{{ secondaryLabel }}</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1,17 +0,0 @@
|
||||
{% with variant=variant|default:"line" width=width|default:"full" %}
|
||||
<div class="bn-divider pd-doodle-draw mx-auto my-8 flex items-center justify-center {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator">
|
||||
{% if variant == "ornament" or variant == "spacer-rule" %}
|
||||
<span class="h-px flex-1 bg-border"></span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="26" viewBox="0 0 72 26" fill="none" stroke="hsl(var(--accent))" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="mx-4 shrink-0" aria-hidden="true">
|
||||
<ellipse cx="30" cy="13" rx="6" ry="9"></ellipse><path d="M30 4 L30 22"></path>
|
||||
<ellipse cx="42" cy="13" rx="6" ry="9"></ellipse><path d="M42 4 L42 22"></path>
|
||||
<path d="M4 13 L18 13"></path><path d="M54 13 L68 13"></path>
|
||||
</svg>
|
||||
<span class="h-px flex-1 bg-border"></span>
|
||||
{% else %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 12" preserveAspectRatio="none" class="h-3 w-full" fill="none" stroke="hsl(var(--border))" stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
||||
<path d="M2 7 Q 40 2 80 7 T 160 6 T 240 7 T 298 6"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
@ -1,40 +0,0 @@
|
||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||
{% if intro %}<p class="pd-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||
{% if layout == "cards" %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{% for event in events %}{% if not (hidePast and event.past) %}
|
||||
<article class="pd-card flex flex-col overflow-hidden{% if event.past %} opacity-60{% endif %}">
|
||||
{% if event.media %}<div class="aspect-video overflow-hidden bg-secondary">{% img event.media alt=event.title class="h-full w-full object-cover" %}</div>{% endif %}
|
||||
<div class="flex flex-1 flex-col gap-2 p-5">
|
||||
<div class="pd-hand text-lg text-accent"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} – {{ event.end }}{% endif %}</time>{% if event.timezone %} <span class="text-muted-foreground">{{ event.timezone }}</span>{% endif %}</div>
|
||||
<h3 class="pd-display text-lg text-primary">{{ event.title }}</h3>
|
||||
{% if event.venue %}<p class="pd-body text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
|
||||
{% if event.description %}<p class="pd-body text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
|
||||
{% if event.link %}<a href="{{ event.link }}" class="pd-hand mt-auto inline-flex items-center gap-1 pt-2 text-lg text-accent hover:underline">{{ event.linkLabel|default:"Details" }}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<ul class="mt-10 divide-y divide-dashed divide-border border-y border-dashed border-border">
|
||||
{% for event in events %}{% if not (hidePast and event.past) %}
|
||||
<li class="flex flex-col gap-3 py-5 sm:flex-row sm:items-center sm:gap-6{% if event.past %} opacity-60{% endif %}">
|
||||
<div class="shrink-0 sm:w-52">
|
||||
<div class="pd-display text-sm text-primary"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time></div>
|
||||
{% if event.end %}<div class="pd-body text-xs text-muted-foreground">to {{ event.end }}</div>{% endif %}
|
||||
{% if event.timezone %}<div class="pd-body text-xs text-muted-foreground">{{ event.timezone }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="pd-display text-base text-primary">{{ event.title }}</h3>
|
||||
{% if event.venue %}<p class="pd-body text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
|
||||
{% if event.description %}<p class="pd-body mt-1 text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
|
||||
</div>
|
||||
{% if event.link %}<a href="{{ event.link }}" class="pd-tag pd-tag--ghost shrink-0 text-xs">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
|
||||
</li>
|
||||
{% endif %}{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@ -1,17 +0,0 @@
|
||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-3xl px-4">
|
||||
{% if heading %}<h2 class="pd-display text-center text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="pd-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||
<div class="mt-10 divide-y divide-dashed divide-border border-y border-dashed border-border">
|
||||
{% for item in items %}
|
||||
<details class="group py-2"{% if variant == "open-list" %} open{% endif %}>
|
||||
<summary class="pd-display flex cursor-pointer list-none items-center justify-between gap-4 py-3 text-lg text-primary marker:content-none [&::-webkit-details-marker]:hidden">
|
||||
<span>{{ item.question }}</span>
|
||||
<svg width="20" height="20" class="h-5 w-5 shrink-0 text-accent transition-transform duration-200 group-open:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
|
||||
</summary>
|
||||
<div class="pd-body pb-4 text-muted-foreground [&_a]:text-accent [&_a]:underline [&_a]:decoration-dashed">{{ item.answer|safe }}</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1,17 +0,0 @@
|
||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
{% if eyebrow %}<p class="pd-hand text-center text-2xl text-accent">{{ eyebrow }}</p>{% endif %}
|
||||
{% if heading %}<h2 class="pd-display mt-1 text-center text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||
{% if intro %}<p class="pd-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||
<div class="mt-12 grid grid-cols-1 gap-8 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
|
||||
{% for item in items %}
|
||||
<div class="{% if layout == "card" %}pd-card p-6 transition-shadow hover:shadow-md{% elif layout == "centered" %}text-center{% endif %}">
|
||||
{% if item.icon %}<div class="mb-4 inline-flex h-11 w-11 items-center justify-center rounded-full bg-accent/15 text-accent{% if layout == "centered" %} mx-auto{% endif %}"><svg width="24" height="24" class="h-6 w-6" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg></div>{% endif %}
|
||||
<h3 class="pd-display text-xl text-primary">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" class="transition-colors hover:text-accent">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>
|
||||
{% if item.text %}<p class="pd-body mt-2 text-muted-foreground">{{ item.text }}</p>{% endif %}
|
||||
{% if item.linkUrl and item.linkLabel %}<a href="{{ item.linkUrl }}" class="pd-hand mt-3 inline-flex items-center gap-1 text-lg text-accent hover:underline">{{ item.linkLabel }}<svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||