Compare commits

..

22 Commits
v0.1.1 ... main

Author SHA1 Message Date
Alex Dunmow
6afa49d026 bump to 0.5.0 2026-07-06 20:01:58 +08:00
Alex Dunmow
32b39d0543 feat(pastel-dream): reshoot gallery with bundled fonts loaded
Fraunces display + Quicksand body now render (was system fallback).
Home-light remains the curated preview.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:01:58 +08:00
Alex Dunmow
dc8d2d5c84 bump to 0.4.1 2026-07-06 19:58:37 +08:00
Alex Dunmow
7e6bd7d81d fix(pastel-dream): load bundled fonts via @font-face + preset typography
Bundled fonts are inert (host does not consume LoadedPlugin.BundledFonts),
so Fraunces and Quicksand were rendering system-font fallbacks. Declare
@font-face against the served /templates/pastel-dream/fonts/*.woff2 paths
and set typography.fontHeading=Fraunces / fontBody=Quicksand in all 5
presets so --font-heading/--font-body resolve to the bundled families.
Gotham@1.2.0 precedent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:58:37 +08:00
Alex Dunmow
8adbe8c64c bump to 0.4.0 2026-07-06 19:51:26 +08:00
Alex Dunmow
0393737155 feat(pastel-dream): Wave B gallery reshoot — product photos on every card
Home Seasonal Specials and article Cake Gallery now render real bakery
photos (no 'Add a photo' residue); prices render fully with no clip or
title collision; About shows the bakery interior. Home-light kept as the
curated preview (strongest, most representative face now that photos land).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:51:26 +08:00
Alex Dunmow
67b42d38a1 bump to 0.3.1 2026-07-06 19:45:18 +08:00
Alex Dunmow
4ac5f76714 fix(pastel-dream): seed product photos, unclippable price row, SVG geometry guard
- Seed 6 licensed bakery photos (media array) and wire them to every
  product card: home Seasonal Specials (strawberry tart/croissant/lemon
  loaf) and the article Cake Gallery (tart/sponge/galette). Kills the
  'Add a photo' placeholder residue on all six cards. Also adds a bakery
  interior shot to the About story section.
- Card title/price row: price moved to its own line (cake gallery) / a
  wrapping badge+price row under the title (seasonal specials) so a long
  product name never collides with the price and '$X.XX' can never clip.
- SVG geometry guard: explicit width/height on all 43 <use> icon SVGs so
  the host Tailwind JIT dropping a size utility can't balloon an icon.
- Lengthen the strawberries article to 8 paragraphs (bakery voice).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:45:04 +08:00
Alex Dunmow
d4e21e484b bump to 0.3.0 2026-07-06 09:53:43 +08:00
Alex Dunmow
99fb13b4a2 feat: Wave B gallery screenshots (home/about/article × light+dark) 2026-07-06 09:53:20 +08:00
Alex Dunmow
2fe663ca74 chore: rename dev host localdev.blockninjacms.com -> blockninja.dev 2026-07-05 20:50:30 +08:00
Alex Dunmow
4409f21e35 refactor: replace hand-rolled utility CSS with Tailwind classes (host build now scans theme templates)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:54:54 +08:00
Alex Dunmow
e6348edd10 fix(pastel-dream): reliable layout via theme CSS + graceful empty media
Host Tailwind (v4, source(none)) does not scan installed .bnp theme
templates, so grid/aspect/gap/padding utilities used only in this theme
were silently dropped: custom persona blocks collapsed to a single
full-width column, media boxes rendered as thin strips, and p-5 loss
clipped the first glyph of every description. Move the theme's layout
primitives into css.input_css_append as plain CSS with explicit
media queries (mobile-first, collapse to 1 column) so the render no
longer depends on the JIT content scan.

- Add .pd-section / .pd-grid(--2/--3/--lg-2/--lg-4) / .pd-menu-cols /
  .pd-menu-list / .pd-media(--square) / .pd-card-body helpers to both
  manifest.yaml (served) and assets/style.css. Semantic tokens only.
- seasonal_specials + cake_gallery: reliable responsive grid, OMIT the
  media box when no image is set (no ballooning empty boxes / thin
  strips), real body padding.
- menu-list: honor columns=2 as a true two-up board; reliable item spacing.
- hours-location / feature-grid: reliable responsive grids.
- Tighten section rhythm (py-14/py-24 -> .pd-section) to remove dead bands.
- navbar drawer width pinned in its inline <style>; body overflow-x: clip
  guards against horizontal scroll on small screens.

No .reveal/scroll-reveal content-hiding in this theme (stats count-up
only, values render by default). No version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:26:33 +08:00
Alex Dunmow
8eaf3a68aa fix(pastel-dream): collapse multi-line ninjatpl comments to single-line (load-fatal lexer error) 2026-07-05 17:06:50 +08:00
Alex Dunmow
834772eef4 bump to 0.2.2 2026-07-05 16:51:02 +08:00
Alex Dunmow
d48114cb8f bump to 0.2.1 2026-07-05 16:44:49 +08:00
Alex Dunmow
dcccb7dd8a docs: publishing README 2026-07-05 13:43:27 +08:00
Alex Dunmow
1ee98d318a feat(pastel-dream): WO-TF-023 bakery/patisserie theme — full Wave A rebuild
Pivot from the wellness/spa WZ-021 state to the locked bakery/patisserie
persona. Codeless .bnp, verified against cms a0f07e2ae (OVERRIDE-CONTRACT).

- 49 mandatory template overrides (+ manifest entries), re-skinned from the
  field-correct coffee sibling: every content field, provider var, custom tag
  and JS preserved verbatim; only styling + copy changed. §4 button/card reads
  reconciled (link/style/label, text/media).
- 7 page templates: default, landing, article, full-width, blog-index,
  contact, auth.
- 4 persona blocks: seasonal_specials, cake_gallery, order_ribbon, bakers_note
  (old 6 duplicate-builtin blocks deleted -> now overrides).
- 5 dual-mode presets (morning-haze/berry-cream/matcha-cream/lavender-honey/
  peach-blossom), all 19 tokens, tuned dusk-berry dark.
- Bundled OFL fonts: Fraunces (heading) + Quicksand (body); all font-family via
  var(--font-*). Themed email-safe wrapper. master_pages + demo seed (home,
  about, blog x3, contact w/ data-table + row_inserted email workflow, login).
- CSS-FIRST motion: watercolor washes, scalloped edges, soft pills; reduced-
  motion honored.

Gates: make -> exit 0 (codeless .bnp); check-safety -> 19 ok 13 skip, exit 0.
Known limits per contract §11/§12 (auth trio + page-suggestions + provider-
backed lists inert until cms fix). Visuals UNVERIFIED until Wave B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:41:03 +08:00
Alex Dunmow
5dfc1cd4be feat: convert to codeless .bnp — templates + manifest, Go/templ deleted (WO-WZ-021)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:27:47 +08:00
Alex Dunmow
8b2f726fbc chore: repo hygiene for the wasm era (WO-WZ-015 fix round)
Replace the retired .so Makefile with the wasm/.bnp convention (build /
verify / archive-check / publish via ninja plugin build), gitignore *.so
and *.bnp artifacts, and refresh the stale [compatibility] block_core
constraint to the v0.15 era.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:15:37 +08:00
Alex Dunmow
2016a28d2a wasm port (WO-WZ-015): reactor-mode wasip1 guest, core v0.15.2
Add wasmguest.Serve(Registration) boilerplate and bump block/core to v0.15.2
for the .so->wasm migration. Built + published as .bnp to the dev registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:11:12 +08:00
Alex Dunmow
c866901ed6 bump to 0.1.2 2026-07-03 22:10:24 +08:00
146 changed files with 2905 additions and 5015 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
tmp/
.idea/
.vscode/
*.bnp

View File

@ -1,280 +1,85 @@
# Pastel Dream — Build Report
# Pastel Dream — Build Report (Wave A, WO-TF-023)
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`).
Bakery / patisserie persona. Codeless `.bnp` theme (no Go, no wasm). Built and
verified against cms main at `a0f07e2ae` (OVERRIDE-CONTRACT freeze).
## What landed
### Identity & module
- `plugin.mod` written verbatim from spec §"plugin.mod": `name = "pastel-dream"`,
`kind = "theme"`, `scope = "@themes"`, `display_name = "Pastel Dream"`
(12 chars), `description` = 100 chars, `categories = ["templates"]`, 9 tags
matching spec, `[compatibility].block_core = ">=0.11.0 <0.12.0"`,
`version = "0.1.0"`.
- `go.mod` module path `git.dev.alexdunmow.com/block/themes/pastel-dream`,
`go 1.26.4`, `git.dev.alexdunmow.com/block/core v0.11.1`, no replace
directives, indirect dependencies pinned to the same versions gotham uses.
- `go.sum` is fully tidied (42 lines) — `go mod tidy` exits clean.
### 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.
### 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.
### 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`).
### 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`.
### Page templates (7)
- `default`, `landing`, `article`, `full-width`, `blog-index`, `contact`, `auth`
under `templates/pastel-dream/`. Soft watercolor-wash chrome (`pd-paper` body).
### 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.
### 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.
### 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).
### 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.
| 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) |
### 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>)`.
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}`.
### Email
- `templates/email/pastel-dream.ninjatpl`: table-based, inline styles, pastel
hex fallbacks for clients without CSS custom properties, scalloped divider.
### Built-in overrides
Four overrides wired via `br.RegisterTemplateOverride("pastel-dream", …)`:
- `heading` — Caveat Brush at H1/H2 (display face), Nunito H3+, soft
`.brush-underline` accent.
- `text``font-body` with `line-height: 1.75` per spec.
- `button``.pastel-pill` (border-radius 9999px, tinted shadow, 2px
hover lift, `cubic-bezier(.22,1,.36,1)` easing).
- `card``.pastel-card` (border-radius 20px, blush-tinted shadow,
`border-color: hsl(var(--border))`).
### Master pages + 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.
### Master pages
`DefaultMasterPages()` returns exactly two entries:
### 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`.
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
## Verification (both 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
$ 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
```
## Safety check
Runtime template rendering is not exercised by these gates (codeless build +
check-safety are static). Visual correctness stays UNVERIFIED until 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
```
## 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.

219
Makefile
View File

@ -1,190 +1,55 @@
# Pastel Dream — build & deploy helpers (.so plugin workflow)
# pastel-dream — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
#
# 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.
# 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.
#
# Usage:
# make # Local single-shot build (CGO go build -buildmode=plugin)
# make rebuild # Full rebuild against a running CMS instance
# make backend # .so + migrations only, restart
# make build-css # Rebuild Tailwind CSS
# make logs # Tail instance logs
# make status # Show instance container status
# make templ # Regenerate *_templ.go files locally
# 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
.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
.PHONY: build verify archive-check publish clean help
# 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)
NINJA := ninja
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
# Default target: build the .so locally for development.
all: $(PLUGIN_NAME).so
# 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 .
# 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 .
# Validate the built .bnp exactly as the registry/loader will.
verify:
$(NINJA) plugin verify $(BNP)
# Remove the compiled .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.
clean:
rm -f $(PLUGIN_NAME).so
# Regenerate templ Go files locally (for development).
templ:
cd $(PLUGIN_SRC) && $(HOME)/go/bin/templ generate
# Ensure blockninja core services and the instance container are running.
spinup:
$(MAKE) -C $(BLOCKNINJA_DIR) spinup
# Full rebuild: frontend + .so plugin + CSS + migrations, restart.
rebuild: spinup
$(MAKE) build-frontend
$(MAKE) build-base-binary
$(MAKE) copy-plugin-source
$(MAKE) build-so
$(MAKE) build-css
$(MAKE) sync-migrations
podman restart $(CONTAINER)
@sleep 2
$(MAKE) deploy-css
@echo ""
@echo "Done. https://$(INSTANCE_SLUG).localdev.blockninjacms.com/"
# Backend-only rebuild: .so plugin + migrations, restart.
backend: spinup
$(MAKE) build-base-binary
$(MAKE) copy-plugin-source
$(MAKE) build-so
$(MAKE) sync-migrations
podman restart $(CONTAINER)
@echo "Backend updated."
# Build host admin UI and deploy to container.
build-frontend:
@echo "==> Building @block-ninja/ui ..."
cd $(BLOCKNINJA_DIR)/cms/packages/ui && pnpm run build
@echo "==> Building host admin UI ..."
cd $(BLOCKNINJA_DIR)/cms/web && pnpm run build
@echo "==> Deploying frontend to container ..."
podman exec $(CONTAINER) rm -rf /app/web/dist
podman cp $(BLOCKNINJA_DIR)/cms/web/dist $(CONTAINER):/app/web/dist
@echo "Frontend deployed."
# Build the base CMS binary (without external plugins) and copy to container.
build-base-binary:
@echo "==> Building base CMS binary ..."
podman run --rm \
-v $(BLOCKNINJA_DIR)/cms/backend:/src/backend:ro \
-v blockninja_go_cache:/go/pkg/mod \
-v /tmp:/out \
-w /src/backend \
$(GO_BUILDER) \
go build -o /out/blockninja-server ./cmd/server
podman cp /tmp/blockninja-server $(CONTAINER):/app/server
rm -f /tmp/blockninja-server
# Copy plugin source into the container's plugin source directory.
copy-plugin-source:
@echo "==> Copying $(PLUGIN_NAME) source to container ..."
podman exec $(CONTAINER) rm -rf $(PLUGIN_DEST)
podman exec $(CONTAINER) mkdir -p $(PLUGIN_DEST)
podman cp $(PLUGIN_SRC)/. $(CONTAINER):$(PLUGIN_DEST)/
podman exec $(CONTAINER) rm -rf $(PLUGIN_DEST)/.git $(PLUGIN_DEST)/Makefile
@echo "Plugin source copied."
# Build the .so using the go-builder container (same toolchain as CMS binary).
build-so:
@echo "==> Building $(PLUGIN_NAME).so ..."
podman run --rm \
-v $(PLUGIN_SRC):/src/plugin:ro \
-v blockninja_go_cache:/go/pkg/mod \
-v /tmp:/out \
-w /src/plugin \
-e CGO_ENABLED=1 \
$(GO_BUILDER) \
go build -buildmode=plugin -ldflags="-s -w" -o /out/$(PLUGIN_NAME).so .
podman exec $(CONTAINER) mkdir -p /app/data/plugins/so
podman cp /tmp/$(PLUGIN_NAME).so $(CONTAINER):/app/data/plugins/so/$(PLUGIN_NAME).so
rm -f /tmp/$(PLUGIN_NAME).so
@echo "$(PLUGIN_NAME).so built."
# Sync base blockninja migration files from host to container.
sync-migrations:
@echo "==> Syncing migrations ..."
@podman unshare bash -c ' \
M=$$(podman mount $(CONTAINER)) && \
rm -rf "$$M/app/migrations" && \
mkdir -p "$$M/app/migrations" && \
podman umount $(CONTAINER)'
@podman cp $(MIGRATIONS_SRC)/. $(CONTAINER):/app/migrations/
@echo "Migrations synced."
# Rebuild Tailwind CSS.
build-css:
@echo "==> Building CSS ..."
cd $(BLOCKNINJA_DIR)/cms && make css
# Copy built CSS to instance styles dir and container.
deploy-css:
@mkdir -p $(STYLES_DIR)
cp $(BLOCKNINJA_DIR)/cms/data/styles/styles.css $(STYLES_DIR)/styles.css
podman cp $(BLOCKNINJA_DIR)/cms/data/styles/styles.css $(CONTAINER):/app/data/styles/styles.css
podman cp $(BLOCKNINJA_DIR)/cms/styles/input.base.css $(CONTAINER):/app/styles/input.base.css
@echo "CSS deployed."
# Tail instance logs.
logs:
podman logs -f $(CONTAINER)
# Show instance container status.
status:
@podman inspect $(CONTAINER) --format \
'Name: {{.Name}}\nImage: {{.Config.Image}}\nStatus: {{.State.Status}}\nHealth: {{.State.Health.Status}}\nStarted: {{.State.StartedAt}}' \
2>/dev/null || echo "Container $(CONTAINER) not found."
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
help:
@echo "Targets:"
@echo " all Build $(PLUGIN_NAME).so locally (default)"
@echo " clean Remove the compiled $(PLUGIN_NAME).so"
@echo " templ Regenerate templ Go files locally"
@echo " spinup Start blockninja core services + instance container"
@echo " rebuild Full rebuild: frontend + .so + CSS + migrations, restart"
@echo " backend .so + migrations only, restart"
@echo " logs Tail instance container logs"
@echo " status Show instance container status"
# --- Version bump targets ---
CURRENT_VERSION := $(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/')
bump-patch:
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.%d.%d", $$1, $$2, $$3+1}'); \
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
echo "Bumped to $$NEW and tagged v$$NEW"
bump-minor:
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.%d.0", $$1, $$2+1}'); \
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
echo "Bumped to $$NEW and tagged v$$NEW"
bump-major:
@NEW=$$(echo $(CURRENT_VERSION) | awk -F. '{printf "%d.0.0", $$1+1}'); \
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$NEW'"/' plugin.mod; \
git add plugin.mod && git commit -m "chore: bump version to $$NEW" && git tag "v$$NEW"; \
echo "Bumped to $$NEW and tagged v$$NEW"
sync-version:
@TAG=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//'); \
if [ -z "$$TAG" ]; then echo "No tags found"; exit 1; fi; \
sed -i 's/version = "$(CURRENT_VERSION)"/version = "'$$TAG'"/' plugin.mod; \
echo "Synced plugin.mod to $$TAG"
@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"

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# 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.

View File

@ -1,45 +1,45 @@
# Recommended fonts — Pastel Dream
This pass ships `fonts.json = []` per the wave-1 fonts policy (see
`themes/docs/FONTS.md`). The visual identity is realised through CSS variable
fallback stacks (`--font-heading`, `--font-body`, `--font-mono`) so the theme
renders sensibly out of the box.
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.
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.
## Bundled (source = template)
## Display / hand-lettered (assign to Heading)
| 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 |
- `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
Assign in the typography panel:
1. Open `/admin/system/typography` (or your instance's typography panel).
2. Open the Google Fonts tab.
3. Search for each family above and click "Add".
4. In the Heading / Body / Mono slots, pick the family you added.
5. Save. The CMS injects an `@import` link and re-renders.
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.
## Why no bundled woff2 in this pass
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.
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.
## 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`.

29
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,29 @@
# 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`).

View File

@ -1,46 +0,0 @@
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
}

View File

@ -1,37 +0,0 @@
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));">
&ldquo;{ data.Quote }&rdquo;
</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>
}

View File

@ -1,118 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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));\">&ldquo;")
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, "&rdquo;</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

View File

@ -1,187 +0,0 @@
/* 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;
}
}

109
assets/fonts/OFL.txt Normal file
View File

@ -0,0 +1,109 @@
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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
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.

View File

@ -1,133 +1,257 @@
/* Pastel Dream theme stylesheet served at /templates/pastel-dream/style.css
/* Pastel Dream theme styles bakery / patisserie.
*
* 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.
* 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 {
--radius-soft: 20px;
--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;
}
.bg-watercolor-blush {
/* --- 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% 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%);
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-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%);
/* --- 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;
}
.shadow-soft {
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
0 4px 12px -2px hsl(var(--primary) / 0.10);
/* --- 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;
}
.deckle-edge {
border: 1px dashed hsl(var(--border));
/* --- 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));
}
.pastel-pill {
border-radius: 9999px;
padding: 0.75rem 1.75rem;
font-family: var(--font-body, "Nunito", "Quicksand", system-ui, sans-serif);
font-weight: 600;
/* --- 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));
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;
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;
display: inline-block;
}
.pastel-pill:hover {
.pd-tag:hover {
transform: translateY(-2px);
box-shadow: 0 12px 32px -6px hsl(var(--primary) / 0.50);
box-shadow: 0 14px 30px -8px hsl(var(--primary) / 0.52);
background-color: hsl(var(--primary) / 0.92);
}
.pastel-pill:focus-visible {
.pd-tag:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
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-card {
border-radius: var(--radius-soft);
/* --- 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));
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
0 4px 12px -2px hsl(var(--primary) / 0.10);
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);
}
.brush-underline {
/* --- 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;
display: inline-block;
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));
}
.brush-underline::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -0.2em;
height: 0.22em;
background-color: hsl(var(--accent) / 0.55);
/* --- 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;
z-index: -1;
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);
}
@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;
/* --- 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) {
.animate-pastel-shimmer,
.animate-breathe,
.pastel-pill {
animation: none !important;
transition: none !important;
}
.pastel-pill:hover {
transform: none;
.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; }

View File

@ -0,0 +1,14 @@
<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>

View File

@ -0,0 +1,30 @@
{
"$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"
}
}
}

33
blocks/blocks.yaml Normal file
View File

@ -0,0 +1,33 @@
# 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

View File

@ -0,0 +1,23 @@
<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>

View File

@ -0,0 +1,39 @@
{
"$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" }
}
}
}
}
}

View File

@ -0,0 +1,18 @@
<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>

View File

@ -0,0 +1,37 @@
{
"$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"
}
}
}

View File

@ -0,0 +1,26 @@
<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>

View File

@ -0,0 +1,46 @@
{
"$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" }
}
}
}
}
}

View File

@ -1,28 +0,0 @@
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
}

View File

@ -1,8 +0,0 @@
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>
}

View File

@ -1,80 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

View File

@ -1,29 +0,0 @@
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
}

View File

@ -1,20 +0,0 @@
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>
}

View File

@ -1,92 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

View File

@ -1,40 +0,0 @@
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
}

View File

@ -1,42 +0,0 @@
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 &middot; with care
</p>
</div>
</section>
}

View File

@ -1,94 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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 &middot; with care</p></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,187 +0,0 @@
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,
)
}

View File

@ -1,450 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// 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

View File

@ -1,49 +0,0 @@
package main
import (
"embed"
"io/fs"
"net/http"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
// Assets returns the embedded assets filesystem.
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "assets")
return sub
}
// Schemas returns the embedded schemas filesystem.
func Schemas() fs.FS {
sub, _ := fs.Sub(schemasFS, "schemas")
return sub
}
// AssetsHandler returns an http.Handler that serves the embedded assets.
func AssetsHandler() http.Handler {
return http.FileServer(http.FS(Assets()))
}
// ThemePresets returns the embedded theme presets JSON.
func ThemePresets() []byte {
return presetsData
}
// BundledFonts returns the embedded fonts manifest JSON.
func BundledFonts() []byte {
return fontsData
}

View File

@ -1,53 +0,0 @@
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
}

View File

@ -1,40 +0,0 @@
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>
}

View File

@ -1,147 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

View File

@ -1 +1,18 @@
[]
[
{
"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
View File

@ -1,20 +0,0 @@
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
View File

@ -1,44 +0,0 @@
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
git.dev.alexdunmow.com/block/core v0.14.0 h1:cO6QQQPndhxwh1zbDvDOzebwrFS8Ka3WsxD8JBJLw/Y=
git.dev.alexdunmow.com/block/core v0.14.0/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
git.dev.alexdunmow.com/block/core v0.14.1 h1:63b25yzWoqrhBd3wdDFhsJzyGNM99wYmdh4WkN35hDk=
git.dev.alexdunmow.com/block/core v0.14.1/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,42 +0,0 @@
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
}

View File

@ -1,42 +0,0 @@
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>
}
}

View File

@ -1,312 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

View File

@ -1,82 +0,0 @@
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 Normal file
View File

@ -0,0 +1,378 @@
# 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; }

45
master_pages.json Normal file
View File

@ -0,0 +1,45 @@
[
{
"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 }
]
}
]

View File

@ -2,11 +2,8 @@
name = "pastel-dream"
display_name = "Pastel Dream"
scope = "@themes"
version = "0.1.1"
description = "Soft watercolor theme with blush, mint, and butter palettes for wellness, parenting, and spa brands."
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."
kind = "theme"
categories = ["templates"]
tags = ["pastel", "soft", "wellness", "parenting", "spa", "doula", "calm", "skincare", "mindfulness"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"
tags = ["pastel", "bakery", "patisserie", "cafe", "food", "soft", "rounded", "watercolor", "menu", "seasonal"]

View File

@ -1,88 +1,271 @@
[
{
"id": "blush-morning",
"name": "Blush Morning",
"description": "Default — warm peach paper with rose-blush primary and sage trim.",
"id": "morning-haze",
"name": "Morning Haze",
"description": "Blush rose on warm cream with a mint accent. The default patisserie counter at opening.",
"theme": {
"mode": "light",
"lightColors": {
"background": "25 60% 98%",
"foreground": "340 20% 22%",
"card": "0 0% 100%",
"cardForeground": "340 20% 22%",
"popover": "0 0% 100%",
"popoverForeground": "340 20% 22%",
"primary": "350 65% 72%",
"primaryForeground": "340 40% 18%",
"secondary": "150 35% 88%",
"secondaryForeground": "155 35% 22%",
"muted": "30 40% 95%",
"mutedForeground": "340 12% 48%",
"accent": "48 85% 80%",
"accentForeground": "35 50% 22%",
"destructive": "0 75% 65%",
"destructiveForeground": "0 0% 100%",
"border": "340 30% 90%",
"input": "340 30% 92%",
"ring": "350 65% 72%"
}
}
"mode": "both",
"typography": {
"fontHeading": "template:pastel-dream:Fraunces",
"fontBody": "template:pastel-dream:Quicksand"
},
{
"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%"
}
}
"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%"
},
{
"id": "twilight-petal",
"name": "Twilight Petal",
"description": "Dusky night-mode pastel — plum velvet with blush glow.",
"theme": {
"mode": "dark",
"darkColors": {
"background": "300 25% 12%",
"foreground": "30 40% 94%",
"card": "300 22% 16%",
"cardForeground": "30 40% 94%",
"popover": "300 22% 16%",
"popoverForeground": "30 40% 94%",
"primary": "345 70% 78%",
"primaryForeground": "340 40% 18%",
"secondary": "155 25% 32%",
"secondaryForeground": "150 30% 92%",
"muted": "300 18% 22%",
"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%",
"mutedForeground": "300 12% 70%",
"accent": "48 75% 72%",
"accentForeground": "35 50% 18%",
"destructive": "0 70% 60%",
"destructiveForeground": "0 0% 100%",
"border": "300 20% 28%",
"input": "300 20% 22%",
"ring": "345 70% 78%"
"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%"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -1,187 +0,0 @@
package main
import (
"context"
"github.com/a-h/templ"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// wrap adapts a templ-returning render function to templates.TemplateFunc.
// templ.Component already implements templates.HTMLComponent via Render.
func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc {
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
return f(ctx, doc)
}
}
// Register is the plugin entry point. 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,
},
},
},
}
}

View File

@ -1,38 +0,0 @@
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),
}
}

View File

@ -1,29 +0,0 @@
{
"$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"
}
}
}

View File

@ -1,39 +0,0 @@
{
"$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"
}
}
}
}

View File

@ -1,42 +0,0 @@
{
"$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"]
}
}
}
}

View File

@ -1,34 +0,0 @@
{
"$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"
}
}
}

View File

@ -1,40 +0,0 @@
{
"$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"
}
}
}

View File

@ -1,43 +0,0 @@
{
"$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"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

BIN
seed/media/croissants.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
seed/media/lemon-loaf.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

BIN
seed/media/sponge-cake.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

184
seed/seed.json Normal file
View File

@ -0,0 +1,184 @@
{
"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>" } }
]
}
]
}

25
seed/workflows.json Normal file
View File

@ -0,0 +1,25 @@
{
"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"
}
}
]
}
]
}

View File

@ -1,40 +0,0 @@
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
}

View File

@ -1,31 +0,0 @@
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>
}

View File

@ -1,117 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// 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

View File

@ -1,252 +0,0 @@
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))
}

View File

@ -1,510 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// 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

View File

@ -0,0 +1,99 @@
<!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: &amp;#39;Quicksand&amp;#39;, &amp;#39;Nunito&amp;#39;, -apple-system, BlinkMacSystemFont, &amp;#39;Segoe UI&amp;#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>

View File

@ -0,0 +1,21 @@
{# 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 %}

View File

@ -0,0 +1,15 @@
<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>

View File

@ -0,0 +1,32 @@
{# 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 %}

View File

@ -0,0 +1,25 @@
{# 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 %}

View File

@ -0,0 +1,20 @@
{% 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 %}

View File

@ -0,0 +1,12 @@
{% 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 %}

View File

@ -0,0 +1,22 @@
<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>

View File

@ -0,0 +1,12 @@
{% 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>

View File

@ -0,0 +1,5 @@
{% 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 %}

View File

@ -0,0 +1,12 @@
{% 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 %}

View File

@ -0,0 +1,17 @@
{% 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>

View File

@ -0,0 +1,17 @@
<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>

View File

@ -0,0 +1,39 @@
{% 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 %}

View File

@ -0,0 +1,42 @@
<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>

View File

@ -0,0 +1,14 @@
<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>

View File

@ -0,0 +1,17 @@
{% 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 %}

View File

@ -0,0 +1,40 @@
<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 %} &ndash; {{ 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>

View File

@ -0,0 +1,17 @@
<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>

View File

@ -0,0 +1,17 @@
<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>

Some files were not shown because too many files have changed in this diff Show More