Compare commits
6 Commits
5dfc1cd4be
...
e6348edd10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6348edd10 | ||
|
|
8eaf3a68aa | ||
|
|
834772eef4 | ||
|
|
d48114cb8f | ||
|
|
dcccb7dd8a | ||
|
|
1ee98d318a |
329
BUILD_REPORT.md
329
BUILD_REPORT.md
@ -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.
|
||||
|
||||
24
README.md
Normal file
24
README.md
Normal 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.
|
||||
@ -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
29
RECOMMENDED_ICONS.md
Normal 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`).
|
||||
@ -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
109
assets/fonts/OFL.txt
Normal 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.
|
||||
BIN
assets/fonts/fraunces-latin-400.woff2
Normal file
BIN
assets/fonts/fraunces-latin-400.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/fraunces-latin-600.woff2
Normal file
BIN
assets/fonts/fraunces-latin-600.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/quicksand-400.woff2
Normal file
BIN
assets/fonts/quicksand-400.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/quicksand-700.woff2
Normal file
BIN
assets/fonts/quicksand-700.woff2
Normal file
Binary file not shown.
@ -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.
|
||||
354
assets/style.css
354
assets/style.css
@ -1,133 +1,293 @@
|
||||
/* 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 {
|
||||
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%);
|
||||
/* --- Watercolor wash background ----------------------------------------- */
|
||||
/* The signature "morning haze": soft radial pastel washes tinted from the
|
||||
* active palette, drifting slowly. Reads as watercolor on cream in light mode
|
||||
* and as a dusk-lilac glow in dark mode — all from the same tokens. */
|
||||
body.pd-paper {
|
||||
background-color: hsl(var(--background));
|
||||
background-image:
|
||||
radial-gradient(60% 55% at 15% 12%, hsl(var(--primary) / 0.14) 0%, transparent 60%),
|
||||
radial-gradient(55% 50% at 85% 20%, hsl(var(--accent) / 0.12) 0%, transparent 58%),
|
||||
radial-gradient(65% 60% at 75% 85%, hsl(var(--secondary) / 0.40) 0%, transparent 62%),
|
||||
radial-gradient(50% 50% at 25% 90%, hsl(var(--primary) / 0.08) 0%, transparent 60%);
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
/* Guard against horizontal scroll on small screens from any off-canvas
|
||||
* chrome (mobile drawer). `clip` does not create a scroll container, so
|
||||
* it will not break the sticky navbar the way `hidden` can. */
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.bg-watercolor-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;
|
||||
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;
|
||||
/* --- Soft pill button (icing-smooth, lifts on hover) -------------------- */
|
||||
.pd-tag {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.7rem 1.5rem;
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
font-family: var(--font-body, var(--pd-body-fallback));
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
box-shadow: 0 8px 22px -8px hsl(var(--primary) / 0.45);
|
||||
transition: transform 220ms cubic-bezier(.22,1,.36,1), box-shadow 220ms cubic-bezier(.22,1,.36,1), background-color 220ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pd-tag:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 14px 30px -8px hsl(var(--primary) / 0.52);
|
||||
background-color: hsl(var(--primary) / 0.92);
|
||||
}
|
||||
.pd-tag:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.pd-tag--ghost {
|
||||
background-color: transparent;
|
||||
color: hsl(var(--primary));
|
||||
box-shadow: none;
|
||||
border: 1.5px solid hsl(var(--primary) / 0.45);
|
||||
}
|
||||
.pd-tag--ghost:hover {
|
||||
background-color: hsl(var(--primary) / 0.08);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pastel-pill:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px -6px hsl(var(--primary) / 0.50);
|
||||
background-color: hsl(var(--primary) / 0.92);
|
||||
/* --- Scalloped edges (piped-icing / doily trim) ------------------------- */
|
||||
.pd-torn-top {
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 6 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 L120 12 L0 12 Z' fill='black'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 6 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 L120 12 L0 12 Z' fill='black'/%3E%3C/svg%3E");
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
.pd-torn-bottom {
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L120 0 L120 6 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 Z' fill='black'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L120 0 L120 6 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 Z' fill='black'/%3E%3C/svg%3E");
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
.pastel-pill:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
/* --- Surfaces (rounded-everything) ------------------------------------- */
|
||||
.pd-card {
|
||||
background-color: hsl(var(--card));
|
||||
color: hsl(var(--card-foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--pd-radius);
|
||||
box-shadow: 0 12px 40px -14px hsl(var(--primary) / 0.22), 0 4px 12px -6px hsl(var(--primary) / 0.10);
|
||||
}
|
||||
.pd-frame {
|
||||
border: 1px solid hsl(var(--border));
|
||||
background-color: hsl(var(--card));
|
||||
border-radius: var(--pd-radius);
|
||||
}
|
||||
|
||||
.pastel-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);
|
||||
/* --- 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));
|
||||
}
|
||||
|
||||
.brush-underline {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
/* --- Hours: today highlight -------------------------------------------- */
|
||||
.pd-hours-today {
|
||||
background-color: hsl(var(--accent) / 0.16);
|
||||
color: hsl(var(--accent-foreground));
|
||||
border-left: 3px solid hsl(var(--accent));
|
||||
border-radius: 0 9999px 9999px 0;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.brush-underline::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;
|
||||
/* --- Location pin ------------------------------------------------------- */
|
||||
.pd-pin {
|
||||
color: hsl(var(--accent));
|
||||
}
|
||||
|
||||
.font-display {
|
||||
font-family: var(--font-heading, "Caveat Brush", "Sacramento", "Brush Script MT", cursive);
|
||||
letter-spacing: 0.01em;
|
||||
/* --- Soft badge (timeline markers) ------------------------------------- */
|
||||
.pd-stamp {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 9999px;
|
||||
background-color: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
border: 3px solid hsl(var(--card));
|
||||
box-shadow: 0 8px 20px -8px hsl(var(--accent) / 0.55);
|
||||
font-family: var(--font-heading, var(--pd-hand-fallback));
|
||||
font-size: 1.05rem;
|
||||
text-align: center;
|
||||
line-height: 1.05;
|
||||
transition: transform 200ms cubic-bezier(.22,1,.36,1);
|
||||
}
|
||||
.pd-stamp:hover {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
.pd-stamp-sm {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.font-body {
|
||||
font-family: var(--font-body, "Nunito", "Quicksand", "Helvetica Neue", system-ui, sans-serif);
|
||||
line-height: 1.75;
|
||||
/* --- 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-mono {
|
||||
font-family: var(--font-mono, "JetBrains Mono", "Fira Code", ui-monospace, monospace);
|
||||
@keyframes pd-draw {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Layout helpers (theme-owned, semantic tokens only) -----------------
|
||||
* Installed .bnp themes live outside the host Tailwind @source globs, so the
|
||||
* host build does NOT compile grid / aspect-ratio / gap / padding utilities
|
||||
* that appear only in this theme's own templates. The theme therefore owns
|
||||
* those layout primitives here as plain CSS: mobile-first, collapsing to a
|
||||
* single column on small screens, with explicit media queries so the render
|
||||
* is reliable regardless of the JIT content scan. No colors — layout only. */
|
||||
.pd-section { padding-top: 2.75rem; padding-bottom: 2.75rem; }
|
||||
@media (min-width: 768px) {
|
||||
.pd-section { padding-top: 4rem; padding-bottom: 4rem; }
|
||||
}
|
||||
|
||||
.pd-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2rem; }
|
||||
@media (min-width: 640px) {
|
||||
.pd-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.pd-grid--lg-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.pd-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.pd-grid--lg-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
/* Two-up menu board: one column on phones, two side by side from md. */
|
||||
.pd-menu-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2.5rem 3rem; }
|
||||
@media (min-width: 768px) {
|
||||
.pd-menu-cols--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
.pd-menu-list { margin-top: 1rem; }
|
||||
.pd-menu-list > li + li { margin-top: 1.25rem; }
|
||||
|
||||
/* Media frames: a fixed aspect box so a photo fills gracefully. The EMPTY
|
||||
* state is omitted in-template (no ballooning boxes when no photo is set). */
|
||||
.pd-media { position: relative; width: 100%; overflow: hidden; aspect-ratio: 4 / 3; }
|
||||
.pd-media--square { aspect-ratio: 1 / 1; }
|
||||
.pd-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
/* Card body padding (host may not emit p-5 for theme templates). */
|
||||
.pd-card-body { padding: 1.25rem 1.25rem 1.5rem; }
|
||||
@media (min-width: 768px) { .pd-card-body { padding: 1.5rem; } }
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
<section class="relative py-16 overflow-hidden animate-pastel-shimmer" style="background-image: linear-gradient(90deg, hsl(var(--background)) 0%, {% if palette == "mint" %}hsl(var(--secondary) / 0.55){% elif palette == "butter" %}hsl(var(--accent) / 0.55){% elif palette == "sky" %}hsl(var(--secondary) / 0.40){% else %}hsl(var(--primary) / 0.35){% endif %} 50%, hsl(var(--background)) 100%);" data-block="pastel-dream:affirmation" data-palette="{% if palette == "mint" or palette == "butter" or palette == "sky" %}{{ palette }}{% else %}blush{% endif %}">
|
||||
<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));">
|
||||
“{{ quote|default:"You are doing enough." }}”
|
||||
</p>
|
||||
{% if author %}<p class="font-body mt-4 text-sm uppercase tracking-[0.2em]" style="color: hsl(var(--muted-foreground));">
|
||||
{{ author }}
|
||||
</p>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
14
blocks/bakers_note.ninjatpl
Normal file
14
blocks/bakers_note.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
||||
<section data-block="pastel-dream:bakers_note" class="pd-section 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>
|
||||
30
blocks/bakers_note.schema.json
Normal file
30
blocks/bakers_note.schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,42 +1,33 @@
|
||||
# Pastel Dream theme block definitions (codeless .bnp, WO-WZ-021).
|
||||
# Keys are fully qualified (pastel-dream:<key>) because the codeless loader
|
||||
# registers definitions verbatim — there is no PluginBlockRegistry prefixing
|
||||
# in the codeless path (unlike the wasm/templ path). The qualified keys match
|
||||
# the hardcoded data-block attributes and the master_pages.json references.
|
||||
# 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:soft-navbar
|
||||
title: Soft Navbar
|
||||
description: Rounded pill navigation with a watercolor blob behind the logo.
|
||||
category: navigation
|
||||
schema: soft-navbar.schema.json
|
||||
template: soft-navbar.ninjatpl
|
||||
- key: pastel-dream:watercolor-hero
|
||||
title: Watercolor Hero
|
||||
description: Hero with two-tone watercolor blobs layered behind soft display type.
|
||||
category: theme
|
||||
schema: watercolor-hero.schema.json
|
||||
template: watercolor-hero.ninjatpl
|
||||
- key: pastel-dream:affirmation
|
||||
title: Affirmation Strip
|
||||
description: Slow-shimmer band carrying a short calming line in display type.
|
||||
category: theme
|
||||
schema: affirmation.schema.json
|
||||
template: affirmation.ninjatpl
|
||||
- key: pastel-dream:testimonial-soft
|
||||
title: Soft Testimonial
|
||||
description: Quote card with a deckle paper edge, avatar, and rating.
|
||||
category: theme
|
||||
schema: testimonial-soft.schema.json
|
||||
template: testimonial-soft.ninjatpl
|
||||
- key: pastel-dream:feature-grid-soft
|
||||
title: Feature Trio
|
||||
description: Three-up card grid for services, offerings, or core values.
|
||||
category: layout
|
||||
schema: feature-grid-soft.schema.json
|
||||
template: feature-grid-soft.ninjatpl
|
||||
- key: pastel-dream:cozy-footer
|
||||
title: Cozy Footer
|
||||
description: Newsletter signup, closing affirmation, menu column, and social pills.
|
||||
category: navigation
|
||||
schema: cozy-footer.schema.json
|
||||
template: cozy-footer.ninjatpl
|
||||
- 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
|
||||
|
||||
27
blocks/cake_gallery.ninjatpl
Normal file
27
blocks/cake_gallery.ninjatpl
Normal file
@ -0,0 +1,27 @@
|
||||
<section data-block="pastel-dream:cake_gallery" class="pd-section 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 pd-grid pd-grid--2 pd-grid--3">
|
||||
{% for cake in cakes %}
|
||||
<figure class="pd-frame overflow-hidden flex flex-col">
|
||||
{% if cake.image %}<div class="pd-media pd-media--square pd-torn-bottom bg-secondary">{% img cake.image alt=cake.name|default:"Cake" class="h-full w-full object-cover" %}</div>{% endif %}
|
||||
<figcaption class="pd-card-body">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% if cake.price %}<span class="pd-mono shrink-0 text-lg text-accent tabular-nums">{% if currency %}{{ currency }}{% endif %}{{ cake.price }}</span>{% endif %}
|
||||
</div>
|
||||
{% if cake.orderUrl %}<a href="{{ cake.orderUrl }}" class="pd-tag pd-tag--ghost mt-4 text-sm">Order this cake<svg 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>
|
||||
39
blocks/cake_gallery.schema.json
Normal file
39
blocks/cake_gallery.schema.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<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));">
|
||||
{{ affirmation|default:"Be gentle with yourself today." }}
|
||||
</p>
|
||||
{% if showSignup != "no" and showSignup != "false" and showSignup != "0" and showSignup != "off" and showSignup != false %}<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>{% endif %}
|
||||
{% if social %}<div class="flex justify-center gap-3 flex-wrap mt-6">
|
||||
{% for href in social %}<a href="{{ 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>{% endfor %}
|
||||
</div>{% endif %}
|
||||
<p class="font-mono text-xs mt-10" style="color: hsl(var(--muted-foreground));">
|
||||
Pastel Dream · with care
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
<section class="py-16 md:py-20" data-block="pastel-dream:feature-grid-soft">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
{% if intro %}<div class="font-body text-center max-w-2xl mx-auto mb-12 text-lg" style="color: hsl(var(--foreground) / 0.78);">
|
||||
{{ intro|safe }}
|
||||
</div>{% endif %}
|
||||
{% if items %}<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for item in 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>{% endif %}
|
||||
{% if item.title %}<h3 class="font-display text-2xl mb-3" style="color: hsl(var(--foreground));">{{ item.title }}</h3>{% endif %}
|
||||
{% if item.body %}<p class="font-body text-base" style="color: hsl(var(--foreground) / 0.78); line-height: 1.75;">{{ item.body }}</p>{% endif %}
|
||||
</article>{% endfor %}
|
||||
</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>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
blocks/order_ribbon.ninjatpl
Normal file
18
blocks/order_ribbon.ninjatpl
Normal file
@ -0,0 +1,18 @@
|
||||
<section data-block="pastel-dream:order_ribbon" class="pd-section 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 class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
37
blocks/order_ribbon.schema.json
Normal file
37
blocks/order_ribbon.schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
26
blocks/seasonal_specials.ninjatpl
Normal file
26
blocks/seasonal_specials.ninjatpl
Normal file
@ -0,0 +1,26 @@
|
||||
<section data-block="pastel-dream:seasonal_specials" class="pd-section 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 pd-grid pd-grid--2 pd-grid--3">
|
||||
{% for item in items %}
|
||||
<article class="pd-card overflow-hidden flex flex-col">
|
||||
{% if item.image %}<div class="pd-media bg-secondary">{% img item.image alt=item.name|default:"Seasonal special" class="h-full w-full object-cover" %}</div>{% endif %}
|
||||
<div class="pd-card-body">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<h3 class="pd-display text-xl text-primary">{{ item.name }}</h3>
|
||||
{% if item.price %}<span class="pd-mono shrink-0 text-lg text-accent tabular-nums">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
|
||||
</div>
|
||||
{% if item.badge %}<span class="pd-hand mt-2 inline-flex rounded-full bg-accent/15 px-3 py-0.5 text-sm text-accent">{{ item.badge }}</span>{% endif %}
|
||||
{% 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>
|
||||
46
blocks/seasonal_specials.schema.json
Normal file
46
blocks/seasonal_specials.schema.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<nav class="w-full px-4 py-4" data-block="pastel-dream:soft-navbar" data-menu="{{ 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);">
|
||||
{{ logo|default:"Pastel Dream" }}
|
||||
</a>
|
||||
<div class="hidden md:flex items-center gap-6 font-body text-sm" data-pastel-menu="{{ 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 ctaText %}<a href="{{ ctaHref|default:"#" }}" class="pastel-pill text-sm">
|
||||
{{ ctaText }}
|
||||
</a>{% endif %}
|
||||
<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>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
<figure class="pastel-card deckle-edge p-8 max-w-xl mx-auto" data-block="pastel-dream:testimonial-soft">
|
||||
{% if rating|integer > 0 %}<div class="flex gap-1 mb-4" aria-label="Rating">
|
||||
{% if rating|integer >= 1 %}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>{% endif %}{% if rating|integer >= 2 %}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>{% endif %}{% if rating|integer >= 3 %}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>{% endif %}{% if rating|integer >= 4 %}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>{% endif %}{% if rating|integer >= 5 %}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>{% endif %}
|
||||
</div>{% endif %}
|
||||
<blockquote class="font-body text-lg leading-relaxed" style="color: hsl(var(--card-foreground));">
|
||||
{{ quote|safe }}
|
||||
</blockquote>
|
||||
{% if name or role %}<figcaption class="flex items-center gap-4 mt-6 pt-6 border-t" style="border-color: hsl(var(--border));">
|
||||
{% if avatar %}<img src="{{ avatar }}" alt="{{ name }}" class="w-12 h-12 rounded-full object-cover">{% elif name %}<span class="w-12 h-12 rounded-full flex items-center justify-center font-display text-lg" style="background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground));">
|
||||
{% for w in name|split:" " %}{% if w and forloop.Counter <= 2 %}{{ w|first|upper }}{% endif %}{% endfor %}
|
||||
</span>{% endif %}
|
||||
<div>
|
||||
{% if name %}<p class="font-display text-xl leading-tight" style="color: hsl(var(--foreground));">{{ name }}</p>{% endif %}
|
||||
{% if role %}<p class="font-body text-sm" style="color: hsl(var(--muted-foreground));">{{ role }}</p>{% endif %}
|
||||
</div>
|
||||
</figcaption>{% endif %}
|
||||
</figure>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
<section class="relative overflow-hidden py-20 md:py-28 bg-watercolor-blush" data-block="pastel-dream:watercolor-hero">
|
||||
<svg class="absolute -top-12 -left-12 w-[28rem] h-[28rem] -z-0 animate-breathe" viewBox="0 0 400 400" aria-hidden="true">
|
||||
<path d="M200,40 C290,40 360,120 360,200 C360,290 280,360 200,360 C110,360 40,280 40,200 C40,110 110,40 200,40 Z" fill="hsl(var(--primary) / 0.22)"></path>
|
||||
</svg>
|
||||
<svg class="absolute -bottom-16 -right-8 w-[24rem] h-[24rem] -z-0 animate-breathe" viewBox="0 0 400 400" aria-hidden="true">
|
||||
<path d="M120,60 C200,40 320,80 360,180 C400,280 320,360 220,360 C120,360 40,300 40,220 C40,160 60,80 120,60 Z" fill="hsl(var(--accent) / 0.30)"></path>
|
||||
</svg>
|
||||
<div class="relative z-10 max-w-6xl mx-auto px-4 grid md:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
{% if eyebrow %}<p class="font-mono text-xs uppercase tracking-[0.2em] mb-4" style="color: hsl(var(--muted-foreground));">
|
||||
{{ eyebrow }}
|
||||
</p>{% endif %}
|
||||
<h1 class="font-display brush-underline text-5xl md:text-6xl leading-tight mb-6" style="color: hsl(var(--foreground));">
|
||||
{{ headline|default:"Soft starts"|safe }}
|
||||
</h1>
|
||||
{% if body %}<div class="font-body text-lg max-w-prose mb-8" style="color: hsl(var(--foreground) / 0.78);">
|
||||
{{ body|safe }}
|
||||
</div>{% endif %}
|
||||
{% if ctaText %}<a href="{{ ctaHref|default:"#" }}" class="pastel-pill text-base animate-breathe">
|
||||
{{ ctaText }}
|
||||
</a>{% endif %}
|
||||
</div>
|
||||
{% if image %}<div class="relative">
|
||||
<div class="pastel-card overflow-hidden">
|
||||
<img src="{{ image }}" alt="" class="w-full h-auto block">
|
||||
</div>
|
||||
</div>{% else %}<div class="relative h-64 md:h-80" aria-hidden="true">
|
||||
<svg viewBox="0 0 300 240" class="w-full h-full">
|
||||
<ellipse cx="150" cy="120" rx="110" ry="80" fill="hsl(var(--secondary) / 0.55)"></ellipse>
|
||||
<ellipse cx="120" cy="100" rx="70" ry="50" fill="hsl(var(--accent) / 0.45)"></ellipse>
|
||||
<ellipse cx="180" cy="140" rx="60" ry="44" fill="hsl(var(--primary) / 0.40)"></ellipse>
|
||||
</svg>
|
||||
</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
fonts.json
19
fonts.json
@ -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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
531
manifest.yaml
531
manifest.yaml
@ -1,231 +1,402 @@
|
||||
# Pastel Dream theme — codeless .bnp manifest (WO-WZ-021 Phase B1).
|
||||
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
|
||||
# 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 watercolor theme with blush, mint, and butter palettes for wellness, parenting, and spa brands."
|
||||
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: "Soft layered hero, masthead nav, footer with newsletter."
|
||||
description: "Header, main and footer over a soft watercolor wash."
|
||||
slots: [header, main, footer]
|
||||
- system: pastel-dream
|
||||
key: landing
|
||||
title: Soft Landing
|
||||
description: "Watercolor hero, affirmation strip, CTA, footer."
|
||||
slots: [hero, main, cta, footer]
|
||||
title: Landing
|
||||
description: "Hero, menu, story and an invitation to order."
|
||||
slots: [hero, menu, story, cta, footer]
|
||||
- system: pastel-dream
|
||||
key: article
|
||||
title: Reading Room
|
||||
description: "Narrow, centered editorial column with side rail."
|
||||
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: Open Air
|
||||
description: "Edge-to-edge sections for galleries and seasonal looks."
|
||||
title: Full Width
|
||||
description: "Edge-to-edge galleries and counter shots."
|
||||
slots: [header, main, footer]
|
||||
|
||||
template_overrides:
|
||||
- { template: pastel-dream, block: heading }
|
||||
- { template: pastel-dream, block: text }
|
||||
- { template: pastel-dream, block: button }
|
||||
- { template: pastel-dream, block: card }
|
||||
- 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-specific CSS appended to host Tailwind input.
|
||||
/* Pastel Dream theme styles — bakery / patisserie.
|
||||
*
|
||||
* 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.
|
||||
* 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;
|
||||
}
|
||||
|
||||
/* --- 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%);
|
||||
|
||||
/* --- Watercolor wash background ----------------------------------------- */
|
||||
/* The signature "morning haze": soft radial pastel washes tinted from the
|
||||
* active palette, drifting slowly. Reads as watercolor on cream in light mode
|
||||
* and as a dusk-lilac glow in dark mode — all from the same tokens. */
|
||||
body.pd-paper {
|
||||
background-color: hsl(var(--background));
|
||||
background-image:
|
||||
radial-gradient(60% 55% at 15% 12%, hsl(var(--primary) / 0.14) 0%, transparent 60%),
|
||||
radial-gradient(55% 50% at 85% 20%, hsl(var(--accent) / 0.12) 0%, transparent 58%),
|
||||
radial-gradient(65% 60% at 75% 85%, hsl(var(--secondary) / 0.40) 0%, transparent 62%),
|
||||
radial-gradient(50% 50% at 25% 90%, hsl(var(--primary) / 0.08) 0%, transparent 60%);
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
/* Guard against horizontal scroll on small screens from any off-canvas
|
||||
* chrome (mobile drawer). `clip` does not create a scroll container, so
|
||||
* it will not break the sticky navbar the way `hidden` can. */
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.bg-watercolor-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;
|
||||
}
|
||||
|
||||
/* --- 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);
|
||||
.pd-body {
|
||||
font-family: var(--font-body, var(--pd-body-fallback));
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.shadow-soft-hover {
|
||||
box-shadow: 0 16px 48px -8px hsl(var(--primary) / 0.24),
|
||||
0 6px 16px -2px hsl(var(--primary) / 0.14);
|
||||
.pd-mono {
|
||||
font-family: var(--font-mono, var(--pd-mono-fallback));
|
||||
}
|
||||
|
||||
/* --- 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));
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* --- 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;
|
||||
|
||||
/* --- 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;
|
||||
}
|
||||
|
||||
.pastel-pill:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px -6px hsl(var(--primary) / 0.50);
|
||||
background-color: hsl(var(--primary) / 0.92);
|
||||
|
||||
/* --- 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:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
|
||||
/* --- 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;
|
||||
}
|
||||
|
||||
/* --- 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);
|
||||
.pd-tag:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 14px 30px -8px hsl(var(--primary) / 0.52);
|
||||
background-color: hsl(var(--primary) / 0.92);
|
||||
}
|
||||
|
||||
/* --- Brush-stroke underline for headings ------------------------------ */
|
||||
.brush-underline {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
.pd-tag:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.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;
|
||||
.pd-tag--ghost {
|
||||
background-color: transparent;
|
||||
color: hsl(var(--primary));
|
||||
box-shadow: none;
|
||||
border: 1.5px solid hsl(var(--primary) / 0.45);
|
||||
}
|
||||
|
||||
/* --- 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;
|
||||
.pd-tag--ghost:hover {
|
||||
background-color: hsl(var(--primary) / 0.08);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.font-body {
|
||||
font-family: var(--font-body, "Nunito", "Quicksand", "Helvetica Neue", system-ui, sans-serif);
|
||||
line-height: 1.75;
|
||||
|
||||
/* --- 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%;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: var(--font-mono, "JetBrains Mono", "Fira Code", ui-monospace, monospace);
|
||||
.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%;
|
||||
}
|
||||
|
||||
/* --- 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;
|
||||
}
|
||||
|
||||
/* --- 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);
|
||||
}
|
||||
|
||||
/* @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;
|
||||
}
|
||||
.pd-frame {
|
||||
border: 1px solid hsl(var(--border));
|
||||
background-color: hsl(var(--card));
|
||||
border-radius: var(--pd-radius);
|
||||
}
|
||||
|
||||
.animate-pastel-shimmer {
|
||||
background-size: 200% 200%;
|
||||
animation: pastel-shimmer 8s ease-in-out infinite;
|
||||
|
||||
/* --- 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);
|
||||
}
|
||||
|
||||
.animate-breathe {
|
||||
animation: breathe 6s ease-in-out infinite;
|
||||
.pd-chalk-rule {
|
||||
border-color: hsl(var(--border));
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
/* --- Accessibility: respect prefers-reduced-motion -------------------- */
|
||||
.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) {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Layout helpers (theme-owned, semantic tokens only) -----------------
|
||||
* Installed .bnp themes live outside the host Tailwind @source globs, so the
|
||||
* host build does NOT compile grid / aspect-ratio / gap / padding utilities
|
||||
* that appear only in this theme's own templates. The theme therefore owns
|
||||
* those layout primitives here as plain CSS: mobile-first, collapsing to a
|
||||
* single column on small screens, with explicit media queries so the render
|
||||
* is reliable regardless of the JIT content scan. No colors — layout only. */
|
||||
.pd-section { padding-top: 2.75rem; padding-bottom: 2.75rem; }
|
||||
@media (min-width: 768px) {
|
||||
.pd-section { padding-top: 4rem; padding-bottom: 4rem; }
|
||||
}
|
||||
|
||||
.pd-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2rem; }
|
||||
@media (min-width: 640px) {
|
||||
.pd-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.pd-grid--lg-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.pd-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.pd-grid--lg-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
/* Two-up menu board: one column on phones, two side by side from md. */
|
||||
.pd-menu-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2.5rem 3rem; }
|
||||
@media (min-width: 768px) {
|
||||
.pd-menu-cols--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
.pd-menu-list { margin-top: 1rem; }
|
||||
.pd-menu-list > li + li { margin-top: 1.25rem; }
|
||||
|
||||
/* Media frames: a fixed aspect box so a photo fills gracefully. The EMPTY
|
||||
* state is omitted in-template (no ballooning boxes when no photo is set). */
|
||||
.pd-media { position: relative; width: 100%; overflow: hidden; aspect-ratio: 4 / 3; }
|
||||
.pd-media--square { aspect-ratio: 1 / 1; }
|
||||
.pd-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
/* Card body padding (host may not emit p-5 for theme templates). */
|
||||
.pd-card-body { padding: 1.25rem 1.25rem 1.5rem; }
|
||||
@media (min-width: 768px) { .pd-card-body { padding: 1.5rem; } }
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
{
|
||||
"key": "pastel-dream:default-master",
|
||||
"title": "Pastel Dream Default Master",
|
||||
"page_templates": ["default", "article"],
|
||||
"page_templates": ["default", "article", "full-width", "contact"],
|
||||
"blocks": [
|
||||
{ "block_key": "pastel-dream:soft-navbar", "title": "Soft Nav", "content": { "menuName": "main", "logo": "Pastel Dream" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Main Slot", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "pastel-dream:cozy-footer", "title": "Cozy Footer", "content": { "showSignup": "yes", "affirmation": "Be gentle with yourself today." }, "slot": "footer", "sort_order": 0 }
|
||||
{ "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 }
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -14,10 +14,32 @@
|
||||
"title": "Pastel Dream Landing Master",
|
||||
"page_templates": ["landing"],
|
||||
"blocks": [
|
||||
{ "block_key": "pastel-dream:watercolor-hero", "title": "Watercolor Hero", "content": { "eyebrow": "new", "headline": "Soft starts", "body": "A gentle place to begin.", "ctaText": "Begin" }, "slot": "hero", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Main Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "pastel-dream:affirmation", "title": "Affirmation Strip", "content": { "quote": "You are doing enough.", "author": "Pastel Dream" }, "slot": "cta", "sort_order": 0 },
|
||||
{ "block_key": "pastel-dream:cozy-footer", "title": "Cozy Footer", "content": { "showSignup": "yes" }, "slot": "footer", "sort_order": 0 }
|
||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Petal & Crumb" }, "slot": "hero", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Hero", "content": { "slotName": "hero", "placeholder": "Drop a hero block" }, "slot": "hero", "sort_order": 1 },
|
||||
{ "block_key": "slot", "title": "Menu", "content": { "slotName": "menu", "placeholder": "Feature the menu" }, "slot": "menu", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Story", "content": { "slotName": "story", "placeholder": "Tell the story" }, "slot": "story", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Call To Action", "content": { "slotName": "cta", "placeholder": "Invite them to order" }, "slot": "cta", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning." }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pastel-dream:blog-master",
|
||||
"title": "Pastel Dream Blog Master",
|
||||
"page_templates": ["blog-index"],
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Petal & Crumb" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Featured", "content": { "slotName": "featured", "placeholder": "Featured posts" }, "slot": "featured", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Posts", "content": { "slotName": "main", "placeholder": "Post grid" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning." }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pastel-dream:auth-master",
|
||||
"title": "Pastel Dream Auth Master",
|
||||
"page_templates": ["auth"],
|
||||
"blocks": [
|
||||
{ "block_key": "slot", "title": "Auth Form", "content": { "slotName": "main", "placeholder": "Auth form" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Footer Note", "content": { "slotName": "footer", "placeholder": "Small print" }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
name = "pastel-dream"
|
||||
display_name = "Pastel Dream"
|
||||
scope = "@themes"
|
||||
version = "0.2.0"
|
||||
description = "Soft watercolor theme with blush, mint, and butter palettes for wellness, parenting, and spa brands."
|
||||
version = "0.2.2"
|
||||
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"]
|
||||
tags = ["pastel", "bakery", "patisserie", "cafe", "food", "soft", "rounded", "watercolor", "menu", "seasonal"]
|
||||
|
||||
321
presets.json
321
presets.json
@ -1,88 +1,251 @@
|
||||
[
|
||||
{
|
||||
"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",
|
||||
"mode": "both",
|
||||
"lightColors": {
|
||||
"background": "25 60% 98%",
|
||||
"foreground": "340 20% 22%",
|
||||
"card": "0 0% 100%",
|
||||
"cardForeground": "340 20% 22%",
|
||||
"popover": "0 0% 100%",
|
||||
"popoverForeground": "340 20% 22%",
|
||||
"primary": "350 65% 72%",
|
||||
"primaryForeground": "340 40% 18%",
|
||||
"secondary": "150 35% 88%",
|
||||
"secondaryForeground": "155 35% 22%",
|
||||
"muted": "30 40% 95%",
|
||||
"mutedForeground": "340 12% 48%",
|
||||
"accent": "48 85% 80%",
|
||||
"accentForeground": "35 50% 22%",
|
||||
"destructive": "0 75% 65%",
|
||||
"destructiveForeground": "0 0% 100%",
|
||||
"border": "340 30% 90%",
|
||||
"input": "340 30% 92%",
|
||||
"ring": "350 65% 72%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mint-meadow",
|
||||
"name": "Mint Meadow",
|
||||
"description": "Sage-led variant for spa and doula brands; rose drops to accent.",
|
||||
"theme": {
|
||||
"mode": "light",
|
||||
"lightColors": {
|
||||
"background": "150 40% 97%",
|
||||
"foreground": "155 35% 18%",
|
||||
"card": "0 0% 100%",
|
||||
"cardForeground": "155 35% 18%",
|
||||
"popover": "0 0% 100%",
|
||||
"popoverForeground": "155 35% 18%",
|
||||
"primary": "155 40% 60%",
|
||||
"primaryForeground": "0 0% 100%",
|
||||
"secondary": "350 50% 92%",
|
||||
"secondaryForeground": "345 40% 28%",
|
||||
"muted": "150 25% 94%",
|
||||
"mutedForeground": "155 18% 42%",
|
||||
"accent": "48 80% 82%",
|
||||
"accentForeground": "35 50% 22%",
|
||||
"destructive": "5 70% 62%",
|
||||
"destructiveForeground": "0 0% 100%",
|
||||
"border": "150 25% 88%",
|
||||
"input": "150 25% 92%",
|
||||
"ring": "155 40% 60%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "twilight-petal",
|
||||
"name": "Twilight Petal",
|
||||
"description": "Dusky night-mode pastel — plum velvet with blush glow.",
|
||||
"theme": {
|
||||
"mode": "dark",
|
||||
"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%"
|
||||
},
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
176
seed/seed.json
Normal file
176
seed/seed.json
Normal file
@ -0,0 +1,176 @@
|
||||
{
|
||||
"demo": true,
|
||||
"settings": {
|
||||
"merge": {
|
||||
"site_title": "Petal & Crumb",
|
||||
"site_description": "A small patisserie baking cakes, pastries and seasonal treats."
|
||||
}
|
||||
},
|
||||
"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": [
|
||||
{ "name": "Strawberry tart", "note": "Vanilla custard, fresh strawberries, a thin glaze.", "price": "8.00", "badge": "New" },
|
||||
{ "name": "Pistachio croissant", "note": "Laminated overnight, filled with pistachio cream.", "price": "6.50", "badge": "Popular" },
|
||||
{ "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" } },
|
||||
{ "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.</p><p>This week they go into the strawberry tart and a plain sponge with cream. When they run out, we move on to the next fruit.</p><p>Come in early. The tarts tend to go before lunch.</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": [
|
||||
{ "name": "Strawberry tart", "flavour": "Vanilla custard", "price": "8.00", "orderUrl": "/contact" },
|
||||
{ "name": "Strawberry sponge", "flavour": "Cream and jam", "price": "7.00", "orderUrl": "/contact" },
|
||||
{ "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
25
seed/workflows.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,98 +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 {
|
||||
-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="background-color: {{ colors.background|default:"#fdf7f1" }}; margin: 0; padding: 0; font-family: &#39;Nunito&#39;, &#39;Quicksand&#39;, Helvetica, Arial, sans-serif;">
|
||||
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden;">
|
||||
{{ preview_text }}
|
||||
</div>{% endif %}
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: {{ colors.background|default:"#fdf7f1" }};">
|
||||
<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="{{ colors.primary|default:"#e9a1ad" }}"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<table role="presentation" class="pastel-email-frame" width="560" cellspacing="0" cellpadding="0" border="0" style="max-width: 560px; background-color: {{ colors.card|default:"#ffffff" }}; border-radius: 20px; border: 1px solid {{ colors.border|default:"#f0d8de" }};">
|
||||
<!-- Masthead -->
|
||||
<tr>
|
||||
<td align="center" class="pastel-pad" style="padding: 36px 40px 8px; border-bottom: 1px solid {{ colors.border|default:"#f0d8de" }};">
|
||||
{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 56px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-family: &#39;Caveat Brush&#39;, &#39;Sacramento&#39;, cursive; font-size: 36px; font-weight: 400; color: {{ colors.foreground|default:"#432e36" }};">
|
||||
{{ site_name }}
|
||||
</h1>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Body -->
|
||||
<tr>
|
||||
<td class="pastel-pad" style="padding: 32px 40px 24px; color: {{ colors.foreground|default:"#432e36" }}; font-size: 16px; line-height: 1.75;">
|
||||
{{ body|safe }}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td class="pastel-pad" align="center" style="padding: 24px 40px 36px; border-top: 1px solid {{ colors.border|default:"#f0d8de" }};">
|
||||
<p style="margin: 0 0 12px; font-family: &#39;Caveat Brush&#39;, &#39;Sacramento&#39;, cursive; font-size: 18px; color: {{ colors.mutedForeground|default:"#80707a" }};">
|
||||
Be gentle with yourself today.
|
||||
</p>
|
||||
{% if site_url %}<p style="margin: 0 0 12px; font-size: 13px; color: {{ colors.mutedForeground|default:"#80707a" }};">
|
||||
<a href="{{ site_url }}" style="color: {{ colors.primary|default:"#e9a1ad" }}; text-decoration: none;">
|
||||
{{ site_url }}
|
||||
</a>
|
||||
</p>{% endif %}
|
||||
{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px; color: {{ colors.mutedForeground|default:"#80707a" }};">
|
||||
<a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#80707a" }}; text-decoration: underline;">
|
||||
Unsubscribe
|
||||
</a>
|
||||
</p>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>{{ site_name }}</title>
|
||||
<style type="text/css">
|
||||
body, table, td, p, a, li, blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
@media only screen and (max-width: 620px) {
|
||||
.pd-email-container {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.pd-email-padding {
|
||||
padding-left: 24px !important;
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: {{ colors.background|default:"#faf4ed" }}; margin: 0; padding: 0; font-family: &#39;Quicksand&#39;, &#39;Nunito&#39;, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;">
|
||||
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 10px; background-color: {{ colors.background|default:"#faf4ed" }};">
|
||||
<table role="presentation" class="pd-email-container" width="560" cellspacing="0" cellpadding="0" border="0" style="max-width: 560px; background-color: {{ colors.card|default:"#fdf9f4" }}; border: 1px solid {{ colors.border|default:"#e5d6c9" }}; border-radius: 4px;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 32px 40px 16px; border-bottom: 1px dashed {{ colors.border|default:"#e5d6c9" }};">
|
||||
{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 48px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-size: 24px; letter-spacing: -0.01em; color: {{ colors.primary|default:"#d05a83" }};">{{ site_name }}</h1>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pd-email-padding" style="padding: 32px 40px; color: {{ colors.foreground|default:"#47323c" }}; font-size: 16px; line-height: 1.65;">{{ body|safe }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 16px 40px 8px;">
|
||||
<!-- inline SVG doodle divider, survives Outlook -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0"><tr><td>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="32" viewBox="0 0 200 32" fill="none" stroke="{{ colors.primary|default:"#d05a83" }}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M4 18 L80 18"></path>
|
||||
<path d="M120 18 L196 18"></path>
|
||||
<ellipse cx="95" cy="18" rx="6" ry="9"></ellipse>
|
||||
<path d="M95 9 L95 27"></path>
|
||||
<ellipse cx="108" cy="18" rx="6" ry="9"></ellipse>
|
||||
<path d="M108 9 L108 27"></path>
|
||||
</svg>
|
||||
</td></tr></table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 16px 40px 32px; color: {{ colors.mutedForeground|default:"#79626c" }}; font-size: 12px; line-height: 1.6;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center">
|
||||
{% if site_name %}<p style="margin: 0 0 6px; font-size: 13px; color: {{ colors.foreground|default:"#47323c" }};">{{ site_name }}</p>{% endif %}
|
||||
<p style="margin: 0 0 6px;">Fresh bakes from the morning. Cakes to order all week.</p>
|
||||
{% if site_url %}<p style="margin: 0 0 8px;"><a href="{{ site_url }}" style="color: {{ colors.primary|default:"#d05a83" }}; text-decoration: none; border-bottom: 1px dashed {{ colors.primary|default:"#d05a83" }};">{{ site_url }}</a></p>{% endif %}
|
||||
{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#79626c" }}; text-decoration: none;">Unsubscribe</a></p>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
21
templates/overrides/pastel-dream/announcement-bar.ninjatpl
Normal file
21
templates/overrides/pastel-dream/announcement-bar.ninjatpl
Normal 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 %}
|
||||
15
templates/overrides/pastel-dream/audio-embed.ninjatpl
Normal file
15
templates/overrides/pastel-dream/audio-embed.ninjatpl
Normal 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 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 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>
|
||||
32
templates/overrides/pastel-dream/auth-form.ninjatpl
Normal file
32
templates/overrides/pastel-dream/auth-form.ninjatpl
Normal 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 %}
|
||||
25
templates/overrides/pastel-dream/auth-status.ninjatpl
Normal file
25
templates/overrides/pastel-dream/auth-status.ninjatpl
Normal 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 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 %}
|
||||
20
templates/overrides/pastel-dream/author-bio-hero.ninjatpl
Normal file
20
templates/overrides/pastel-dream/author-bio-hero.ninjatpl
Normal 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 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 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 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 class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
|
||||
</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
12
templates/overrides/pastel-dream/author-bio.ninjatpl
Normal file
12
templates/overrides/pastel-dream/author-bio.ninjatpl
Normal 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 %}
|
||||
22
templates/overrides/pastel-dream/blog-hero.ninjatpl
Normal file
22
templates/overrides/pastel-dream/blog-hero.ninjatpl
Normal 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>
|
||||
12
templates/overrides/pastel-dream/breadcrumbs.ninjatpl
Normal file
12
templates/overrides/pastel-dream/breadcrumbs.ninjatpl
Normal 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 class="mx-2 h-4 w-4 text-accent" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"></use></svg>{% else %}<span class="mx-2 text-accent">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
|
||||
{% if item.is_current %}{% if showCurrent %}<span class="pd-display text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-muted-foreground transition-colors hover:text-accent">{{ item.label }}</a>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
@ -1,3 +1,5 @@
|
||||
<a href="{{ href|default:"#" }}" class="pastel-pill" data-block-type="button" data-variant="{{ variant|default:"primary" }}">
|
||||
{{ text|default:"Continue" }}
|
||||
</a>
|
||||
{% 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 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 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 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 class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</button>{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
<div class="pastel-card p-6" data-block-type="card" style="border-radius: 20px;">
|
||||
{% if title %}<h3 class="font-display text-2xl mb-3" style="color: hsl(var(--card-foreground));">{{ title }}</h3>{% endif %}
|
||||
{% if body %}<div class="font-body" style="color: hsl(var(--card-foreground) / 0.85); line-height: 1.75;">
|
||||
{{ body|safe }}
|
||||
</div>{% endif %}
|
||||
{% if footer %}<div class="mt-6 pt-4 border-t font-body text-sm" style="border-color: hsl(var(--border)); color: hsl(var(--muted-foreground));">
|
||||
{{ footer|safe }}
|
||||
</div>{% endif %}
|
||||
{% 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 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 %}
|
||||
|
||||
17
templates/overrides/pastel-dream/category-list.ninjatpl
Normal file
17
templates/overrides/pastel-dream/category-list.ninjatpl
Normal 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>
|
||||
17
templates/overrides/pastel-dream/contact-card.ninjatpl
Normal file
17
templates/overrides/pastel-dream/contact-card.ninjatpl
Normal 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 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>
|
||||
39
templates/overrides/pastel-dream/contact-form.ninjatpl
Normal file
39
templates/overrides/pastel-dream/contact-form.ninjatpl
Normal 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 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 %}
|
||||
42
templates/overrides/pastel-dream/countdown.ninjatpl
Normal file
42
templates/overrides/pastel-dream/countdown.ninjatpl
Normal 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 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>
|
||||
14
templates/overrides/pastel-dream/cta.ninjatpl
Normal file
14
templates/overrides/pastel-dream/cta.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
||||
<section class="pd-section 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 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>
|
||||
17
templates/overrides/pastel-dream/divider.ninjatpl
Normal file
17
templates/overrides/pastel-dream/divider.ninjatpl
Normal 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 %}
|
||||
40
templates/overrides/pastel-dream/event-list.ninjatpl
Normal file
40
templates/overrides/pastel-dream/event-list.ninjatpl
Normal 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 %} – {{ 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 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>
|
||||
17
templates/overrides/pastel-dream/faq.ninjatpl
Normal file
17
templates/overrides/pastel-dream/faq.ninjatpl
Normal 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 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>
|
||||
17
templates/overrides/pastel-dream/feature-grid.ninjatpl
Normal file
17
templates/overrides/pastel-dream/feature-grid.ninjatpl
Normal 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 pd-grid pd-grid--2 {% if columns == 2 %}pd-grid--lg-2{% elif columns == 4 %}pd-grid--lg-4{% else %}pd-grid--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 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 class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
21
templates/overrides/pastel-dream/featured-posts.ninjatpl
Normal file
21
templates/overrides/pastel-dream/featured-posts.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
||||
<section class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
{% if heading %}<h2 class="pd-display mb-8 text-2xl text-primary md:text-3xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||
<div class="grid grid-cols-1 gap-6 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 post in posts %}
|
||||
<article class="pd-card group flex flex-col overflow-hidden transition-shadow hover:shadow-md">
|
||||
{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-secondary">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
||||
<div class="flex flex-1 flex-col p-5">
|
||||
<h3 class="pd-display text-lg text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
|
||||
{% if showExcerpt and post.excerpt %}<p class="pd-body mt-2 flex-1 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
|
||||
<div class="pd-hand mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 text-base text-accent">
|
||||
{% if showAuthor and post.author_name %}<span>{{ post.author_name }}</span>{% endif %}
|
||||
{% if showDate and post.published_at %}<span class="text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}
|
||||
{% if post.reading_time %}<span class="text-muted-foreground">{{ post.reading_time }} min</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
38
templates/overrides/pastel-dream/footer.ninjatpl
Normal file
38
templates/overrides/pastel-dream/footer.ninjatpl
Normal file
@ -0,0 +1,38 @@
|
||||
{# pastel-dream footer — scalloped top edge, soft pastel chrome, builtin footer_menus provider. #}
|
||||
{% set fl = layout %}
|
||||
{% if not fl %}{% if template == "centered" %}{% set fl = "centered" %}{% else %}{% set fl = "columns" %}{% endif %}{% endif %}
|
||||
{% macro sociallinks(cls) %}{% if social_items %}<div class="flex items-center gap-3 {{ cls }}">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="text-muted-foreground transition-colors hover:text-accent"><span class="sr-only">{{ s.label }}</span>{% if s.icon %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ s.icon|split:":"|first }}.svg#{{ s.icon|split:":"|last }}"></use></svg>{% else %}{{ s.label }}{% endif %}</a>{% endfor %}</div>{% endif %}{% endmacro %}
|
||||
{% macro newsletter() %}{% if showNewsletter %}<div class="max-w-sm"><h3 class="pd-hand text-2xl text-accent">{{ newsletterTitle|default:"Stay in the loop" }}</h3>{% if newsletterDescription %}<p class="pd-body mt-1 text-sm text-muted-foreground">{{ newsletterDescription }}</p>{% endif %}<div class="mt-3">{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div></div>{% endif %}{% endmacro %}
|
||||
{% macro legalbar() %}<div class="mt-10 flex flex-col items-center justify-between gap-3 border-t border-dashed border-border pt-6 text-sm text-muted-foreground sm:flex-row"><p class="pd-body">{% if copyright %}© {{ copyright }}{% else %}© {{ companyName }}{% endif %}</p>{% if legal_items %}<nav class="flex flex-wrap items-center gap-x-4 gap-y-1">{% for l in legal_items %}<a href="{{ l.url|default:'#' }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body transition-colors hover:text-foreground">{{ l.label }}</a>{% endfor %}</nav>{% endif %}</div>{% endmacro %}
|
||||
<footer class="pd-torn-top w-full bg-muted/50 text-foreground {{ class }}">
|
||||
<div class="{% if fullWidth %}w-full px-4 sm:px-6 lg:px-8{% else %}mx-auto max-w-7xl px-4 sm:px-6 lg:px-8{% endif %} py-16">
|
||||
{% if fl == "minimal" %}
|
||||
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||
<div class="flex items-center gap-3">{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-8 w-auto"/>{% elif companyName %}<span class="pd-display text-xl text-primary">{{ companyName }}</span>{% endif %}{% if tagline %}<span class="pd-hand hidden text-lg text-accent sm:inline">{{ tagline }}</span>{% endif %}</div>
|
||||
{{ sociallinks("") }}
|
||||
</div>
|
||||
{{ legalbar() }}
|
||||
{% elif fl == "centered" %}
|
||||
<div class="flex flex-col items-center gap-5 text-center">
|
||||
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-10 w-auto"/>{% elif companyName %}<span class="pd-display text-2xl text-primary">{{ companyName }}</span>{% endif %}
|
||||
{% if tagline %}<p class="pd-hand max-w-md text-xl text-accent">{{ tagline }}</p>{% endif %}
|
||||
{% if columns %}<nav class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">{% for col in columns %}{% for item in col.items %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body text-sm text-muted-foreground transition-colors hover:text-foreground">{{ item.label }}</a>{% endfor %}{% endfor %}</nav>{% endif %}
|
||||
{{ newsletter() }}
|
||||
{{ sociallinks("justify-center") }}
|
||||
</div>
|
||||
{{ legalbar() }}
|
||||
{% else %}
|
||||
<div class="grid grid-cols-2 gap-8 md:grid-cols-4 lg:grid-cols-5">
|
||||
<div class="col-span-2 lg:col-span-2">
|
||||
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-9 w-auto"/>{% elif companyName %}<span class="pd-display text-2xl text-primary">{{ companyName }}</span>{% endif %}
|
||||
{% if tagline %}<p class="pd-hand mt-2 max-w-xs text-xl text-accent">{{ tagline }}</p>{% endif %}
|
||||
{% if address or email or phone %}<address class="pd-body mt-4 space-y-1 text-sm not-italic text-muted-foreground">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="transition-colors hover:text-foreground">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="transition-colors hover:text-foreground">{{ phone }}</a></div>{% endif %}</address>{% endif %}
|
||||
{{ sociallinks("mt-5") }}
|
||||
</div>
|
||||
{% for col in columns %}<div><h3 class="pd-hand text-xl text-accent">{{ col.header }}</h3><ul class="mt-3 space-y-2">{% for item in col.items %}<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body text-sm text-muted-foreground transition-colors hover:text-foreground">{{ item.label }}</a></li>{% endfor %}</ul></div>{% endfor %}
|
||||
{% if showNewsletter %}<div class="col-span-2 lg:col-span-1">{{ newsletter() }}</div>{% endif %}
|
||||
</div>
|
||||
{{ legalbar() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</footer>
|
||||
9
templates/overrides/pastel-dream/gallery.ninjatpl
Normal file
9
templates/overrides/pastel-dream/gallery.ninjatpl
Normal file
@ -0,0 +1,9 @@
|
||||
{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %}
|
||||
<div class="bn-gallery my-8{% if class %} {{ class }}{% endif %}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}">
|
||||
<div class="{% if layout == "carousel" %}flex snap-x snap-mandatory overflow-x-auto pb-2 {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% elif layout == "masonry" %}columns-2 {% if cols == 4 %}md:columns-4{% elif cols == 2 %}md:columns-2{% else %}md:columns-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% else %}grid grid-cols-2 {% if cols == 4 %}md:grid-cols-4{% elif cols == 2 %}md:grid-cols-2{% else %}md:grid-cols-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% endif %}">
|
||||
{% for image in images %}
|
||||
<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:"" }}" class="group relative block overflow-hidden rounded-sm pd-frame p-1.5 bg-card focus:outline-none focus-visible:ring-2 focus-visible:ring-ring {% if layout == "carousel" %}w-64 shrink-0 snap-start sm:w-80{% elif layout == "masonry" %}mb-4 w-full break-inside-avoid{% endif %}">{% if layout == "grid" %}{% img image.src alt=image.alt|default:"" class="aspect-square w-full rounded-sm object-cover" %}{% else %}{% img image.src alt=image.alt|default:"" class="h-auto w-full rounded-sm" %}{% endif %}{% if image.caption %}<span class="sr-only">{{ image.caption }}</span>{% endif %}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
@ -1 +1,6 @@
|
||||
{% if level|integer == 1 %}<h1 class="font-display text-5xl md:text-6xl leading-tight brush-underline {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="font-body text-3xl font-semibold tracking-tight {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="font-body text-2xl font-semibold {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="font-body text-xl font-medium {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="font-body text-lg font-medium {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h6>{% else %}<h2 class="font-display text-4xl md:text-5xl leading-tight brush-underline {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h2>{% endif %}
|
||||
{% if level == 1 %}<h1 class="pd-display text-4xl md:text-5xl leading-tight text-primary flex-1 {{ textClass }}">{{ text }}</h1>
|
||||
{% elif level == 3 %}<h3 class="pd-display text-2xl text-primary flex-1 {{ textClass }}"><span class="pd-doodle-underline">{{ text }}</span></h3>
|
||||
{% elif level == 4 %}<h4 class="pd-display text-xl text-primary flex-1 {{ textClass }}">{{ text }}</h4>
|
||||
{% elif level == 5 %}<h5 class="pd-display text-lg font-medium text-primary flex-1 {{ textClass }}">{{ text }}</h5>
|
||||
{% elif level == 6 %}<h6 class="pd-hand text-xl text-accent flex-1 {{ textClass }}">{{ text }}</h6>
|
||||
{% else %}<h2 class="pd-display text-3xl md:text-4xl text-primary flex-1 {{ textClass }}"><span class="pd-doodle-underline">{{ text }}</span></h2>{% endif %}
|
||||
|
||||
36
templates/overrides/pastel-dream/hero.ninjatpl
Normal file
36
templates/overrides/pastel-dream/hero.ninjatpl
Normal file
@ -0,0 +1,36 @@
|
||||
{# pastel-dream hero — watercolor chrome; styled-text fields accept {text} object OR bare string. #}
|
||||
{% set eyebrowText = eyebrow.text|default:eyebrow %}
|
||||
{% set headlineText = headline.text|default:headline %}
|
||||
{% set subText = subheadline.text|default:subheadline %}
|
||||
{% set descText = description.text|default:description %}
|
||||
{% set pText = ctaPrimary.text %}
|
||||
{% set pUrl = ctaPrimary.url %}
|
||||
{% set sText = ctaSecondary.text %}
|
||||
{% set sUrl = ctaSecondary.url %}
|
||||
{% set variant = template|default:"centered" %}
|
||||
{% set hasImage = backgroundImage %}
|
||||
{% set showOverlay = hasImage and overlayColor != "none" and overlayColor != "" %}
|
||||
{% macro herocontent(align) %}
|
||||
{% if eyebrowText %}<span class="pd-hand mb-2 inline-block text-2xl text-accent">{{ eyebrowText }}</span>{% endif %}
|
||||
{% if headlineText %}<h1 class="pd-display text-4xl leading-tight text-primary sm:text-5xl lg:text-6xl">{{ headlineText }}</h1>{% endif %}
|
||||
{% if subText %}<h2 class="pd-body mt-4 max-w-2xl text-xl text-muted-foreground sm:text-2xl{% if align == "center" %} mx-auto{% endif %}">{{ subText }}</h2>{% endif %}
|
||||
{% if descText %}<p class="pd-body mt-4 max-w-xl leading-relaxed text-muted-foreground{% if align == "center" %} mx-auto{% endif %}">{{ descText }}</p>{% endif %}
|
||||
{% if pText or sText %}<div class="mt-8 flex flex-col gap-4 sm:flex-row{% if align == "center" %} justify-center{% endif %}">
|
||||
{% if pText and pUrl %}<a href="{{ pUrl }}" class="pd-tag text-base px-6 py-3">{{ pText }}</a>{% endif %}
|
||||
{% if sText and sUrl %}<a href="{{ sUrl }}" class="pd-tag pd-tag--ghost text-base px-6 py-3">{{ sText }}</a>{% endif %}
|
||||
</div>{% endif %}
|
||||
{% if trustLogos %}<div class="mt-10"><p class="pd-hand mb-3 text-lg text-accent">Regulars love</p><div class="flex flex-wrap items-center gap-6{% if align == "center" %} justify-center{% endif %}">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}" target="_blank" rel="noopener" class="opacity-60 transition-opacity hover:opacity-100">{% img logo.image alt=logo.alt class="h-8 w-auto" %}</a>{% else %}{% img logo.image alt=logo.alt class="h-8 w-auto opacity-60" %}{% endif %}{% endfor %}</div></div>{% endif %}
|
||||
{% endmacro %}
|
||||
{% if variant == "split" %}
|
||||
<section class="hero grid grid-cols-1 overflow-hidden bg-background text-foreground lg:grid-cols-2{% if class %} {{ class }}{% endif %}">
|
||||
<div class="relative min-h-[320px] bg-secondary {% if mediaPosition == "left" %}order-1 lg:order-1{% else %}order-1 lg:order-2{% endif %}">{% if hasImage %}{% img backgroundImage alt=headlineText class="absolute inset-0 h-full w-full object-cover" layout="hero" %}{% endif %}</div>
|
||||
<div class="flex flex-col justify-center px-6 py-16 lg:px-12 {% if mediaPosition == "left" %}order-2 lg:order-2{% else %}order-2 lg:order-1{% endif %}"><div class="max-w-lg text-left">{{ herocontent("left") }}</div></div>
|
||||
</section>
|
||||
{% else %}
|
||||
{% set align = "center" %}
|
||||
{% if variant == "left-aligned" %}{% set align = "left" %}{% endif %}
|
||||
<section class="hero relative flex flex-col overflow-hidden bg-background text-foreground {% if minHeight == "screen" %}min-h-screen{% elif minHeight == "50vh" %}min-h-[50vh]{% elif minHeight == "auto" %}min-h-[400px]{% else %}min-h-[75vh]{% endif %} {% if verticalAlign == "top" %}justify-start pt-20{% elif verticalAlign == "bottom" %}justify-end pb-20{% else %}justify-center{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
{% if hasImage %}<div class="absolute inset-0 z-0">{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}{% if showOverlay %}<div class="absolute inset-0 {% if overlayColor == "light" %}bg-background/60{% elif overlayColor == "primary" %}bg-primary/50{% elif overlayOpacity <= 30 %}bg-black/30{% elif overlayOpacity <= 50 %}bg-black/50{% elif overlayOpacity <= 70 %}bg-black/70{% else %}bg-black/80{% endif %}"></div>{% endif %}</div>{% endif %}
|
||||
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 sm:px-6 lg:px-8 {% if variant == "left-aligned" %}text-left{% else %}text-center{% endif %}">{{ herocontent(align) }}</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
40
templates/overrides/pastel-dream/hours-location.ninjatpl
Normal file
40
templates/overrides/pastel-dream/hours-location.ninjatpl
Normal file
@ -0,0 +1,40 @@
|
||||
<section class="pd-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-5xl px-4">
|
||||
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||
<div class="mt-10 pd-grid pd-grid--2">
|
||||
<div>
|
||||
{% if hours %}
|
||||
<table class="pd-body w-full text-sm" data-pd-hours>
|
||||
<tbody class="divide-y divide-dashed divide-border">
|
||||
{% for row in hours %}
|
||||
<tr data-day="{{ row.day }}">
|
||||
<th scope="row" class="pd-display py-2 pr-4 text-left text-base text-primary">{{ row.day }}</th>
|
||||
<td class="py-2 text-right {% if row.closed %}text-muted-foreground{% else %}text-foreground{% endif %} tabular-nums">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if specialHoursNote %}<p class="pd-body mt-4 rounded-md border border-dashed border-border bg-muted px-3 py-2 text-sm text-muted-foreground">{{ specialHoursNote }}</p>{% endif %}
|
||||
{% if address %}<div class="mt-6"><div class="pd-hand text-lg text-accent">Find us</div><address class="pd-body mt-1 whitespace-pre-line not-italic">{{ address }}</address></div>{% endif %}
|
||||
{% if phone %}<div class="mt-4"><div class="pd-hand text-lg text-accent">Call ahead</div><a href="tel:{{ phone }}" class="pd-body mt-1 inline-block font-medium transition-colors hover:text-accent">{{ phone }}</a></div>{% endif %}
|
||||
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener" class="pd-tag mt-6 text-sm"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#navigation"></use></svg>Get directions</a>{% endif %}
|
||||
</div>
|
||||
{% if mapEmbedUrl %}
|
||||
<div class="pd-frame aspect-square w-full overflow-hidden rounded-sm bg-secondary md:aspect-auto">
|
||||
<button type="button" data-map-src="{{ mapEmbedUrl }}" onclick="var f=document.createElement('iframe');f.src=this.getAttribute('data-map-src');f.setAttribute('class','h-full w-full border-0');f.setAttribute('loading','lazy');f.setAttribute('referrerpolicy','no-referrer-when-downgrade');f.setAttribute('title','Map');this.parentNode.replaceChild(f,this);" class="pd-body flex h-full min-h-[16rem] w-full items-center justify-center gap-2 text-sm font-medium transition-colors hover:bg-muted"><svg class="h-5 w-5 pd-pin" aria-hidden="true"><use href="/icons/lucide.svg#map-pin"></use></svg>{{ mapButtonLabel|default:"Load map" }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function(){
|
||||
var days=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
|
||||
var today=days[new Date().getDay()];
|
||||
document.querySelectorAll('[data-pd-hours] tr[data-day]').forEach(function(tr){
|
||||
var d=(tr.getAttribute('data-day')||'').trim();
|
||||
if(d.toLowerCase().indexOf(today.toLowerCase())===0){tr.querySelector('th').classList.add('pd-hours-today');}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
8
templates/overrides/pastel-dream/image.ninjatpl
Normal file
8
templates/overrides/pastel-dream/image.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
||||
{% with size=size|default:"full" align=align|default:"center" %}
|
||||
<figure class="my-8 flex flex-1 flex-col {% if align == "left" %}items-start{% elif align == "right" %}items-end{% else %}items-center{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
<div class="pd-frame pd-torn-bottom relative overflow-hidden p-2 {% if size == "xs" %}max-w-[200px] w-full{% elif size == "sm" %}max-w-[400px] w-full{% elif size == "md" %}max-w-[600px] w-full{% elif size == "lg" %}max-w-[800px] w-full{% else %}w-full{% endif %}">
|
||||
{% if src %}{% img src alt=alt class="block h-auto w-full rounded-sm object-cover" layout=size %}{% else %}<div class="flex h-48 w-full items-center justify-center pd-hand text-2xl text-muted-foreground">Add an image</div>{% endif %}
|
||||
</div>
|
||||
{% if caption %}<figcaption class="pd-hand mt-3 text-center text-xl text-accent">{{ caption }}</figcaption>{% endif %}
|
||||
</figure>
|
||||
{% endwith %}
|
||||
15
templates/overrides/pastel-dream/logo-strip.ninjatpl
Normal file
15
templates/overrides/pastel-dream/logo-strip.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
||||
<section class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
{% if heading %}<p class="pd-hand text-center text-2xl text-accent">{{ heading }}</p>{% endif %}
|
||||
<div class="mt-8 {% if marquee %}group relative overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]{% endif %}">
|
||||
<div class="flex flex-wrap items-center justify-center gap-x-10 gap-y-6 {% if marquee %}flex-nowrap [animation:logo-marquee_30s_linear_infinite] motion-reduce:[animation:none] group-hover:[animation-play-state:paused]{% endif %}">
|
||||
{% for logo in logos %}
|
||||
{% if logo.url %}<a href="{{ logo.url }}" class="block shrink-0 opacity-80 transition hover:opacity-100">{% endif %}
|
||||
<span class="inline-flex h-8 items-center{% if grayscale %} grayscale transition-[filter] hover:grayscale-0{% endif %}">{% img logo.image alt=logo.alt class="h-8 w-auto object-contain" %}</span>
|
||||
{% if logo.url %}</a>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% if marquee %}<style>@keyframes logo-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}</style>{% endif %}
|
||||
31
templates/overrides/pastel-dream/menu-list.ninjatpl
Normal file
31
templates/overrides/pastel-dream/menu-list.ninjatpl
Normal file
@ -0,0 +1,31 @@
|
||||
<section class="pd-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-5xl 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 %}
|
||||
<div class="pd-chalkboard mt-10 p-6 md:p-10">
|
||||
<div class="pd-menu-cols {% if columns == 2 %}pd-menu-cols--2{% endif %}">
|
||||
{% for section in sections %}
|
||||
<div class="menu-section">
|
||||
{% if section.title %}<h3 class="pd-hand border-b pd-chalk-rule pb-2 text-3xl">{{ section.title }}</h3>{% endif %}
|
||||
{% if section.note %}<p class="pd-body pd-chalk-muted mt-2 text-sm">{{ section.note }}</p>{% endif %}
|
||||
<ul class="pd-menu-list">
|
||||
{% for item in section.items %}
|
||||
<li class="flex items-baseline gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<span class="pd-display text-lg">{{ item.name }}</span>
|
||||
{% for badge in item.badges %}{% if badge.label %}<span class="pd-hand rounded-full border pd-chalk-rule px-2 py-0.5 text-sm pd-chalk-muted">{{ badge.label }}</span>{% endif %}{% endfor %}
|
||||
</div>
|
||||
{% if item.description %}<p class="pd-body pd-chalk-muted mt-0.5 text-sm">{{ item.description }}</p>{% endif %}
|
||||
</div>
|
||||
{% if item.price %}<span class="pd-mono shrink-0 tabular-nums">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if footnote %}<p class="pd-body pd-chalk-muted mt-10 text-xs">{{ footnote }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
74
templates/overrides/pastel-dream/navbar.ninjatpl
Normal file
74
templates/overrides/pastel-dream/navbar.ninjatpl
Normal file
@ -0,0 +1,74 @@
|
||||
{# pastel-dream navbar — paper-strip chrome over the builtin menu provider. One navbar per page (fixed drawer ids). #}
|
||||
{% set nv = variant %}
|
||||
{% if not nv %}{% if template == "centered" %}{% set nv = "centered-logo" %}{% else %}{% set nv = "standard" %}{% endif %}{% endif %}
|
||||
{% set sb = stickyBehavior %}
|
||||
{% if not sb %}{% if sticky %}{% set sb = "sticky" %}{% else %}{% set sb = "none" %}{% endif %}{% endif %}
|
||||
{% set cta_text = cta.text|default:ctaLabel %}
|
||||
{% set cta_url = cta.url|default:ctaUrl %}
|
||||
{% macro navlink(item) %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} class="pd-body px-3 py-2 text-sm font-medium text-foreground/80 transition-colors hover:text-accent {{ item.css_class }}">{{ item.label }}</a>{% endmacro %}
|
||||
{% macro dropdown(item) %}<div class="relative group"><button type="button" class="pd-body inline-flex items-center gap-1 px-3 py-2 text-sm font-medium text-foreground/80 transition-colors hover:text-accent">{{ item.label }}<svg class="h-4 w-4 transition-transform group-hover:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg></button><div class="absolute left-0 z-50 mt-1 w-56 invisible opacity-0 transition-all duration-200 group-hover:visible group-hover:opacity-100"><div class="pd-card py-2 shadow-lg">{% for child in item.children %}{% if child.item_type == "header" %}<div class="pd-hand px-4 py-2 text-base text-accent">{{ child.label }}</div>{% else %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body block px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted {{ child.css_class }}">{{ child.label }}</a>{% endif %}{% endfor %}</div></div></div>{% endmacro %}
|
||||
{% macro brand(size) %}<a href="/" class="flex flex-shrink-0 items-center gap-2">{% if logoType == "text" and logoText.text %}<span class="pd-display text-2xl text-primary {{ logoText.color }}">{{ logoText.text }}</span>{% elif logo %}<img src="{{ logo }}" alt="{{ alt|default:companyName }}" class="{{ size }} w-auto"{% if logoMaxWidth or logoMaxHeight %} style="{% if logoMaxWidth %}max-width:{{ logoMaxWidth }};{% endif %}{% if logoMaxHeight %}max-height:{{ logoMaxHeight }};{% endif %}"{% endif %}/>{% elif companyName %}<span class="pd-display text-2xl text-primary">{{ companyName }}</span>{% endif %}</a>{% endmacro %}
|
||||
{% macro ctabtn(extra) %}{% if cta_text %}<a href="{{ cta_url|default:'#' }}" class="pd-tag text-sm {{ extra }}">{{ cta_text }}</a>{% endif %}{% endmacro %}
|
||||
<nav data-bn-navbar data-bn-sticky="{{ sb }}" class="bn-navbar w-full {% if sb == "sticky" or sb == "shrink-on-scroll" %}sticky top-0 z-40 {% endif %}bg-background/95 backdrop-blur border-b border-dashed border-border {{ class }}">
|
||||
{% if showUtilityBar %}<div class="w-full border-b border-border/60 bg-muted/60 text-xs text-muted-foreground {% if not showOnMobile %}hidden sm:block{% endif %}"><div class="mx-auto flex h-8 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8"><div class="pd-body">{{ utilityBarText|safe }}</div><div class="flex items-center gap-4">{% if utilityBarPhone %}<a href="tel:{{ utilityBarPhone }}" class="hover:text-foreground">{{ utilityBarPhone }}</a>{% endif %}{% if utilityBarEmail %}<a href="mailto:{{ utilityBarEmail }}" class="hover:text-foreground">{{ utilityBarEmail }}</a>{% endif %}</div></div></div>{% endif %}
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="bn-navbar-row flex h-16 items-center justify-between">
|
||||
{% if nv == "centered-logo" %}
|
||||
<div class="hidden flex-1 items-center gap-1 md:flex">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}</div>
|
||||
<div class="flex items-center justify-center">{{ brand("h-9") }}</div>
|
||||
<div class="hidden flex-1 items-center justify-end gap-3 md:flex">{{ ctabtn("") }}</div>
|
||||
{% else %}
|
||||
<div class="flex items-center">{{ brand("h-9") }}</div>
|
||||
<div class="hidden items-center gap-1 md:flex">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}</div>
|
||||
{% endif %}
|
||||
<div class="flex items-center md:hidden">
|
||||
<label for="bn-navbar-toggle" data-bn-nav-open aria-controls="bn-navbar-drawer" aria-expanded="false" class="inline-flex cursor-pointer items-center justify-center rounded-md p-2 transition-colors hover:bg-muted"><span class="sr-only">Open menu</span><svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="checkbox" id="bn-navbar-toggle" class="bn-navbar-toggle" aria-hidden="true"/>
|
||||
<label for="bn-navbar-toggle" class="bn-navbar-overlay fixed inset-0 z-40 bg-black/50 md:hidden" aria-hidden="true"></label>
|
||||
<div id="bn-navbar-drawer" class="bn-navbar-drawer fixed right-0 top-0 z-50 flex h-full w-80 max-w-[85vw] flex-col bg-background text-foreground shadow-xl md:hidden" role="dialog" aria-modal="true" aria-label="Site menu">
|
||||
<div class="flex items-center justify-between border-b border-dashed border-border p-4">{{ brand("h-8") }}<label for="bn-navbar-toggle" data-bn-nav-close class="cursor-pointer rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground"><span class="sr-only">Close menu</span><svg class="h-6 w-6" 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></label></div>
|
||||
<nav class="flex-1 overflow-y-auto py-4" aria-label="Mobile">{% for item in menu.items %}{% if item.children %}<details class="group/acc"><summary class="pd-body flex cursor-pointer list-none items-center justify-between px-6 py-3 text-base font-medium transition-colors hover:bg-muted">{{ item.label }}<svg class="h-4 w-4 transition-transform group-open/acc:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg></summary><div class="pb-2 pl-8">{% for child in item.children %}{% if child.item_type != "header" %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body block px-2 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground">{{ child.label }}</a>{% endif %}{% endfor %}</div></details>{% else %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body block px-6 py-3 text-base font-medium transition-colors hover:bg-muted">{{ item.label }}</a>{% endif %}{% endfor %}</nav>
|
||||
{% if cta_text %}<div class="border-t border-dashed border-border p-4"><a href="{{ cta_url|default:'#' }}" class="pd-tag w-full justify-center">{{ cta_text }}</a></div>{% endif %}
|
||||
</div>
|
||||
<style>
|
||||
.bn-navbar-toggle{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;}
|
||||
.bn-navbar-drawer{width:20rem;max-width:85vw;transform:translateX(100%);transition:transform .3s ease;}
|
||||
.bn-navbar-overlay{opacity:0;pointer-events:none;transition:opacity .3s ease;}
|
||||
.bn-navbar-toggle:checked ~ .bn-navbar-drawer{transform:translateX(0);}
|
||||
.bn-navbar-toggle:checked ~ .bn-navbar-overlay{opacity:1;pointer-events:auto;}
|
||||
.bn-navbar-drawer details > summary::-webkit-details-marker{display:none;}
|
||||
</style>
|
||||
<script>
|
||||
(function(){
|
||||
if(window.__bnPastelNavInit)return;window.__bnPastelNavInit=1;
|
||||
function ready(fn){if(document.readyState!='loading')fn();else document.addEventListener('DOMContentLoaded',fn);}
|
||||
ready(function(){
|
||||
document.querySelectorAll('[data-bn-navbar]').forEach(function(nav){
|
||||
var toggle=nav.querySelector('.bn-navbar-toggle');
|
||||
var drawer=nav.querySelector('.bn-navbar-drawer');
|
||||
var opener=nav.querySelector('[data-bn-nav-open]');
|
||||
if(!toggle||!drawer)return;
|
||||
function focusables(){return drawer.querySelectorAll('a[href],button,label[for],input,summary,[tabindex]:not([tabindex="-1"])');}
|
||||
toggle.addEventListener('change',function(){
|
||||
var open=toggle.checked;
|
||||
document.documentElement.style.overflow=open?'hidden':'';
|
||||
document.body.style.overflow=open?'hidden':'';
|
||||
if(opener)opener.setAttribute('aria-expanded',open?'true':'false');
|
||||
if(open){var f=focusables();if(f.length)f[0].focus();}else if(opener&&opener.focus)opener.focus();
|
||||
});
|
||||
drawer.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'){toggle.checked=false;toggle.dispatchEvent(new Event('change'));return;}
|
||||
if(e.key!=='Tab')return;
|
||||
var f=focusables();if(!f.length)return;
|
||||
var first=f[0],last=f[f.length-1];
|
||||
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}
|
||||
else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}
|
||||
});
|
||||
drawer.querySelectorAll('a[href]').forEach(function(a){a.addEventListener('click',function(){toggle.checked=false;toggle.dispatchEvent(new Event('change'));});});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@ -0,0 +1,9 @@
|
||||
<section class="py-12 bg-background text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-3xl px-4">
|
||||
<div class="pd-torn-top pd-torn-bottom rounded-2xl {% if layout == "inline" %}bg-primary px-6 py-10 text-primary-foreground md:px-12{% else %}pd-card px-6 py-10 md:px-10{% endif %} text-center">
|
||||
{% if title %}<h2 class="pd-display text-2xl md:text-3xl {% if layout == "inline" %}text-primary-foreground{% else %}text-primary{% endif %}">{{ title }}</h2>{% endif %}
|
||||
{% if description %}<p class="pd-body mx-auto mt-3 max-w-xl {% if layout == "inline" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}">{{ description }}</p>{% endif %}
|
||||
<div class="mx-auto mt-6 max-w-md">{% signup_form list=listSlug source=source submit=submitText success_message=successMessage placeholder=placeholder %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
20
templates/overrides/pastel-dream/page-suggestions.ninjatpl
Normal file
20
templates/overrides/pastel-dream/page-suggestions.ninjatpl
Normal file
@ -0,0 +1,20 @@
|
||||
{# pastel-dream page-suggestions — themed 404 helper. Suggestions (when the platform provides them) list {url|slug, title, excerpt}; otherwise a friendly hand-drawn "lost your way" panel with a route home. #}
|
||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-3xl px-4 text-center">
|
||||
<p class="pd-hand text-3xl text-accent">Lost your way?</p>
|
||||
<h2 class="pd-display mt-1 text-3xl text-primary md:text-4xl">{{ title|default:"Were you looking for..." }}</h2>
|
||||
{% if suggestions %}
|
||||
<ul class="mt-8 space-y-3 text-left">
|
||||
{% for s in suggestions %}
|
||||
<li class="pd-card p-4">
|
||||
<a href="{{ s.url|default:s.slug }}" class="pd-display text-lg text-primary transition-colors hover:text-accent">{{ s.title }}</a>
|
||||
{% if showDescription and s.excerpt %}<p class="pd-body mt-1 text-sm text-muted-foreground">{{ s.excerpt }}</p>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="pd-body mx-auto mt-4 max-w-xl text-muted-foreground">{{ emptyMessage|default:"That page has wandered off. Head back to the counter and start again." }}</p>
|
||||
{% endif %}
|
||||
<a href="/" class="pd-tag mt-8"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#home"></use></svg>Back home</a>
|
||||
</div>
|
||||
</section>
|
||||
@ -0,0 +1,13 @@
|
||||
{# pastel-dream password-reset-form — request panel in soft pastel chrome. Endpoint matches the platform (/api/auth/request-password-reset, #reset-message). #}
|
||||
<div class="password-reset-form pd-card mx-auto max-w-md p-6">
|
||||
<div id="reset-message"></div>
|
||||
<div id="request-reset-panel">
|
||||
<h2 class="pd-display text-2xl text-primary"><span class="pd-doodle-underline">Reset password</span></h2>
|
||||
<p class="pd-body mt-2 text-muted-foreground">Enter your email address and we will send you a link to reset your password.</p>
|
||||
<form hx-post="/api/auth/request-password-reset" hx-target="#reset-message" hx-swap="innerHTML" class="mt-4">
|
||||
<div class="mb-4"><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>
|
||||
<button type="submit" class="pd-tag w-full justify-center" hx-disabled-elt="this">Send reset link</button>
|
||||
</form>
|
||||
<p class="pd-body mt-4 text-center text-sm"><a href="/login" class="text-accent underline decoration-dashed">Back to login</a></p>
|
||||
</div>
|
||||
</div>
|
||||
15
templates/overrides/pastel-dream/popular-posts.ninjatpl
Normal file
15
templates/overrides/pastel-dream/popular-posts.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
||||
<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 %}
|
||||
<ol class="space-y-4">
|
||||
{% for post in posts %}
|
||||
<li class="flex items-start gap-3">
|
||||
{% if showRank %}<span class="pd-display mt-0.5 text-2xl text-accent/60">{{ forloop.Counter }}</span>{% endif %}
|
||||
{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="pd-frame h-12 w-12 flex-shrink-0 overflow-hidden rounded-sm p-0.5">{% img post.featured_image_url alt=post.title class="h-full w-full rounded-sm object-cover" %}</a>{% endif %}
|
||||
<div class="min-w-0">
|
||||
<h3 class="pd-display text-sm text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
|
||||
{% if post.published_at %}<p class="pd-body mt-0.5 text-xs text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</section>
|
||||
19
templates/overrides/pastel-dream/post-hero.ninjatpl
Normal file
19
templates/overrides/pastel-dream/post-hero.ninjatpl
Normal file
@ -0,0 +1,19 @@
|
||||
{% set center = align != "left" %}
|
||||
{% if post.featured_image_url %}
|
||||
<section class="hero relative flex min-h-[50vh] flex-col justify-end overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="absolute inset-0 z-0">{% img post.featured_image_url alt=post.featured_image_alt class="h-full w-full object-cover" layout="hero" %}<div class="absolute inset-0 bg-black/60"></div></div>
|
||||
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 py-12 sm:px-6 lg:px-8 {% if center %}text-center{% endif %}">
|
||||
<h1 class="pd-display text-3xl leading-tight text-white sm:text-4xl lg:text-5xl">{{ post.title }}</h1>
|
||||
{% if showExcerpt and post.excerpt %}<p class="pd-body mt-4 max-w-2xl text-lg text-white/80{% if center %} mx-auto{% endif %}">{{ post.excerpt }}</p>{% endif %}
|
||||
{% if showMeta %}<div class="pd-hand mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-lg text-white/90{% if center %} justify-center{% endif %}">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span>{{ post.reading_time }} min</span>{% endif %}</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="bg-background py-14 text-foreground md:py-20{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto w-full max-w-3xl px-4 {% if center %}text-center{% endif %}">
|
||||
<h1 class="pd-display text-3xl leading-tight text-primary sm:text-4xl lg:text-5xl">{{ post.title }}</h1>
|
||||
{% if showExcerpt and post.excerpt %}<p class="pd-body mt-4 text-lg text-muted-foreground{% if center %} mx-auto max-w-2xl{% endif %}">{{ post.excerpt }}</p>{% endif %}
|
||||
{% if showMeta %}<div class="pd-hand mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-lg text-accent{% if center %} justify-center{% endif %}">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="text-muted-foreground">{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span class="text-muted-foreground">{{ post.reading_time }} min</span>{% endif %}</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
8
templates/overrides/pastel-dream/post-metadata.ninjatpl
Normal file
8
templates/overrides/pastel-dream/post-metadata.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
||||
{% if post %}
|
||||
<div class="pd-body flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground{% if class %} {{ class }}{% endif %}">
|
||||
{% if showAuthor and post.author_name %}<span class="flex items-center gap-2">{% if showAvatar and post.author_avatar %}{% img post.author_avatar alt=post.author_name class="h-7 w-7 rounded-full object-cover" %}{% endif %}<span class="pd-display text-foreground">{{ post.author_name }}</span></span>{% endif %}
|
||||
{% if showDate and post.published_at %}<time datetime="{{ post.published_at|date:"Y-m-d" }}">{{ post.published_at|date:"F j, Y" }}</time>{% endif %}
|
||||
{% if showReadingTime and post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
|
||||
{% if post.categories %}<span class="flex flex-wrap gap-1.5">{% for cat in post.categories %}<a href="{{ cat.url|default:'#' }}" class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground transition-colors hover:text-accent">{{ cat.name }}</a>{% endfor %}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
24
templates/overrides/pastel-dream/pricing.ninjatpl
Normal file
24
templates/overrides/pastel-dream/pricing.ninjatpl
Normal file
@ -0,0 +1,24 @@
|
||||
<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-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-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{% for tier in tiers %}
|
||||
<div class="pd-card relative flex flex-col p-6 {% if tier.highlighted %}ring-2 ring-accent{% endif %}">
|
||||
{% if tier.badge %}<span class="pd-hand absolute -top-4 left-1/2 -translate-x-1/2 rotate-[-3deg] rounded-full bg-accent px-4 py-1 text-lg text-accent-foreground shadow-sm">{{ tier.badge }}</span>{% endif %}
|
||||
<h3 class="pd-display text-xl text-primary">{{ tier.name }}</h3>
|
||||
<p class="mt-4 flex items-baseline gap-1">
|
||||
<span class="pd-display text-4xl text-accent">{% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }}</span>
|
||||
{% if tier.period %}<span class="pd-body text-sm text-muted-foreground">{{ tier.period }}</span>{% endif %}
|
||||
</p>
|
||||
{% if tier.description %}<p class="pd-body mt-3 text-sm text-muted-foreground">{{ tier.description }}</p>{% endif %}
|
||||
{% if tier.features %}<ul class="mt-6 flex-1 space-y-3 text-sm">
|
||||
{% for feature in tier.features %}<li class="pd-body flex items-start gap-2"><svg class="mt-0.5 h-4 w-4 shrink-0 text-accent" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg><span>{{ feature }}</span></li>{% endfor %}
|
||||
</ul>{% endif %}
|
||||
{% if tier.ctaUrl and tier.ctaLabel %}<a href="{{ tier.ctaUrl }}" class="pd-tag mt-8 w-full justify-center {% if not tier.highlighted %}pd-tag--ghost{% endif %}">{{ tier.ctaLabel }}</a>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if billingNote %}<p class="pd-body mt-8 text-center text-sm text-muted-foreground">{{ billingNote }}</p>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
6
templates/overrides/pastel-dream/quote.ninjatpl
Normal file
6
templates/overrides/pastel-dream/quote.ninjatpl
Normal file
@ -0,0 +1,6 @@
|
||||
{% with style=style|default:"block" align=align|default:"left" %}
|
||||
<figure class="bn-quote my-8 {% if style == "inline-card" %}pd-card p-6 md:p-8{% elif style == "pull" %}border-l-4 border-accent pl-6 md:pl-8 py-2{% else %}border-l-2 border-dashed border-border pl-5 py-1{% endif %}{% if align == "center" %} text-center{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
<blockquote{% if cite %} cite="{{ cite }}"{% endif %} class="pd-display {% if style == "pull" %}text-2xl md:text-3xl leading-snug{% else %}text-xl md:text-2xl leading-relaxed{% endif %} text-primary">{{ quote|safe }}</blockquote>
|
||||
{% if attribution or role or cite %}<figcaption class="pd-hand mt-4 text-xl text-accent{% if align == "center" %} flex flex-col items-center{% endif %}">{% if attribution %}<span>{{ attribution }}</span>{% endif %}{% if role %}<span class="{% if attribution %}block {% endif %}text-lg text-muted-foreground">{{ role }}</span>{% endif %}{% if cite %}<a href="{{ cite }}" class="{% if attribution or role %}mt-1 {% endif %}pd-body inline-block text-sm underline decoration-dashed underline-offset-2 hover:text-foreground">Source</a>{% endif %}</figcaption>{% endif %}
|
||||
</figure>
|
||||
{% endwith %}
|
||||
16
templates/overrides/pastel-dream/related-posts.ninjatpl
Normal file
16
templates/overrides/pastel-dream/related-posts.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
||||
{% if posts %}
|
||||
<section class="border-t border-dashed border-border py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
<h2 class="pd-display mb-8 text-2xl text-primary">{{ heading|default:"More from the counter" }}</h2>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% else %}lg:grid-cols-3{% endif %}">
|
||||
{% for post in posts %}
|
||||
<article class="group flex flex-col">
|
||||
{% if post.featured_image_url %}<a href="{{ post.url }}" class="pd-frame mb-3 block aspect-video overflow-hidden rounded-sm p-1.5">{% img post.featured_image_url alt=post.title class="h-full w-full rounded-sm object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
||||
<h3 class="pd-display text-base text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
|
||||
{% if post.published_at %}<p class="pd-hand mt-1 text-base text-accent">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
13
templates/overrides/pastel-dream/rich-section.ninjatpl
Normal file
13
templates/overrides/pastel-dream/rich-section.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
<section class="py-16 md:py-24 {% if background == "muted" %}bg-muted text-foreground{% elif background == "card" %}bg-card text-card-foreground{% else %}bg-background text-foreground{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto grid max-w-6xl items-center gap-10 px-4 md:grid-cols-2 md:gap-16">
|
||||
<div class="{% if mediaPosition == "left" %}md:order-2{% else %}md:order-1{% endif %}">
|
||||
{% if eyebrow %}<p class="pd-hand text-2xl text-accent">{{ eyebrow }}</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 body %}<div class="pd-body mt-4 space-y-4 text-lg text-muted-foreground [&_a]:text-accent [&_a]:underline [&_a]:decoration-dashed [&_strong]:text-primary">{{ body|safe }}</div>{% endif %}
|
||||
{% if ctaUrl and ctaLabel %}<a href="{{ ctaUrl }}" class="pd-tag mt-6 text-base px-5 py-3">{{ ctaLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||
</div>
|
||||
<div class="{% if mediaPosition == "left" %}md:order-1{% else %}md:order-2{% endif %}">
|
||||
{% if media %}<div class="pd-frame pd-torn-bottom overflow-hidden rounded-sm p-2">{% img media alt=mediaAlt class="h-auto w-full rounded-sm object-cover" %}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
1
templates/overrides/pastel-dream/rich-text.ninjatpl
Normal file
1
templates/overrides/pastel-dream/rich-text.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
{% if html %}<article class="rich-text pd-body prose prose-lg max-w-none text-foreground [&_h1]:pd-display [&_h2]:pd-display [&_h3]:pd-display [&_h1]:text-primary [&_h2]:text-primary [&_h3]:text-primary [&_a]:text-accent [&_a]:decoration-dashed [&_blockquote]:border-l-2 [&_blockquote]:border-accent [&_blockquote]:italic">{{ html|safe }}</article>{% endif %}
|
||||
12
templates/overrides/pastel-dream/sidebar-nav.ninjatpl
Normal file
12
templates/overrides/pastel-dream/sidebar-nav.ninjatpl
Normal file
@ -0,0 +1,12 @@
|
||||
{% set compact = template == "compact" %}
|
||||
<nav class="flex-1 {% if compact %}w-56 py-4 pr-3{% else %}w-64 py-6 pr-4{% endif %}{% if showBorder %} border-r border-dashed border-border{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
{% if title %}<h3 class="pd-hand {% if compact %}px-2 mb-3 text-lg{% else %}px-3 mb-4 text-xl{% endif %} text-accent">{{ title }}</h3>{% endif %}
|
||||
<ul class="{% if compact %}space-y-0.5{% else %}space-y-1{% endif %}">
|
||||
{% for item in menu.items %}
|
||||
<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} data-bn-sidebar-link class="pd-body flex items-center rounded-md text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground {% if compact %}px-2 py-1.5{% else %}px-3 py-2{% endif %} {{ item.css_class }}">{% if item.icon %}<svg class="mr-2 h-4 w-4" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg>{% endif %}{{ item.label }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% if highlightCurrent %}<script>
|
||||
(function(){var p=window.location.pathname;document.querySelectorAll('[data-bn-sidebar-link]').forEach(function(a){if(a.getAttribute('href')===p){a.classList.remove('text-muted-foreground');a.classList.add('text-foreground','bg-muted','font-medium');}});})();
|
||||
</script>{% endif %}
|
||||
13
templates/overrides/pastel-dream/social-links.ninjatpl
Normal file
13
templates/overrides/pastel-dream/social-links.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
<div class="social-links{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<p class="pd-hand mb-3 text-xl text-accent">{{ heading }}</p>{% endif %}
|
||||
<ul class="flex {% if style == "stack" %}flex-col gap-2{% else %}flex-wrap items-center gap-3{% endif %}">
|
||||
{% for link in links %}{% if link.url %}
|
||||
<li>
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener" aria-label="{{ link.label|default:link.network }}" class="inline-flex items-center gap-2 rounded-full border border-dashed border-border text-foreground transition-colors hover:border-accent hover:text-accent {% if style == "stack" %}w-full px-4 py-2{% else %}p-2.5{% endif %}">
|
||||
{% if link.network %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ link.network|split:":"|first }}.svg#{{ link.network|split:":"|last }}"></use></svg>{% endif %}
|
||||
{% if style == "stack" and link.label %}<span class="pd-body text-sm font-medium">{{ link.label }}</span>{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
44
templates/overrides/pastel-dream/stats.ninjatpl
Normal file
44
templates/overrides/pastel-dream/stats.ninjatpl
Normal file
@ -0,0 +1,44 @@
|
||||
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}"{% if countUp %} data-stats{% endif %}>
|
||||
<div class="mx-auto max-w-6xl 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 %}
|
||||
<dl class="mt-12 grid grid-cols-2 gap-8 {% if columns == 2 %}md:grid-cols-2{% elif columns == 3 %}md:grid-cols-3{% else %}md:grid-cols-4{% endif %}">
|
||||
{% for item in items %}
|
||||
<div class="text-center">
|
||||
<dd class="pd-display text-4xl text-accent md:text-5xl">{% if item.prefix %}{{ item.prefix }}{% endif %}<span{% if countUp %} data-countup-value="{{ item.value }}"{% endif %}>{{ item.value }}</span>{% if item.suffix %}{{ item.suffix }}{% endif %}</dd>
|
||||
{% if item.label %}<dt class="pd-hand mt-2 text-xl text-muted-foreground">{{ item.label }}</dt>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
{% if countUp %}<script>
|
||||
(function(){
|
||||
var sections = document.querySelectorAll('[data-stats]');
|
||||
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
function animate(section){
|
||||
section.querySelectorAll('[data-countup-value]').forEach(function(node){
|
||||
var raw = node.getAttribute('data-countup-value') || '';
|
||||
var target = parseFloat(raw.replace(/[^0-9.\-]/g, ''));
|
||||
if (isNaN(target)) return;
|
||||
var decimals = (raw.split('.')[1] || '').length;
|
||||
var start = null, dur = 1200;
|
||||
function step(ts){
|
||||
if (start === null) start = ts;
|
||||
var p = Math.min((ts - start) / dur, 1);
|
||||
var eased = 1 - Math.pow(1 - p, 3);
|
||||
node.textContent = (target * eased).toFixed(decimals);
|
||||
if (p < 1) requestAnimationFrame(step); else node.textContent = target.toFixed(decimals);
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
});
|
||||
}
|
||||
sections.forEach(function(section){
|
||||
if (reduce || !('IntersectionObserver' in window)) return;
|
||||
var io = new IntersectionObserver(function(entries){
|
||||
entries.forEach(function(e){ if (e.isIntersecting){ animate(section); io.disconnect(); } });
|
||||
}, { threshold: 0.3 });
|
||||
io.observe(section);
|
||||
});
|
||||
})();
|
||||
</script>{% endif %}
|
||||
21
templates/overrides/pastel-dream/team.ninjatpl
Normal file
21
templates/overrides/pastel-dream/team.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
||||
<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-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 {% if layout == "list" %}mx-auto max-w-3xl space-y-6{% elif layout == "single" %}mx-auto max-w-xl{% else %}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 %}{% endif %}">
|
||||
{% for member in members %}
|
||||
<div class="{% if layout == "list" %}pd-card flex items-start gap-5 p-5{% else %}text-center{% endif %}">
|
||||
{% if member.photo %}<span class="{% if layout == "list" %}h-20 w-20{% else %}mx-auto h-28 w-28{% endif %} block shrink-0 overflow-hidden rounded-full bg-secondary ring-2 ring-border">{% img member.photo alt=member.name class="h-full w-full object-cover" %}</span>{% endif %}
|
||||
<div class="{% if layout != "list" %}mt-4{% endif %}">
|
||||
{% if member.name %}<h3 class="pd-display text-lg text-primary">{{ member.name }}</h3>{% endif %}
|
||||
{% if member.role %}<p class="pd-hand text-lg text-accent">{{ member.role }}</p>{% endif %}
|
||||
{% if member.bio %}<p class="pd-body mt-2 text-sm text-muted-foreground">{{ member.bio }}</p>{% endif %}
|
||||
{% if member.socials %}<div class="mt-3 flex gap-3 {% if layout != "list" %}justify-center{% endif %}">
|
||||
{% for social in member.socials %}{% if social.url %}<a href="{{ social.url }}" class="text-muted-foreground transition-colors hover:text-accent" aria-label="Social link"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ social.icon|split:":"|first }}.svg#{{ social.icon|split:":"|last }}"></use></svg></a>{% endif %}{% endfor %}
|
||||
</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
26
templates/overrides/pastel-dream/testimonial.ninjatpl
Normal file
26
templates/overrides/pastel-dream/testimonial.ninjatpl
Normal file
@ -0,0 +1,26 @@
|
||||
<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-center text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||
<div class="mt-12 {% if layout == "single" %}mx-auto max-w-3xl{% elif layout == "carousel" %}flex snap-x snap-mandatory gap-6 overflow-x-auto pb-4{% else %}grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3{% endif %}">
|
||||
{% for q in quotes %}
|
||||
<figure class="pd-card flex flex-col p-6{% if layout == "carousel" %} min-w-[18rem] max-w-sm shrink-0 snap-start{% endif %}">
|
||||
{% if q.rating %}<div class="mb-3 flex gap-0.5" aria-label="Rated {{ q.rating }} out of 5">
|
||||
<svg class="h-4 w-4 {% if q.rating >= 1 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
<svg class="h-4 w-4 {% if q.rating >= 2 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
<svg class="h-4 w-4 {% if q.rating >= 3 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
<svg class="h-4 w-4 {% if q.rating >= 4 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
<svg class="h-4 w-4 {% if q.rating >= 5 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
</div>{% endif %}
|
||||
<blockquote class="pd-body flex-1 text-lg leading-relaxed text-foreground">{% if q.quote %}“{{ q.quote }}”{% endif %}</blockquote>
|
||||
<figcaption class="mt-5 flex items-center gap-3">
|
||||
{% if q.avatar %}<span class="h-10 w-10 shrink-0 overflow-hidden rounded-full bg-secondary">{% img q.avatar alt=q.name class="h-full w-full object-cover" %}</span>{% endif %}
|
||||
<span>
|
||||
{% if q.name %}<span class="pd-display block text-base text-primary">{{ q.name }}</span>{% endif %}
|
||||
{% if q.role %}<span class="pd-hand block text-lg text-accent">{{ q.role }}</span>{% endif %}
|
||||
</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1,3 +1 @@
|
||||
<div class="font-body prose max-w-none {{ class }}" style="color: hsl(var(--foreground)); line-height: 1.75;">
|
||||
{{ text|safe }}
|
||||
</div>
|
||||
<div class="pd-body prose max-w-none flex-1 text-foreground [&_a]:text-accent [&_a]:underline [&_a]:decoration-dashed [&_strong]:text-primary {{ proseClass }}">{{ text|safe }}</div>
|
||||
|
||||
21
templates/overrides/pastel-dream/timeline.ninjatpl
Normal file
21
templates/overrides/pastel-dream/timeline.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
||||
<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-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 %}
|
||||
<ol class="mt-12 {% if orientation == "horizontal" %}grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4{% else %}relative space-y-8 before:absolute before:left-4 before:top-2 before:h-[calc(100%-1rem)] before:w-px before:bg-border{% endif %}">
|
||||
{% for step in steps %}
|
||||
<li class="{% if orientation == "horizontal" %}text-center{% else %}relative pl-14{% endif %}">
|
||||
<span class="pd-stamp pd-stamp-sm {% if orientation == "horizontal" %}mx-auto{% else %}absolute left-0 top-0 ring-4 ring-background{% endif %}">
|
||||
{% if marker == "numbered" %}{{ forloop.counter }}{% elif marker == "dot" %}<span class="h-2 w-2 rounded-full bg-accent-foreground"></span>{% else %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#croissant"></use></svg>{% endif %}
|
||||
</span>
|
||||
<div class="{% if orientation == "horizontal" %}mt-4{% else %}pt-1{% endif %}">
|
||||
{% if step.label %}<p class="pd-hand text-lg text-accent">{{ step.label }}</p>{% endif %}
|
||||
{% if step.title %}<h3 class="pd-display mt-1 text-lg text-primary">{{ step.title }}</h3>{% endif %}
|
||||
{% if step.text %}<p class="pd-body mt-1 text-muted-foreground">{{ step.text }}</p>{% endif %}
|
||||
{% if step.media %}<div class="pd-frame mt-3 overflow-hidden rounded-sm p-1.5">{% img step.media alt=step.title class="h-auto w-full rounded-sm object-cover" %}</div>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
10
templates/overrides/pastel-dream/utility-bar.ninjatpl
Normal file
10
templates/overrides/pastel-dream/utility-bar.ninjatpl
Normal file
@ -0,0 +1,10 @@
|
||||
{# pastel-dream utility-bar — thin contact strip; left/right item lists {icon,label,link}. #}
|
||||
{% set ubcls = "bg-muted text-muted-foreground border-b border-dashed border-border" %}
|
||||
{% if variant == "primary" %}{% set ubcls = "bg-primary text-primary-foreground" %}{% elif variant == "dark" %}{% set ubcls = "bg-foreground text-background" %}{% endif %}
|
||||
{% macro ubitem(it) %}{% if it.link %}<a href="{{ it.link }}" class="inline-flex items-center gap-1.5 transition-opacity hover:opacity-80">{% if it.icon %}<svg class="h-3.5 w-3.5" aria-hidden="true"><use href="/icons/{{ it.icon|split:":"|first }}.svg#{{ it.icon|split:":"|last }}"></use></svg>{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</a>{% else %}<span class="inline-flex items-center gap-1.5">{% if it.icon %}<svg class="h-3.5 w-3.5" aria-hidden="true"><use href="/icons/{{ it.icon|split:":"|first }}.svg#{{ it.icon|split:":"|last }}"></use></svg>{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</span>{% endif %}{% endmacro %}
|
||||
<div class="pd-body w-full text-xs {% if not showOnMobile %}hidden sm:block{% endif %} {{ ubcls }} {{ class }}">
|
||||
<div class="mx-auto flex h-9 max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-wrap items-center gap-4">{% for it in leftItems %}{{ ubitem(it) }}{% endfor %}</div>
|
||||
<div class="flex flex-wrap items-center justify-end gap-4">{% for it in rightItems %}{{ ubitem(it) }}{% endfor %}</div>
|
||||
</div>
|
||||
</div>
|
||||
22
templates/overrides/pastel-dream/video-embed.ninjatpl
Normal file
22
templates/overrides/pastel-dream/video-embed.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
||||
{% with aspect=aspect|default:"16:9" %}<figure class="bn-video my-8{% if class %} {{ class }}{% endif %}"><div class="bn-video-frame pd-frame relative w-full overflow-hidden rounded-sm bg-secondary p-1.5 {% if aspect == "4:3" %}aspect-[4/3]{% elif aspect == "1:1" %}aspect-square{% elif aspect == "21:9" %}aspect-[21/9]{% else %}aspect-video{% endif %}" data-bn-video data-video-url="{{ url|default:"" }}" data-video-media="{% if media %}{{ media|mediaURL }}{% endif %}"><button type="button" data-bn-video-play class="group absolute inset-0 flex h-full w-full items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" aria-label="Play video{% if title %}: {{ title }}{% endif %}">{% if poster %}{% img poster alt=title|default:"" class="absolute inset-0 h-full w-full rounded-sm object-cover" %}{% endif %}<span class="absolute inset-0 bg-foreground/10 transition-colors group-hover:bg-foreground/20" aria-hidden="true"></span><span class="relative inline-flex h-16 w-16 items-center justify-center rounded-full bg-accent text-accent-foreground shadow-lg transition-transform group-hover:scale-105 motion-reduce:transform-none"><svg class="h-7 w-7" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg></span></button></div>{% if caption %}<figcaption class="pd-hand mt-3 text-center text-xl text-accent">{{ caption }}</figcaption>{% endif %}</figure><script>
|
||||
(function(){
|
||||
if(window.__bnVideoFacade)return;window.__bnVideoFacade=true;
|
||||
function yt(u){var m=u.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{6,})/);return m?m[1]:null;}
|
||||
function vim(u){var m=u.match(/vimeo\.com\/(?:video\/)?(\d+)/);return m?m[1]:null;}
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest('[data-bn-video-play]');if(!btn)return;
|
||||
var frame=btn.closest('[data-bn-video]');if(!frame)return;
|
||||
e.preventDefault();
|
||||
var url=frame.getAttribute('data-video-url')||'',media=frame.getAttribute('data-video-media')||'',node;
|
||||
var vstyle='position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:rgba(0,0,0,1);';
|
||||
if(media){node=document.createElement('video');node.src=media;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
|
||||
else{var y=yt(url),v=vim(url),embed;
|
||||
if(y)embed='https://www.youtube-nocookie.com/embed/'+y+'?autoplay=1&rel=0';
|
||||
else if(v)embed='https://player.vimeo.com/video/'+v+'?autoplay=1';
|
||||
if(embed){node=document.createElement('iframe');node.src=embed;node.allow='autoplay; fullscreen; picture-in-picture';node.setAttribute('allowfullscreen','');node.setAttribute('title',btn.getAttribute('aria-label')||'Video');node.style.cssText='position:absolute;inset:0;width:100%;height:100%;border:0;';}
|
||||
else if(url){node=document.createElement('video');node.src=url;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
|
||||
}
|
||||
if(node){btn.remove();frame.appendChild(node);}
|
||||
});
|
||||
})();
|
||||
</script>{% endwith %}
|
||||
@ -1,21 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full">
|
||||
{{ slots.header|safe }}
|
||||
<div class="max-w-3xl mx-auto px-4">{{ slots.header|safe }}</div>
|
||||
</header>
|
||||
<main class="flex-grow w-full max-w-2xl mx-auto px-4 py-16">
|
||||
{% if slots.main %}<article class="font-body prose max-w-none" style="line-height: 1.75; color: hsl(var(--foreground));">
|
||||
{{ slots.main|safe }}
|
||||
</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>{% endif %}
|
||||
<main class="flex-grow max-w-2xl mx-auto w-full px-4 py-12 pd-dropcap">
|
||||
{% if slots.main %}<article class="prose prose-lg max-w-none">{{ slots.main|safe }}</article>{% else %}<div class="py-20 text-center pd-body text-muted-foreground italic"><p>Write something honest here.</p></div>{% endif %}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
{{ slots.footer|safe }}
|
||||
</footer>
|
||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
19
templates/pastel-dream/auth.ninjatpl
Normal file
19
templates/pastel-dream/auth.ninjatpl
Normal file
@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||
{{ admin_banner_html|safe }}
|
||||
<main class="flex flex-grow w-full flex-col items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
<a href="/" class="mb-6 flex items-center justify-center" aria-label="Home">
|
||||
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt|default:site_settings.Title }}" class="h-10 w-auto">{% else %}<span class="pd-display text-3xl text-primary">{{ site_settings.Title|default:"Petal & Crumb" }}</span>{% endif %}
|
||||
</a>
|
||||
<div>
|
||||
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="pd-card p-6 text-center pd-body text-muted-foreground"><p>No form assigned to this page.</p></div>{% endif %}
|
||||
</div>
|
||||
{% if slots.footer %}<div class="pd-body mt-6 text-center text-sm text-muted-foreground">{{ slots.footer|safe }}</div>{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
15
templates/pastel-dream/blog-index.ninjatpl
Normal file
15
templates/pastel-dream/blog-index.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full">{{ slots.header|safe }}</header>
|
||||
<main class="flex-grow w-full">
|
||||
{% if title %}<div class="mx-auto max-w-6xl px-4 pt-12"><h1 class="pd-display text-4xl text-primary md:text-5xl"><span class="pd-doodle-underline">{{ title }}</span></h1>{% if page_meta.MetaDescription %}<p class="pd-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ page_meta.MetaDescription }}</p>{% endif %}</div>{% endif %}
|
||||
{% if slots.featured %}<section class="w-full">{{ slots.featured|safe }}</section>{% endif %}
|
||||
{% if slots.main %}<section class="w-full">{{ slots.main|safe }}</section>{% else %}<div class="py-20 text-center pd-hand text-2xl text-muted-foreground"><p>No stories brewed yet.</p></div>{% endif %}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
13
templates/pastel-dream/contact.ninjatpl
Normal file
13
templates/pastel-dream/contact.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full">{{ slots.header|safe }}</header>
|
||||
<main class="flex-grow w-full">
|
||||
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center pd-hand text-2xl text-muted-foreground"><p>Add a contact form and your hours.</p></div>{% endif %}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
@ -1,19 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full">
|
||||
{{ slots.header|safe }}
|
||||
</header>
|
||||
<main class="flex-grow w-full max-w-4xl mx-auto px-4 py-12">
|
||||
{% if slots.main %}{{ slots.main|safe }}{% 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>{% endif %}
|
||||
<header class="w-full">{{ slots.header|safe }}</header>
|
||||
<main class="flex-grow w-full">
|
||||
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center pd-hand text-2xl text-muted-foreground"><p>Pour something in here.</p></div>{% endif %}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
{{ slots.footer|safe }}
|
||||
</footer>
|
||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,19 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full">
|
||||
{{ slots.header|safe }}
|
||||
</header>
|
||||
<header class="w-full">{{ slots.header|safe }}</header>
|
||||
<main class="flex-grow w-full">
|
||||
{% if slots.main %}{{ slots.main|safe }}{% 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>{% endif %}
|
||||
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-3xl mx-auto py-20 px-4 text-center pd-body text-muted-foreground italic"><p>Add a gallery, a wide photo, a confession.</p></div>{% endif %}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
{{ slots.footer|safe }}
|
||||
</footer>
|
||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,22 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||
{{ admin_banner_html|safe }}
|
||||
<section class="w-full">{{ slots.hero|safe }}</section>
|
||||
<section class="w-full">
|
||||
{{ slots.hero|safe }}
|
||||
<div class="max-w-5xl mx-auto px-4">{{ slots.menu|safe }}</div>
|
||||
</section>
|
||||
<main class="flex-grow w-full">
|
||||
{% if slots.main %}<div class="max-w-6xl mx-auto px-4 py-20">
|
||||
{{ slots.main|safe }}
|
||||
</div>{% endif %}
|
||||
</main>
|
||||
<section class="w-full">
|
||||
{{ slots.cta|safe }}
|
||||
<div class="max-w-3xl mx-auto px-4 py-12">{{ slots.story|safe }}</div>
|
||||
</section>
|
||||
<footer class="w-full mt-auto">
|
||||
{{ slots.footer|safe }}
|
||||
</footer>
|
||||
<section class="w-full">
|
||||
<div class="max-w-5xl mx-auto px-4">{{ slots.cta|safe }}</div>
|
||||
</section>
|
||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user