Compare commits

..

22 Commits
v0.1.1 ... main

Author SHA1 Message Date
Alex Dunmow
32eaa92fe9 bump to 0.5.0 2026-07-06 21:21:17 +08:00
Alex Dunmow
5ef52d8874 feat: Wave B gallery reshoot with bundled fonts (home/about/article × light+dark)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:21:03 +08:00
Alex Dunmow
c2a1c62ec2 bump to 0.4.1 2026-07-06 21:14:33 +08:00
Alex Dunmow
51d12fd6a1 feat(earthen): bundle Fraunces + Work Sans via @font-face + preset typography refs
Host BundledFonts is inert, so declare the two fonts.json families as
@font-face against /templates/earthen/fonts/*.woff2 and point every preset's
typography.fontHeading/fontBody at template:earthen:<Family> so --font-heading
and --font-body resolve to the bundled faces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:14:29 +08:00
Alex Dunmow
46a45d7160 bump to 0.4.0 2026-07-06 19:26:19 +08:00
Alex Dunmow
ac4ba1523c feat: Wave B gallery screenshots (home/about/article × light+dark)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:25:58 +08:00
Alex Dunmow
7456205750 bump to 0.3.2 2026-07-06 16:30:18 +08:00
Alex Dunmow
c71eebf919 fix(earthen): image block caption below image, not beside it
The figure was a flex ROW (flex justify-*), so a figcaption became a second
flex item sitting to the right of the frame. flex-col + items-* keeps the
alignment semantics and puts the caption underneath.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:30:18 +08:00
Alex Dunmow
bf89cdba6d bump to 0.3.1 2026-07-06 16:24:40 +08:00
Alex Dunmow
c2fc45e7f7 fix(earthen): kill placeholder copy, enrich home, wire stock media, size all SVGs, full-bleed article header
- seed: real Rootline Trust content — no 'example figures' placeholder copy;
  home composed as split hero (hands-soil image) + stats + feature-grid +
  botanical divider + field note + newsletter; About gap filled with fern
  image + impact report between story and pull-quote; first article expanded
  to ~10 field-notes paragraphs with inline forest image; 6 Pixabay images
  seeded via seed/media (id+file+alt)
- article.ninjatpl: header slot now full-bleed (was max-w-3xl wrapped — the
  boxed-header artifact bracketing the nav with vertical rules)
- landing.ninjatpl: main slot full-bleed so composed sections span the page
- SVG geometry guard: every <use>-icon svg now carries explicit width/height
  matching its size class (18 files)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:24:31 +08:00
Alex Dunmow
32455b5497 bump to 0.3.0 2026-07-06 09:25:04 +08:00
Alex Dunmow
bd0d886854 feat: Wave B gallery screenshots (home/about/article × light+dark) 2026-07-06 09:24:44 +08:00
Alex Dunmow
24a11b75d3 chore: rename dev host localdev.blockninjacms.com -> blockninja.dev 2026-07-05 20:50:30 +08:00
Alex Dunmow
ad1a1d83aa fix(earthen): repair blank hero (.text hard-error) + trim section whitespace
Hero blank-band bug (root cause):
- The hero override read styled-text fields as `field.text|default:field`
  (eyebrow/headline/subheadline/description). The ninjatpl engine hard-errors
  on attribute-access-against-a-bare-string, and the demo seed passes these
  fields as bare strings, so the whole hero errored and rendered as an empty
  ~75vh band on the landing page. Switched to `field|get:"text"|default:field`
  — the process-global `get` filter degrades gracefully on a bare string
  (returns nothing, so `|default:` falls back to the string) and still reads
  `.text` from the admin/editor `{text,color}` object form. Matches the fixed
  sibling lanes (terminal, scifi-clean). navbar `cta.text` left as-is: that
  field is always a link object, never a bare string (terminal did the same).

Whitespace pass:
- Trimmed oversized marketing section padding py-16 md:py-24 -> py-12 md:py-20
  across 15 blocks (cta, feature-grid, pricing, stats, team, testimonial,
  timeline, faq, countdown, rich-section, contact-card, contact-form,
  event-list, menu-list, hours-location) to remove dead inter-section bands.
  Tailwind classes only — the host Tailwind build now scans theme .ninjatpl,
  so responsive utilities compile; no duplicate hand-rolled media-query CSS
  added.

No new CSS (earthen's manifest CSS is all bespoke aesthetic — nothing
duplicated a utility). No scroll-reveal system exists to harden; the stats
count-up already no-ops under reduced-motion / no-JS and renders final values.
Codeless build OK (earthen@0.2.1). check-safety: 32 checks 19 ok 13 skip -> OK.
No version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:52:46 +08:00
Alex Dunmow
7e29730072 fix(theme): drive responsive layout from theme-owned CSS
Host Tailwind JIT does not scan theme templates, so responsive grid/hero
utilities (sm:/md:/lg:grid-cols-*, min-h-screen, arbitrary values) are
dropped and multi-column layouts collapse to one column. Ship the
load-bearing responsive layout as compiled theme CSS and point the
override templates at it. Layout only — colours stay on semantic tokens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:40:20 +08:00
Alex Dunmow
5f65fd8e4c bump to 0.2.1 2026-07-05 16:44:48 +08:00
Alex Dunmow
f52b9d0578 docs: publishing README 2026-07-05 13:43:21 +08:00
Alex Dunmow
8b8fb4d65d feat(earthen): Wave A theme lane — 49 overrides, 7 templates, seed (WO-TF-016)
Deliver the full conservation-nonprofit theme per OVERRIDE-CONTRACT (Wave A).

- 49 mandatory builtin overrides in templates/overrides/earthen/ (chrome,
  marketing, primitives, commerce, blog family, auth surface, page-suggestions),
  re-skinned from the frozen builtins into botanical/paper-grain chrome with
  all content fields, custom tags, provider vars, HTMX/auth wiring and JS
  preserved verbatim. Stale button/card/image/text/heading overrides fixed to
  the new field contract (link/style/label, media/text via {% img %}).
- 7 page templates: default, full-width, landing, article, blog-index, contact,
  auth.
- Blocks rework: impact_metrics/partner_logos/botanical_divider/footer converted
  to builtin overrides (deleted as theme blocks); donation_cta and field_note
  kept; impact_report added. Persona blocks now 3.
- 5 presets (all 19 tokens; mossbed/sage-morning/ember-loam are dual-mode).
- Bundled Fraunces + Work Sans woff2 (OFL) in assets/fonts/; fonts.json,
  RECOMMENDED_FONTS.md, RECOMMENDED_ICONS.md; required_icon_packs=[lucide].
- Seed: home/about/blog(3 posts)/contact/login as Rootline Trust, contact-form
  formConfig.targetTable=contact_submissions + row_inserted email-admins workflow.
- Themed email-safe wrapper; on-theme master pages; CSS-first botanical motion.

Verified: make (build) exit 0, make archive-check exit 0, check-safety exit 0.

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:11:12 +08:00
Alex Dunmow
482592c44e bump to 0.1.2 2026-07-03 22:09:40 +08:00
145 changed files with 2879 additions and 5649 deletions

1
.gitignore vendored
View File

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

View File

@ -1,29 +1,55 @@
# Earthen — build helpers (.so plugin workflow)
# earthen — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
#
# Local build only. This Makefile deliberately does NOT include rebuild/deploy
# targets to avoid touching the live CMS container during agent runs.
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
# classifies the repo as codeless (no Go at the root) and packs the artifact
# with manifest.pb (codeless: true) and NO plugin.wasm.
#
# Usage:
# make build # pack earthen-<version>.bnp (codeless — no wasm)
# make verify # validate the built .bnp (loader-identical checks)
# make archive-check # prove a clean `git archive HEAD` still builds codeless
# make publish # ninja plugin publish --bnp (to the active registry)
# make clean # remove build artefacts
.PHONY: all clean templ help
.PHONY: build verify archive-check publish clean help
PLUGIN_NAME := earthen
NINJA := ninja
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
# Default target: build the .so locally.
all: $(PLUGIN_NAME).so
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
build:
$(NINJA) plugin build --dir .
# Local plugin build.
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
# Validate the built .bnp exactly as the registry/loader will.
verify:
$(NINJA) plugin verify $(BNP)
# Regenerate templ Go files.
templ:
templ generate
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
archive-check:
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
trap 'rm -rf "$$T"' EXIT && \
git archive HEAD | tar -x -C "$$T" && \
cd "$$T" && $(NINJA) plugin build --dir . && \
echo "archive-check OK"
# Remove built artefacts.
# Publish the prebuilt .bnp to the active registry.
# Pass --host https://my.blockninja.dev for the dev orchestrator.
publish: build
$(NINJA) plugin publish --bnp $(BNP)
# Remove build artefacts.
clean:
rm -f $(PLUGIN_NAME).so
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
help:
@echo "Targets:"
@echo " all Build $(PLUGIN_NAME).so locally (default)"
@echo " templ Regenerate templ Go files"
@echo " clean Remove built .so"
@echo " build Pack the codeless .bnp (no wasm)"
@echo " verify Validate the built .bnp (loader-identical checks)"
@echo " archive-check Prove a clean git archive HEAD packs codeless"
@echo " publish Publish the .bnp to the active registry"
@echo " clean Remove build artefacts"

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# Earthen
A warm, hand-crafted theme in moss, clay, and sage over sun-bleached cream. Botanical line-art and soft organic shapes sit on a paper-grain ground, so a page feels grounded and hopeful rather than corporate. It was made for causes people care about. The palette carries a quiet, outdoor calm.
## Good for
We built Earthen with cause-driven work in mind. Treat these as starting points, not limits. If the mood fits your brand, use it:
- Conservation groups and land trusts
- Nonprofits and NGOs running donation drives
- Community farms and food co-ops
- Sustainability and outdoor education projects
## What you get
- Five color presets, each with a tuned light mode and a tuned dark mode: Mossbed, Clay Field, Wet Loam, Sage Morning, and Ember Loam.
- Fraunces for headings and Work Sans for body text, both bundled and ready. Fonts stay admin-controlled, so you can swap them from the typography settings.
- Three persona blocks: a donation appeal, a field-dispatch note, and an impact report.
- 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. It comes seeded as a conservation org called Rootline Trust.
- Every core block restyled to match, including botanical dividers, 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 dressed starting point.

View File

@ -1,47 +1,39 @@
# Earthen — recommended fonts
# Earthen — fonts
This theme ships `fonts.json = []` per the wave-1 fonts policy
(see `themes/docs/FONTS.md`). The CSS uses
`var(--font-heading)` / `var(--font-body)` / `var(--font-mono)` with a
fallback stack so the page reads close to the intended aesthetic before
the admin assigns fonts.
Earthen **bundles** its display and body faces as `source=template` rows via
`fonts.json`, so the theme reads correctly on activation with no admin setup.
All CSS references fonts through `var(--font-heading)` / `var(--font-body)` /
`var(--font-mono)` with a fallback stack, so the admin can still override any
role from the Typography panel.
Configure these in the admin Typography panel to match the spec's
intended look.
## Bundled faces (ship with the theme)
## Display / heading — assign to `Heading`
| Role | Family | Weights | File | License |
|------|--------|---------|------|---------|
| Heading | **Fraunces** | 400, 600 | `assets/fonts/fraunces-latin-400.woff2`, `fraunces-latin-600.woff2` | SIL Open Font License 1.1 (`assets/fonts/OFL.txt`) |
| Body | **Work Sans** | 400, 600 | `assets/fonts/worksans-latin-400.woff2`, `worksans-latin-600.woff2` | SIL Open Font License 1.1 (`assets/fonts/OFL.txt`) |
Primary pick:
Fraunces gives the organic, slightly wobbly display voice the persona wants;
Work Sans keeps body copy warm but legible for donation and report pages. Both
are OFL, subset to the Latin range to stay inside the marketplace size gate.
- **Fraunces** — Source: `google:Fraunces`.
Open the Typography panel, switch to the Google Fonts tab, search
"Fraunces", click Add, then assign to Heading. (OFL, free to host.)
## Fallback stacks (used before/without admin fonts)
Spec-preferred display face (commercial):
- Heading: `var(--font-heading, "Fraunces", "Playfair Display", Georgia, serif)`
- Body: `var(--font-body, "Work Sans", ui-sans-serif, system-ui, sans-serif)`
- Mono: `var(--font-mono, "JetBrains Mono", ui-monospace, monospace)`
- **Recoleta** — Source: `upload:Recoleta`.
Recoleta is a commercial face from Latinotype. Purchase the desktop +
webfont licence, upload the woff2 in the Fonts panel, then assign to
Heading. Until then Fraunces is the fallback used by the theme CSS.
## Optional admin choices
## Body — assign to `Body`
- **Spectral** — Source: `google:Spectral`.
Open the Typography panel, Google Fonts tab, search "Spectral",
click Add, assign to Body. (OFL, free to host.)
## Mono / data captions — assign to `Mono`
- **JetBrains Mono** — Source: `google:JetBrains Mono`.
Open the Typography panel, Google Fonts tab, search "JetBrains Mono",
click Add, assign to Mono. (OFL, free to host.)
- **Mono** is not bundled. If your content uses monospace (data captions, code),
add **JetBrains Mono** (`google:JetBrains Mono`, OFL) from the Typography panel
Google Fonts tab and assign it to Mono. Until then the system mono stack applies.
- **Alegreya** (`google:Alegreya`, OFL) is a good serif alternative for Body if you
want a more literary, long-read feel for field journals. Assign it to Body to swap.
## Notes
- All three Google-Font picks are part of the curated picker list.
No manual upload required for the OFL families.
- A future build pass may bundle Recoleta woff2s directly via a
populated `fonts.json` once the licence is confirmed.
- The CSS fallback stack inside the theme (`"Fraunces", "Playfair Display",
Georgia, serif` for headings; `"Spectral", Georgia, serif` for body)
keeps the moss/clay character visible during the cold-load window.
- Do not hardcode a `font-family` anywhere. Every family flows through the CSS
variables so the admin font picker keeps working.
- Bundled woff2 files are subset to Latin. Extend the subset only if you serve
non-Latin content, and keep the total artifact within the registry size gate.

23
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,23 @@
# Earthen — icon packs
Earthen renders icons through the sprite system only
(`<svg><use href="/icons/<pack>.svg#<name>"/></svg>`, or the `::pack:name:size::`
shorthand in templates). It does **not** inline icon SVGs. The theme's botanical
line-art (fern, seed, root motifs) is decorative illustration, not icons, and is
drawn inline on purpose.
## Required pack
- **Lucide** — declared in `plugin.mod` `required_icon_packs = ["lucide"]`.
Lucide ships bundled with the CMS, so no action is normally needed. If it is
missing on your instance, open `/admin/icons`, install **Lucide**, and enable it.
Block schemas that expose an icon picker default to Lucide names, so the theme
renders correctly out of the box. When the loader begins honoring
`required_icon_packs`, Lucide will be installed automatically on activation.
## Optional
No other packs are required. If you add icons from another pack in block content,
install that pack via `/admin/icons` first, then reference it as `"<pack>:<name>"`
in the block's icon field.

View File

@ -1,114 +0,0 @@
/* Earthen — theme utilities injected via CSSManifest.InputCSSAppend. */
/* All colour values reference the 19-token HSL custom properties so they */
/* shift with the active preset (mossbed, clay-field, wet-loam). */
/* Paper grain backdrop a single inline SVG noise pattern keeps page weight
inside the marketplace gate ( 81920 bytes). */
.bg-paper-grain {
background-color: hsl(var(--background));
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27160%27%20height%3D%27160%27%20viewBox%3D%270%200%20160%20160%27%3E%3Cfilter%20id%3D%27n%27%3E%3CfeTurbulence%20type%3D%27fractalNoise%27%20baseFrequency%3D%270.9%27%20numOctaves%3D%272%27%20seed%3D%274%27%2F%3E%3CfeColorMatrix%20values%3D%270%200%200%200%200.36%200%200%200%200%200.32%200%200%200%200%200.24%200%200%200%200.05%200%27%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D%27160%27%20height%3D%27160%27%20filter%3D%27url%28%23n%29%27%2F%3E%3C%2Fsvg%3E");
background-repeat: repeat;
background-size: 240px 240px;
}
/* Botanical hairline divider — used inside .botanical-rule sections. */
.botanical-rule > span {
display: inline-block;
}
/* Hand-drawn crayon underline on h2 — uses currentColor and primary token. */
.crayon-underline {
position: relative;
padding-bottom: 0.35em;
}
.crayon-underline::after {
content: "";
position: absolute;
left: 0;
right: auto;
bottom: 0;
height: 6px;
width: 2.5em;
background-color: hsl(var(--accent) / 0.4);
border-radius: 4px 8px 6px 12px;
transform: skewY(-1deg);
}
/* Botanical drop cap — applied to the first paragraph of .drop-cap-host. */
.drop-cap-host > p:first-of-type::first-letter,
.drop-cap > p:first-of-type::first-letter,
.drop-cap::first-letter {
font-family: var(--font-heading, "Fraunces", "Playfair Display", Georgia, serif);
color: hsl(var(--primary));
float: left;
font-size: 3.4em;
line-height: 0.85;
margin: 0.15em 0.18em 0 0;
font-weight: 600;
}
/* Tile selection (donation CTA) — uses primary ring when checked. */
.earthen-tile input[type="radio"]:checked + span {
color: hsl(var(--primary));
}
.earthen-tile:has(input[type="radio"]:checked) {
box-shadow: 0 0 0 2px hsl(var(--primary));
}
/* Partner logos — grayscale at rest, full colour on hover. */
.earthen-partner-img,
.earthen-partner-fallback {
filter: grayscale(1);
opacity: 0.85;
transition: filter 200ms ease, opacity 200ms ease;
}
.earthen-partner-tile:hover .earthen-partner-img,
.earthen-partner-link:hover .earthen-partner-img,
.earthen-partner-link:focus-visible .earthen-partner-img,
.earthen-partner-tile:hover .earthen-partner-fallback,
.earthen-partner-link:hover .earthen-partner-fallback {
filter: grayscale(0);
opacity: 1;
}
/* Paper-grain frame around field-note imagery. */
.earthen-paper-frame {
box-shadow: inset 0 0 0 1px hsl(var(--border) / 0.6);
}
.earthen-paper-frame-inner {
box-shadow: 0 1px 2px hsl(var(--foreground) / 0.06);
}
/* Footer links — botanical underline on hover. */
.earthen-footer-link {
text-decoration: none;
border-bottom: 1px solid transparent;
transition: color 150ms ease, border-color 150ms ease;
}
.earthen-footer-link:hover,
.earthen-footer-link:focus-visible {
color: hsl(var(--primary-foreground));
border-bottom-color: hsl(var(--accent));
}
/* Buttons — focus ring follows the active preset. */
.earthen-button:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* Article aside — keeps the byline rail comfortable on mobile. */
.earthen-aside {
color: hsl(var(--muted-foreground));
}
/* Ensure article main column uses the Spectral body fallback. */
.earthen-article {
font-family: var(--font-body, "Spectral", Georgia, serif);
color: hsl(var(--foreground));
}
/* Divider glyph — softer stroke at rest. */
.earthen-glyph {
color: hsl(var(--primary));
}

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

@ -0,0 +1,111 @@
Bundled fonts — SIL Open Font License 1.1
=========================================
This directory bundles latin-subset woff2 files for three families, all
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)
Work Sans: Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans)
Caveat: Copyright 2019 The Caveat Project Authors (https://github.com/googlefonts/caveat)
Files
-----
fraunces-latin-400.woff2, fraunces-latin-600.woff2 — Fraunces (heading)
worksans-latin-400.woff2, worksans-latin-600.woff2 — Work Sans (body)
caveat-latin-400.woff2, caveat-latin-700.woff2 — Caveat (hand-drawn accents)
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.

View File

@ -1,5 +1,9 @@
# Earthen fonts
Reserved for woff2 bundles in a future build pass. The wave-1 build ships
`fonts.json = []` per the FONTS.md policy; admins assign Fraunces /
Spectral / JetBrains Mono through the typography picker.
Bundled woff2 faces, served as `source=template` rows via `../../fonts.json`:
- **Fraunces** 400/600 (`fraunces-latin-400.woff2`, `fraunces-latin-600.woff2`) — heading.
- **Work Sans** 400/600 (`worksans-latin-400.woff2`, `worksans-latin-600.woff2`) — body.
Both are SIL Open Font License 1.1 (see `OFL.txt`), subset to Latin. See
`../../RECOMMENDED_FONTS.md` for admin assignment and the mono option.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

268
assets/style.css Normal file
View File

@ -0,0 +1,268 @@
/* Earthen — theme utilities injected via CSSManifest.InputCSSAppend. */
/* All colour values reference the 19-token HSL custom properties so they */
/* shift with the active preset (mossbed, clay-field, wet-loam). */
/* Paper grain backdrop a single inline SVG noise pattern keeps page weight
inside the marketplace gate ( 81920 bytes). */
.bg-paper-grain {
background-color: hsl(var(--background));
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27160%27%20height%3D%27160%27%20viewBox%3D%270%200%20160%20160%27%3E%3Cfilter%20id%3D%27n%27%3E%3CfeTurbulence%20type%3D%27fractalNoise%27%20baseFrequency%3D%270.9%27%20numOctaves%3D%272%27%20seed%3D%274%27%2F%3E%3CfeColorMatrix%20values%3D%270%200%200%200%200.36%200%200%200%200%200.32%200%200%200%200%200.24%200%200%200%200.05%200%27%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D%27160%27%20height%3D%27160%27%20filter%3D%27url%28%23n%29%27%2F%3E%3C%2Fsvg%3E");
background-repeat: repeat;
background-size: 240px 240px;
}
/* Botanical hairline divider — used inside .botanical-rule sections. */
.botanical-rule > span {
display: inline-block;
}
/* Hand-drawn crayon underline on h2 — uses currentColor and primary token. */
.crayon-underline {
position: relative;
padding-bottom: 0.35em;
}
.crayon-underline::after {
content: "";
position: absolute;
left: 0;
right: auto;
bottom: 0;
height: 6px;
width: 2.5em;
background-color: hsl(var(--accent) / 0.4);
border-radius: 4px 8px 6px 12px;
transform: skewY(-1deg);
}
/* Botanical drop cap — applied to the first paragraph of .drop-cap-host. */
.drop-cap-host > p:first-of-type::first-letter,
.drop-cap > p:first-of-type::first-letter,
.drop-cap::first-letter {
font-family: var(--font-heading, "Fraunces", "Playfair Display", Georgia, serif);
color: hsl(var(--primary));
float: left;
font-size: 3.4em;
line-height: 0.85;
margin: 0.15em 0.18em 0 0;
font-weight: 600;
}
/* Tile selection (donation CTA) — uses primary ring when checked. */
.earthen-tile input[type="radio"]:checked + span {
color: hsl(var(--primary));
}
.earthen-tile:has(input[type="radio"]:checked) {
box-shadow: 0 0 0 2px hsl(var(--primary));
}
/* Partner logos — grayscale at rest, full colour on hover. */
.earthen-partner-img,
.earthen-partner-fallback {
filter: grayscale(1);
opacity: 0.85;
transition: filter 200ms ease, opacity 200ms ease;
}
.earthen-partner-tile:hover .earthen-partner-img,
.earthen-partner-link:hover .earthen-partner-img,
.earthen-partner-link:focus-visible .earthen-partner-img,
.earthen-partner-tile:hover .earthen-partner-fallback,
.earthen-partner-link:hover .earthen-partner-fallback {
filter: grayscale(0);
opacity: 1;
}
/* Paper-grain frame around field-note imagery. */
.earthen-paper-frame {
box-shadow: inset 0 0 0 1px hsl(var(--border) / 0.6);
}
.earthen-paper-frame-inner {
box-shadow: 0 1px 2px hsl(var(--foreground) / 0.06);
}
/* Footer links — botanical underline on hover. */
.earthen-footer-link {
text-decoration: none;
border-bottom: 1px solid transparent;
transition: color 150ms ease, border-color 150ms ease;
}
.earthen-footer-link:hover,
.earthen-footer-link:focus-visible {
color: hsl(var(--primary-foreground));
border-bottom-color: hsl(var(--accent));
}
/* Buttons — focus ring follows the active preset. */
.earthen-button:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* Article aside — keeps the byline rail comfortable on mobile. */
.earthen-aside {
color: hsl(var(--muted-foreground));
}
/* Ensure article main column uses the Work Sans body fallback. */
.earthen-article {
font-family: var(--font-body, "Work Sans", ui-sans-serif, system-ui, sans-serif);
color: hsl(var(--foreground));
}
/* Divider glyph — softer stroke at rest. */
.earthen-glyph {
color: hsl(var(--primary));
}
/* Organic blob soft asymmetric corners for cards, media frames and hero art.
The signature earthen shape: hand-crafted, never a hard rectangle. */
.earthen-blob {
border-radius: 42% 58% 62% 38% / 42% 44% 56% 58%;
}
.earthen-blob-soft {
border-radius: 1.75rem 2.25rem 1.75rem 2.25rem;
}
/* Soft card / panel frame — cream surface with a hairline botanical border. */
.earthen-panel {
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
border: 1px solid hsl(var(--border));
box-shadow: inset 0 0 0 1px hsl(var(--border) / 0.35);
}
/* Terracotta seedling button — the primary call to action across the theme. */
.earthen-cta {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
border-radius: 0.75rem;
transition: box-shadow 200ms ease, transform 120ms ease;
}
.earthen-cta:hover,
.earthen-cta:focus-visible {
box-shadow: 0 6px 0 -2px hsl(var(--accent) / 0.35);
transform: translateY(-1px);
}
.earthen-cta:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* Sage link — dotted botanical underline that fills on hover. */
.earthen-link {
color: hsl(var(--primary));
text-decoration: none;
border-bottom: 1px dotted hsl(var(--primary) / 0.6);
transition: color 150ms ease, border-color 150ms ease;
}
.earthen-link:hover,
.earthen-link:focus-visible {
color: hsl(var(--accent));
border-bottom-color: hsl(var(--accent));
}
/* Leaf line-draw — a hairline that draws in on scroll where motion is allowed. */
@media (prefers-reduced-motion: no-preference) {
.earthen-draw {
stroke-dasharray: 240;
stroke-dashoffset: 240;
animation: earthen-draw-in 1.4s ease forwards;
}
@keyframes earthen-draw-in {
to { stroke-dashoffset: 0; }
}
.earthen-blob-morph {
animation: earthen-blob-morph 18s ease-in-out infinite;
}
@keyframes earthen-blob-morph {
0%, 100% { border-radius: 42% 58% 62% 38% / 42% 44% 56% 58%; }
50% { border-radius: 58% 42% 38% 62% / 56% 58% 42% 44%; }
}
}
/* Responsive layout primitives
These are shipped as compiled CSS (not Tailwind utilities) on purpose: the
host Tailwind JIT does not scan a theme's page/layout, override or
master-page templates, so responsive grid utilities that live only in those
files (e.g. sm:grid-cols-3, md:grid-cols-[2fr_1fr]) are silently dropped and
never collapse or expand. Compiling them here guarantees the layout works on
both mobile and desktop. Layout-only no colour tokens. */
/* Donation CTA — preset amount tiles: 1-up on mobile, 3-up from 640px. */
.earthen-amount-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
}
@media (min-width: 640px) {
.earthen-amount-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* Donation CTA — custom amount + submit: stack on mobile, inline from 640px. */
.earthen-amount-row {
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: stretch;
}
@media (min-width: 640px) {
.earthen-amount-row { flex-direction: row; align-items: center; }
}
/* Field Journal — post cards: 1-up mobile, 2-up >=640px, 3-up >=1024px. */
.earthen-post-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
@media (min-width: 640px) {
.earthen-post-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
.earthen-post-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* Contact — form + aside: stacked on mobile, 2fr/1fr from 768px. */
.earthen-contact-grid {
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
}
@media (min-width: 768px) {
.earthen-contact-grid { grid-template-columns: 2fr 1fr; }
}
/* Article — body + byline rail: stacked on mobile, main + 260px rail >=1024px. */
.earthen-article-grid {
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
}
@media (min-width: 1024px) {
.earthen-article-grid { grid-template-columns: minmax(0, 1fr) 260px; }
}
/* Hero comfortable padded band. A text-only (image-less) hero uses padding
only, so it never balloons into a tall empty band. When a background image
IS present the taller band is applied via the --media modifier. */
.earthen-hero {
padding-top: 4rem;
padding-bottom: 4rem;
}
@media (min-width: 768px) {
.earthen-hero { padding-top: 6rem; padding-bottom: 6rem; }
}
.earthen-hero.earthen-hero--media {
min-height: 60vh;
padding-top: 3rem;
padding-bottom: 3rem;
}
@media (min-width: 768px) {
.earthen-hero.earthen-hero--media { min-height: 72vh; }
}
/* Split hero the media column needs a floor so its absolutely-positioned
image has height to fill. Only rendered when an image is present. */
.earthen-hero-split-media {
position: relative;
min-height: 320px;
}

26
blocks/blocks.yaml Normal file
View File

@ -0,0 +1,26 @@
# Earthen theme block definitions (codeless .bnp, WO-WZ-021 / WO-TF-016).
# Only TRUE persona furniture remains as theme blocks. Everything that
# duplicated a builtin (footer, stats/impact-metrics, logo-strip/partner-logos,
# divider/botanical-divider) is now a template OVERRIDE of the builtin key
# (templates/overrides/earthen/). Keys are fully qualified (earthen:<key>) — the
# codeless loader registers definitions verbatim; the qualified keys match the
# data-block attributes, master_pages.json and seed references.
blocks:
- key: earthen:donation_cta
title: Donation CTA
description: Donation appeal with preset amount tiles and a custom amount field.
category: content
schema: donation_cta.schema.json
template: donation_cta.ninjatpl
- key: earthen:field_note
title: Field Note
description: Dispatch from the field with byline, location and a lede image.
category: content
schema: field_note.schema.json
template: field_note.ninjatpl
- key: earthen:impact_report
title: Impact Report
description: Narrative impact report with a summary lede and outcome rows.
category: content
schema: impact_report.schema.json
template: impact_report.ninjatpl

View File

@ -0,0 +1,16 @@
<section data-block="earthen:donation_cta"{% if not headline and not body and not amounts and not buttonLabel and not processorUrl %} data-empty{% endif %} class="earthen-donation-cta py-16 px-4" style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground));">
<div class="max-w-3xl mx-auto text-center">
{% if headline %}<h2 class="earthen-display text-3xl md:text-4xl mb-4" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--primary));">{{ headline }}</h2>{% endif %}
{% if body %}<div class="earthen-body text-lg mb-8 mx-auto max-w-xl" style="font-family: var(--font-body, \000022Work Sans\000022, ui-sans-serif, system-ui, sans-serif); color: hsl(var(--foreground));">{{ body|safe }}</div>{% endif %}
<form action="{{ processorUrl|default:"#" }}" method="get" class="space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3" role="radiogroup" aria-label="Preset donation amounts">
{% if amounts %}{% for a in amounts %}<label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="{{ a }}" class="sr-only"{% if forloop.First %} checked{% endif %}><span>${{ a }}</span></label>{% endfor %}{% else %}<label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="25" class="sr-only" checked><span>$25</span></label><label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="50" class="sr-only"><span>$50</span></label><label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="100" class="sr-only"><span>$100</span></label>{% endif %}
</div>
<div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<label class="sr-only" for="earthen-custom-amount">Custom amount</label>
<input id="earthen-custom-amount" type="number" name="customAmount" min="1" step="1" placeholder="Other amount" class="flex-1 rounded-lg py-3 px-4 text-base" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--input));">
<button type="submit" class="earthen-button inline-block rounded-lg py-3 px-6 font-semibold transition-colors" style="background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground));">{{ buttonLabel|default:"Donate" }}</button>
</div>
</form>
</div>
</section>

View File

@ -0,0 +1,8 @@
<article data-block="earthen:field_note"{% if not author and not location and not dateline and not body and not image %} data-empty{% endif %} class="earthen-field-note earthen-paper-frame mx-auto my-12 max-w-3xl p-8 rounded-lg" style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border));">
<header class="mb-6">
<div class="earthen-byline flex flex-wrap gap-x-3 gap-y-1 uppercase tracking-wider text-xs mb-2" style="color: hsl(var(--muted-foreground));">{% if author %}<span class="earthen-byline-author">By {{ author }}</span>{% endif %}{% if location %}<span class="earthen-byline-location">{{ location }}</span>{% endif %}{% if dateline %}<span class="earthen-byline-date">{{ dateline }}</span>{% endif %}</div>
<div class="botanical-rule" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="28" height="28" data-motif="seed" class="earthen-glyph" style="color: hsl(var(--accent));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="24" cy="24" rx="6" ry="10"></ellipse><path d="M24 14V6M18 18l-6-6M30 18l6-6M24 34v8M18 30l-6 6M30 30l6 6"></path></svg></div>
</header>
{% if image %}<figure class="my-6 earthen-paper-frame-inner overflow-hidden earthen-blob-soft">{% img image alt="" class="w-full h-auto" %}</figure>{% endif %}
{% if body %}<div class="earthen-body drop-cap prose-base" style="font-family: var(--font-body, \000022Work Sans\000022, ui-sans-serif, system-ui, sans-serif); color: hsl(var(--foreground));">{{ body|safe }}</div>{% else %}<p class="earthen-empty" style="color: hsl(var(--muted-foreground));">No dispatch yet — add a body to share what you saw in the field.</p>{% endif %}
</article>

View File

@ -0,0 +1,14 @@
<section data-block="earthen:impact_report"{% if not title and not summary and not outcomes and not period %} data-empty{% endif %} class="earthen-impact-report bg-paper-grain py-16 px-4" style="color: hsl(var(--foreground));">
<div class="earthen-panel earthen-blob-soft mx-auto max-w-4xl p-8 md:p-12">
<div class="mb-8 flex items-start gap-4">
<span class="mt-1 shrink-0" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="40" height="40" data-motif="fern" class="earthen-glyph" style="color: hsl(var(--primary));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg></span>
<div>
{% if period %}<p class="mb-1 text-xs uppercase tracking-widest" style="color: hsl(var(--accent)); font-family: var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ period }}</p>{% endif %}
{% if title %}<h2 class="crayon-underline text-3xl md:text-4xl" style="font-family: var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif); color: hsl(var(--primary));">{{ title }}</h2>{% endif %}
</div>
</div>
{% if summary %}<div class="mb-10 max-w-2xl text-lg leading-relaxed" style="font-family: var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif); color: hsl(var(--foreground));">{{ summary|safe }}</div>{% endif %}
{% if outcomes %}<dl class="divide-y" style="border-color: hsl(var(--border));">{% for o in outcomes %}<div class="flex flex-col gap-1 py-5 sm:flex-row sm:items-baseline sm:gap-6" style="border-color: hsl(var(--border));"><dt class="shrink-0 text-4xl font-semibold sm:w-48" style="font-family: var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif); color: hsl(var(--accent));">{{ o.value }}</dt><dd class="flex-1"><p class="text-base font-semibold" style="color: hsl(var(--foreground));">{{ o.label }}</p>{% if o.detail %}<p class="mt-1 text-sm" style="color: hsl(var(--muted-foreground));">{{ o.detail }}</p>{% endif %}</dd></div>{% endfor %}</dl>{% else %}<p class="text-center" style="color: hsl(var(--muted-foreground));">Add outcomes to show what this season grew.</p>{% endif %}
{% if sourceNote %}<p class="mt-8 text-xs italic" style="color: hsl(var(--muted-foreground));">{{ sourceNote }}</p>{% endif %}
</div>
</section>

View File

@ -0,0 +1,62 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Impact Report",
"description": "Narrative impact report with a summary lede and a list of outcome rows",
"type": "object",
"properties": {
"period": {
"type": "string",
"title": "Period",
"description": "Reporting period label (e.g. 'Season 2025')",
"x-editor": "text"
},
"title": {
"type": "string",
"title": "Title",
"description": "Report heading",
"x-editor": "text",
"default": "What we grew together"
},
"summary": {
"type": "string",
"title": "Summary",
"description": "Opening paragraph of the report",
"x-editor": "richtext"
},
"outcomes": {
"type": "array",
"title": "Outcomes",
"description": "Reported outcomes shown as rows",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string",
"title": "Value",
"description": "Headline figure or result (e.g. '840 acres')",
"x-editor": "text"
},
"label": {
"type": "string",
"title": "Label",
"description": "What the figure counts",
"x-editor": "text"
},
"detail": {
"type": "string",
"title": "Detail",
"description": "One line of context",
"x-editor": "text"
}
}
}
},
"sourceNote": {
"type": "string",
"title": "Source Note",
"description": "Footnote crediting the source of the figures",
"x-editor": "text"
}
}
}

View File

@ -1,43 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// BotanicalDividerBlockMeta defines metadata for the botanical divider block.
var BotanicalDividerBlockMeta = blocks.BlockMeta{
Key: "botanical_divider",
Title: "Botanical Divider",
Description: "Hand-illustrated SVG section break",
Source: "earthen",
Category: blocks.CategoryLayout,
}
// BotanicalDividerBlock renders the botanical divider block.
// Content shape: {motif:"fern"|"root"|"seed"}
func BotanicalDividerBlock(ctx context.Context, content map[string]any) string {
motif := getString(content, "motif")
switch motif {
case "fern", "root", "seed":
default:
motif = "fern"
}
data := BotanicalDividerData{
Motif: motif,
Empty: len(content) == 0,
}
var buf bytes.Buffer
_ = botanicalDividerComponent(data).Render(ctx, &buf)
return buf.String()
}
// BotanicalDividerData contains data for the divider component.
type BotanicalDividerData struct {
Motif string
Empty bool
}

View File

@ -1,79 +0,0 @@
package main
// botanicalDividerComponent renders a centred botanical SVG flourish flanked by hairlines.
templ botanicalDividerComponent(data BotanicalDividerData) {
<div
data-block="earthen:botanical_divider"
data-empty?={ data.Empty }
class="earthen-divider flex items-center justify-center gap-4 my-12 px-4"
aria-hidden="true"
>
<span class="earthen-divider-rule flex-1 h-px" style="background-color: hsl(var(--border));"></span>
@botanicalGlyph(data.Motif, 48)
<span class="earthen-divider-rule flex-1 h-px" style="background-color: hsl(var(--border));"></span>
</div>
}
// botanicalGlyph renders the SVG illustration for the named motif.
templ botanicalGlyph(motif string, size int) {
switch motif {
case "root":
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width={ size }
height={ size }
data-motif="root"
class="earthen-glyph"
style="color: hsl(var(--primary));"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M24 4v18"/>
<path d="M24 22c-4 4-9 5-13 4M24 22c4 4 9 5 13 4M24 22c-2 7-3 12-5 16M24 22c2 7 3 12 5 16M24 22c0 9-1 15-1 20"/>
<circle cx="24" cy="22" r="2"/>
</svg>
case "seed":
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width={ size }
height={ size }
data-motif="seed"
class="earthen-glyph"
style="color: hsl(var(--accent));"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linecap="round"
stroke-linejoin="round"
>
<ellipse cx="24" cy="24" rx="6" ry="10"/>
<path d="M24 14V6M18 18l-6-6M30 18l6-6M24 34v8M18 30l-6 6M30 30l6 6"/>
</svg>
default:
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width={ size }
height={ size }
data-motif="fern"
class="earthen-glyph"
style="color: hsl(var(--primary));"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M24 44V8"/>
<path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"/>
<path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"/>
<path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"/>
<path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"/>
</svg>
}
}

View File

@ -1,180 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// botanicalDividerComponent renders a centred botanical SVG flourish flanked by hairlines.
func botanicalDividerComponent(data BotanicalDividerData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div data-block=\"earthen:botanical_divider\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Empty {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " data-empty")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " class=\"earthen-divider flex items-center justify-center gap-4 my-12 px-4\" aria-hidden=\"true\"><span class=\"earthen-divider-rule flex-1 h-px\" style=\"background-color: hsl(var(--border));\"></span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = botanicalGlyph(data.Motif, 48).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"earthen-divider-rule flex-1 h-px\" style=\"background-color: hsl(var(--border));\"></span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// botanicalGlyph renders the SVG illustration for the named motif.
func botanicalGlyph(motif string, size int) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch motif {
case "root":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\" width=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(size)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `botanical_divider.templ`, Line: 24, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" height=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(size)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `botanical_divider.templ`, Line: 25, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" data-motif=\"root\" class=\"earthen-glyph\" style=\"color: hsl(var(--primary));\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M24 4v18\"></path> <path d=\"M24 22c-4 4-9 5-13 4M24 22c4 4 9 5 13 4M24 22c-2 7-3 12-5 16M24 22c2 7 3 12 5 16M24 22c0 9-1 15-1 20\"></path> <circle cx=\"24\" cy=\"22\" r=\"2\"></circle></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "seed":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\" width=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(size)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `botanical_divider.templ`, Line: 43, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" height=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(size)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `botanical_divider.templ`, Line: 44, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" data-motif=\"seed\" class=\"earthen-glyph\" style=\"color: hsl(var(--accent));\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><ellipse cx=\"24\" cy=\"24\" rx=\"6\" ry=\"10\"></ellipse> <path d=\"M24 14V6M18 18l-6-6M30 18l6-6M24 34v8M18 30l-6 6M30 30l6 6\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\" width=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(size)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `botanical_divider.templ`, Line: 61, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" height=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(size)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `botanical_divider.templ`, Line: 62, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" data-motif=\"fern\" class=\"earthen-glyph\" style=\"color: hsl(var(--primary));\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M24 44V8\"></path> <path d=\"M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0\"></path> <path d=\"M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1\"></path> <path d=\"M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3\"></path> <path d=\"M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5\"></path></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,27 +0,0 @@
package main
import (
"bytes"
"context"
)
// EarthenButtonBlock renders a button with Earthen styling.
// Content expects: {"label": "...", "url": "...", "variant": "primary|secondary|ghost|destructive"}
func EarthenButtonBlock(ctx context.Context, content map[string]any) string {
label := getString(content, "label")
if label == "" {
label = getString(content, "text")
}
url := getString(content, "url")
if url == "" {
url = getString(content, "href")
}
variant := getString(content, "variant")
if variant == "" {
variant = "primary"
}
var buf bytes.Buffer
_ = earthenButtonComponent(label, url, variant).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,34 +0,0 @@
package main
// earthenButtonStyle returns inline CSS based on variant.
func earthenButtonStyle(variant string) string {
switch variant {
case "secondary":
return "background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border: 1px solid hsl(var(--border));"
case "ghost":
return "background-color: transparent; color: hsl(var(--primary)); border: 1.5px dashed hsl(var(--primary) / 0.6);"
case "destructive":
return "background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground));"
default:
return "background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground));"
}
}
// earthenButtonComponent renders a clay-fired button with Earthen styling.
templ earthenButtonComponent(label, url, variant string) {
<a
href={ templ.SafeURL(earthenButtonURL(url)) }
data-variant={ variant }
class="earthen-button inline-flex items-center justify-center px-6 py-3 rounded-lg font-semibold transition-colors focus:outline-none"
style={ earthenButtonStyle(variant) }
>
{ label }
</a>
}
func earthenButtonURL(url string) string {
if url == "" {
return "#"
}
return url
}

View File

@ -1,114 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// earthenButtonStyle returns inline CSS based on variant.
func earthenButtonStyle(variant string) string {
switch variant {
case "secondary":
return "background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border: 1px solid hsl(var(--border));"
case "ghost":
return "background-color: transparent; color: hsl(var(--primary)); border: 1.5px dashed hsl(var(--primary) / 0.6);"
case "destructive":
return "background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground));"
default:
return "background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground));"
}
}
// earthenButtonComponent renders a clay-fired button with Earthen styling.
func earthenButtonComponent(label, url, variant string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(earthenButtonURL(url)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 20, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" data-variant=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(variant)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 21, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"earthen-button inline-flex items-center justify-center px-6 py-3 rounded-lg font-semibold transition-colors focus:outline-none\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(earthenButtonStyle(variant))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 23, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 25, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func earthenButtonURL(url string) string {
if url == "" {
return "#"
}
return url
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,33 +0,0 @@
package main
import (
"bytes"
"context"
)
// EarthenCardBlock renders a card with softer radius and ink-edge border.
// Content expects: {"title":"…","body":"…","image":"…","link":"…","linkLabel":"…"}
func EarthenCardBlock(ctx context.Context, content map[string]any) string {
data := EarthenCardData{
Title: getString(content, "title"),
Body: getString(content, "body"),
Image: getString(content, "image"),
Link: getString(content, "link"),
LinkLabel: getString(content, "linkLabel"),
Empty: len(content) == 0,
}
var buf bytes.Buffer
_ = earthenCardComponent(data).Render(ctx, &buf)
return buf.String()
}
// EarthenCardData is the data shape for the card override component.
type EarthenCardData struct {
Title string
Body string
Image string
Link string
LinkLabel string
Empty bool
}

View File

@ -1,46 +0,0 @@
package main
// earthenCardComponent renders a card with soft radius and ink-edge border.
templ earthenCardComponent(data EarthenCardData) {
<article
class="earthen-card rounded-xl overflow-hidden p-6"
data-empty?={ data.Empty }
style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border)); box-shadow: inset 0 0 0 1px hsl(var(--border) / 0.4);"
>
if data.Image != "" {
<div class="earthen-card-image -mx-6 -mt-6 mb-4 overflow-hidden">
<img src={ resolveMedia(data.Image) } alt={ data.Title } class="w-full h-auto"/>
</div>
}
if data.Title != "" {
<h3
class="earthen-card-title text-2xl mb-3"
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));" }
>
{ data.Title }
</h3>
}
if data.Body != "" {
<div
class="earthen-card-body mb-4"
style={ "font-family: var(--font-body, \"Spectral\", Georgia, serif); color: hsl(var(--foreground));" }
>
@templ.Raw(data.Body)
</div>
}
if data.Link != "" {
<a
href={ templ.SafeURL(data.Link) }
class="earthen-card-link inline-flex items-center gap-2 font-semibold"
style="color: hsl(var(--accent));"
>
if data.LinkLabel != "" {
<span>{ data.LinkLabel }</span>
} else {
<span>Read more</span>
}
<span aria-hidden="true">→</span>
</a>
}
</article>
}

View File

@ -1,193 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// earthenCardComponent renders a card with soft radius and ink-edge border.
func earthenCardComponent(data EarthenCardData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<article class=\"earthen-card rounded-xl overflow-hidden p-6\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Empty {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " data-empty")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " style=\"background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border)); box-shadow: inset 0 0 0 1px hsl(var(--border) / 0.4);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Image != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"earthen-card-image -mx-6 -mt-6 mb-4 overflow-hidden\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(resolveMedia(data.Image))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 12, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 12, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"w-full h-auto\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"earthen-card-title text-2xl mb-3\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 18, Col: 127}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 20, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Body != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"earthen-card-body mb-4\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-body, \"Spectral\", Georgia, serif); color: hsl(var(--foreground));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 26, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Link != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 templ.SafeURL
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(data.Link))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 33, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"earthen-card-link inline-flex items-center gap-2 font-semibold\" style=\"color: hsl(var(--accent));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.LinkLabel != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.LinkLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `card_override.templ`, Line: 38, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span>Read more</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span aria-hidden=\"true\">→</span></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,67 +0,0 @@
package main
import (
"bytes"
"context"
"strconv"
"git.dev.alexdunmow.com/block/core/blocks"
)
// DonationCTABlockMeta defines metadata for the donation CTA block.
var DonationCTABlockMeta = blocks.BlockMeta{
Key: "donation_cta",
Title: "Donation CTA",
Description: "Donation call-to-action with preset amount tiles and custom amount field",
Source: "earthen",
Category: blocks.CategoryContent,
}
// DonationCTABlock renders the donation call-to-action block.
// Content shape: {headline, body, amounts:[int], buttonLabel, processorUrl}
func DonationCTABlock(ctx context.Context, content map[string]any) string {
amounts := getNumberSlice(content, "amounts")
if len(amounts) == 0 {
amounts = []float64{25, 50, 100}
}
tiles := make([]DonationTile, 0, len(amounts))
for _, a := range amounts {
tiles = append(tiles, DonationTile{
Value: a,
Label: "$" + strconv.FormatFloat(a, 'f', -1, 64),
})
}
data := DonationCTAData{
Headline: getString(content, "headline"),
Body: getString(content, "body"),
ButtonLabel: getString(content, "buttonLabel"),
ProcessorURL: getString(content, "processorUrl"),
Tiles: tiles,
Empty: len(content) == 0,
}
if data.ButtonLabel == "" {
data.ButtonLabel = "Donate"
}
var buf bytes.Buffer
_ = donationCTAComponent(data).Render(ctx, &buf)
return buf.String()
}
// DonationCTAData contains data for the donation CTA component.
type DonationCTAData struct {
Headline string
Body string
ButtonLabel string
ProcessorURL string
Tiles []DonationTile
Empty bool
}
// DonationTile is a single preset amount tile.
type DonationTile struct {
Value float64
Label string
}

View File

@ -1,79 +0,0 @@
package main
import "strconv"
// donationCTAComponent renders the moss/clay donation CTA section.
templ donationCTAComponent(data DonationCTAData) {
<section
data-block="earthen:donation_cta"
data-empty?={ data.Empty }
class="earthen-donation-cta py-16 px-4"
style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground));"
>
<div class="max-w-3xl mx-auto text-center">
if data.Headline != "" {
<h2
class="earthen-display text-3xl md:text-4xl mb-4"
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));" }
>
{ data.Headline }
</h2>
}
if data.Body != "" {
<div
class="earthen-body text-lg mb-8 mx-auto max-w-xl"
style={ "font-family: var(--font-body, \"Spectral\", Georgia, serif); color: hsl(var(--foreground));" }
>
@templ.Raw(data.Body)
</div>
}
<form action={ templ.SafeURL(donationFormAction(data.ProcessorURL)) } method="get" class="space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3" role="radiogroup" aria-label="Preset donation amounts">
for i, tile := range data.Tiles {
<label
data-amount-tile
class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors"
style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"
>
<input
type="radio"
name="amount"
value={ strconv.FormatFloat(tile.Value, 'f', -1, 64) }
class="sr-only"
checked?={ i == 0 }
/>
<span>{ tile.Label }</span>
</label>
}
</div>
<div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<label class="sr-only" for="earthen-custom-amount">Custom amount</label>
<input
id="earthen-custom-amount"
type="number"
name="customAmount"
min="1"
step="1"
placeholder="Other amount"
class="flex-1 rounded-lg py-3 px-4 text-base"
style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--input));"
/>
<button
type="submit"
class="earthen-button inline-block rounded-lg py-3 px-6 font-semibold transition-colors"
style="background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground));"
>
{ data.ButtonLabel }
</button>
</div>
</form>
</div>
</section>
}
func donationFormAction(url string) string {
if url == "" {
return "#"
}
return url
}

View File

@ -1,195 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "strconv"
// donationCTAComponent renders the moss/clay donation CTA section.
func donationCTAComponent(data DonationCTAData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"earthen:donation_cta\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Empty {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " data-empty")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " class=\"earthen-donation-cta py-16 px-4\" style=\"background-color: hsl(var(--card)); color: hsl(var(--card-foreground));\"><div class=\"max-w-3xl mx-auto text-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Headline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h2 class=\"earthen-display text-3xl md:text-4xl mb-4\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `donation_cta.templ`, Line: 17, Col: 128}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Headline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `donation_cta.templ`, Line: 19, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Body != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"earthen-body text-lg mb-8 mx-auto max-w-xl\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-body, \"Spectral\", Georgia, serif); color: hsl(var(--foreground));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `donation_cta.templ`, Line: 25, Col: 106}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<form action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(donationFormAction(data.ProcessorURL)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `donation_cta.templ`, Line: 30, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" method=\"get\" class=\"space-y-6\"><div class=\"grid grid-cols-1 sm:grid-cols-3 gap-3\" role=\"radiogroup\" aria-label=\"Preset donation amounts\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for i, tile := range data.Tiles {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<label data-amount-tile class=\"earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors\" style=\"border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));\"><input type=\"radio\" name=\"amount\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatFloat(tile.Value, 'f', -1, 64))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `donation_cta.templ`, Line: 41, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"sr-only\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if i == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " checked")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(tile.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `donation_cta.templ`, Line: 45, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></label>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div><div class=\"flex flex-col sm:flex-row gap-3 items-stretch sm:items-center\"><label class=\"sr-only\" for=\"earthen-custom-amount\">Custom amount</label> <input id=\"earthen-custom-amount\" type=\"number\" name=\"customAmount\" min=\"1\" step=\"1\" placeholder=\"Other amount\" class=\"flex-1 rounded-lg py-3 px-4 text-base\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--input));\"> <button type=\"submit\" class=\"earthen-button inline-block rounded-lg py-3 px-6 font-semibold transition-colors\" style=\"background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.ButtonLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `donation_cta.templ`, Line: 66, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</button></div></form></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func donationFormAction(url string) string {
if url == "" {
return "#"
}
return url
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,17 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/templates"
)
// EarthenEmailWrapper wraps body content in a botanical cream-and-moss email frame.
func EarthenEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
if err := earthenEmailTemplate(emailCtx, body).Render(context.Background(), &buf); err != nil {
return body
}
return buf.String()
}

View File

@ -1,154 +0,0 @@
package main
import (
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// earthenEmailTemplate renders the cream/moss email wrapper.
templ earthenEmailTemplate(emailCtx templates.EmailContext, body string) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<title>{ emailCtx.SiteSettings.SiteName }</title>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Spectral', Georgia, serif; color: %s;", earthenEmailBg(emailCtx), earthenEmailFg(emailCtx)) }>
if emailCtx.PreviewText != "" {
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">
{ emailCtx.PreviewText }
</div>
}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("background-color: %s;", earthenEmailBg(emailCtx)) }>
<tr>
<td align="center" style="padding: 32px 12px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s; border-radius: 12px; overflow: hidden;", earthenEmailCard(emailCtx), earthenEmailBorder(emailCtx)) }>
<tr>
<td align="center" style={ fmt.Sprintf("padding: 28px 32px; background-color: %s; color: %s;", earthenEmailPrimary(emailCtx), earthenEmailPrimaryFg(emailCtx)) }>
if emailCtx.SiteSettings.LogoURL != "" {
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } style="max-height: 44px; width: auto; display: block;"/>
} else if emailCtx.SiteSettings.SiteName != "" {
<h1 style={ fmt.Sprintf("margin: 0; font-family: 'Fraunces', 'Playfair Display', Georgia, serif; font-size: 26px; font-weight: 600; letter-spacing: -0.01em; color: %s;", earthenEmailPrimaryFg(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</h1>
}
</td>
</tr>
<tr>
<td style={ fmt.Sprintf("padding: 36px 40px; font-size: 16px; line-height: 1.7; color: %s; font-family: 'Spectral', Georgia, serif;", earthenEmailFg(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<tr>
<td style={ fmt.Sprintf("padding: 24px 40px; background-color: %s; border-top: 1px solid %s;", earthenEmailMuted(emailCtx), earthenEmailBorder(emailCtx)) }>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center">
<p style={ fmt.Sprintf("margin: 0 0 8px; font-family: 'Fraunces', 'Playfair Display', Georgia, serif; font-size: 14px; font-weight: 600; color: %s;", earthenEmailFg(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</p>
<p style={ fmt.Sprintf("margin: 0 0 6px; font-size: 11px; color: %s;", earthenEmailMutedFg(emailCtx)) }>
Registered charity. We respect your inbox.
</p>
if emailCtx.SiteSettings.SiteURL != "" {
<p style={ fmt.Sprintf("margin: 0 0 12px; font-size: 12px; color: %s;", earthenEmailMutedFg(emailCtx)) }>
<a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ fmt.Sprintf("color: %s; text-decoration: none; border-bottom: 1px dotted %s;", earthenEmailPrimary(emailCtx), earthenEmailPrimary(emailCtx)) }>
{ emailCtx.SiteSettings.SiteURL }
</a>
</p>
}
if emailCtx.UnsubscribeURL != "" {
<p style={ fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", earthenEmailMutedFg(emailCtx)) }>
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", earthenEmailMutedFg(emailCtx)) }>
Unsubscribe
</a>
</p>
} else {
<p style={ fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", earthenEmailMutedFg(emailCtx)) }>
<a href="#" style={ fmt.Sprintf("color: %s; text-decoration: underline;", earthenEmailMutedFg(emailCtx)) }>
Unsubscribe
</a>
</p>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}
// Email colour helpers — fall back to cream/moss defaults when EmailColors are empty.
func earthenEmailBg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Background != "" {
return emailCtx.Colors.Background
}
return "#F6F1E6"
}
func earthenEmailCard(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Card != "" {
return emailCtx.Colors.Card
}
return "#FCF9F2"
}
func earthenEmailFg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Foreground != "" {
return emailCtx.Colors.Foreground
}
return "#2D3A28"
}
func earthenEmailPrimary(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Primary != "" {
return emailCtx.Colors.Primary
}
return "#3A5A36"
}
func earthenEmailPrimaryFg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.PrimaryForeground != "" {
return emailCtx.Colors.PrimaryForeground
}
return "#F6F1E6"
}
func earthenEmailBorder(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Border != "" {
return emailCtx.Colors.Border
}
return "#D6D1C2"
}
func earthenEmailMuted(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Muted != "" {
return emailCtx.Colors.Muted
}
return "#EFEBDF"
}
func earthenEmailMutedFg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.MutedForeground != "" {
return emailCtx.Colors.MutedForeground
}
return "#6E7560"
}

View File

@ -1,470 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// earthenEmailTemplate renders the cream/moss email wrapper.
func earthenEmailTemplate(emailCtx templates.EmailContext, body string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><!--[if mso]>\n\t\t\t<noscript>\n\t\t\t\t<xml>\n\t\t\t\t\t<o:OfficeDocumentSettings>\n\t\t\t\t\t\t<o:PixelsPerInch>96</o:PixelsPerInch>\n\t\t\t\t\t</o:OfficeDocumentSettings>\n\t\t\t\t</xml>\n\t\t\t</noscript>\n\t\t\t<![endif]--><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 27, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title></head><body style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Spectral', Georgia, serif; color: %s;", earthenEmailBg(emailCtx), earthenEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 29, Col: 179}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.PreviewText != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display: none; max-height: 0; overflow: hidden; mso-hide: all;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 32, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s;", earthenEmailBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 35, Col: 156}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><tr><td align=\"center\" style=\"padding: 32px 12px;\"><table role=\"presentation\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s; border-radius: 12px; overflow: hidden;", earthenEmailCard(emailCtx), earthenEmailBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 38, Col: 269}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 28px 32px; background-color: %s; color: %s;", earthenEmailPrimary(emailCtx), earthenEmailPrimaryFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 40, Col: 166}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.LogoURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.LogoURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 42, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 42, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" style=\"max-height: 44px; width: auto; display: block;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if emailCtx.SiteSettings.SiteName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h1 style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-family: 'Fraunces', 'Playfair Display', Georgia, serif; font-size: 26px; font-weight: 600; letter-spacing: -0.01em; color: %s;", earthenEmailPrimaryFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 44, Col: 212}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 45, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</td></tr><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 36px 40px; font-size: 16px; line-height: 1.7; color: %s; font-family: 'Spectral', Georgia, serif;", earthenEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 51, Col: 167}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td></tr><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 24px 40px; background-color: %s; border-top: 1px solid %s;", earthenEmailMuted(emailCtx), earthenEmailBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 56, Col: 161}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\"><p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0 0 8px; font-family: 'Fraunces', 'Playfair Display', Georgia, serif; font-size: 14px; font-weight: 600; color: %s;", earthenEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 60, Col: 187}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 61, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p><p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0 0 6px; font-size: 11px; color: %s;", earthenEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 63, Col: 113}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">Registered charity. We respect your inbox.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.SiteURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0 0 12px; font-size: 12px; color: %s;", earthenEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 67, Col: 115}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 templ.SafeURL
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.SiteSettings.SiteURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 68, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: none; border-bottom: 1px dotted %s;", earthenEmailPrimary(emailCtx), earthenEmailPrimary(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 68, Col: 219}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 69, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", earthenEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 74, Col: 108}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 templ.SafeURL
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.UnsubscribeURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 75, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", earthenEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 75, Col: 157}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">Unsubscribe</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<p style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-size: 11px; color: %s;", earthenEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 80, Col: 108}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"><a href=\"#\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", earthenEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 81, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">Unsubscribe</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</td></tr></table></td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Email colour helpers — fall back to cream/moss defaults when EmailColors are empty.
func earthenEmailBg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Background != "" {
return emailCtx.Colors.Background
}
return "#F6F1E6"
}
func earthenEmailCard(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Card != "" {
return emailCtx.Colors.Card
}
return "#FCF9F2"
}
func earthenEmailFg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Foreground != "" {
return emailCtx.Colors.Foreground
}
return "#2D3A28"
}
func earthenEmailPrimary(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Primary != "" {
return emailCtx.Colors.Primary
}
return "#3A5A36"
}
func earthenEmailPrimaryFg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.PrimaryForeground != "" {
return emailCtx.Colors.PrimaryForeground
}
return "#F6F1E6"
}
func earthenEmailBorder(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Border != "" {
return emailCtx.Colors.Border
}
return "#D6D1C2"
}
func earthenEmailMuted(emailCtx templates.EmailContext) string {
if emailCtx.Colors.Muted != "" {
return emailCtx.Colors.Muted
}
return "#EFEBDF"
}
func earthenEmailMutedFg(emailCtx templates.EmailContext) string {
if emailCtx.Colors.MutedForeground != "" {
return emailCtx.Colors.MutedForeground
}
return "#6E7560"
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,64 +0,0 @@
package main
import (
"embed"
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/plugin"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
// Assets returns the embedded assets filesystem.
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "assets")
return sub
}
// Schemas returns the embedded schemas filesystem.
func Schemas() fs.FS {
sub, _ := fs.Sub(schemasFS, "schemas")
return sub
}
// AssetsHandler returns an http.Handler that serves the embedded assets.
func AssetsHandler() http.Handler {
return http.FileServer(http.FS(Assets()))
}
// ThemePresets returns the embedded theme presets JSON.
func ThemePresets() []byte {
return presetsData
}
// BundledFonts returns the embedded fonts manifest JSON.
func BundledFonts() []byte {
return fontsData
}
// ThemeCSSManifest returns the theme's custom CSS utilities injected into
// the host Tailwind input so paper-grain, drop-cap, and botanical rules
// survive the global purge.
func ThemeCSSManifest() *plugin.CSSManifest {
css, err := assetsFS.ReadFile("assets/css/earthen.css")
if err != nil {
return &plugin.CSSManifest{}
}
return &plugin.CSSManifest{
InputCSSAppend: string(css),
}
}

View File

@ -1,44 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// FieldNoteBlockMeta defines metadata for the field note block.
var FieldNoteBlockMeta = blocks.BlockMeta{
Key: "field_note",
Title: "Field Note",
Description: "Article-style dispatch from the field with byline and location",
Source: "earthen",
Category: blocks.CategoryContent,
}
// FieldNoteBlock renders the field note block.
// Content shape: {author, location, dateline, body, image}
func FieldNoteBlock(ctx context.Context, content map[string]any) string {
data := FieldNoteData{
Author: getString(content, "author"),
Location: getString(content, "location"),
Dateline: getString(content, "dateline"),
Body: getString(content, "body"),
Image: getString(content, "image"),
Empty: len(content) == 0,
}
var buf bytes.Buffer
_ = fieldNoteComponent(data).Render(ctx, &buf)
return buf.String()
}
// FieldNoteData contains data for the field note component.
type FieldNoteData struct {
Author string
Location string
Dateline string
Body string
Image string
Empty bool
}

View File

@ -1,52 +0,0 @@
package main
// fieldNoteComponent renders an article-style dispatch from the field.
templ fieldNoteComponent(data FieldNoteData) {
<article
data-block="earthen:field_note"
data-empty?={ data.Empty }
class="earthen-field-note earthen-paper-frame mx-auto my-12 max-w-3xl p-8 rounded-lg"
style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border));"
>
<header class="mb-6">
<div
class="earthen-byline flex flex-wrap gap-x-3 gap-y-1 uppercase tracking-wider text-xs mb-2"
style="color: hsl(var(--muted-foreground));"
>
if data.Author != "" {
<span class="earthen-byline-author">By { data.Author }</span>
}
if data.Location != "" {
<span class="earthen-byline-location">{ data.Location }</span>
}
if data.Dateline != "" {
<span class="earthen-byline-date">{ data.Dateline }</span>
}
</div>
<div class="botanical-rule" aria-hidden="true">
@botanicalGlyph("seed", 28)
</div>
</header>
if data.Image != "" {
<figure class="my-6 earthen-paper-frame-inner">
<img
src={ resolveMedia(data.Image) }
alt=""
class="w-full h-auto rounded"
/>
</figure>
}
if data.Body != "" {
<div
class="earthen-body drop-cap prose-base"
style={ "font-family: var(--font-body, \"Spectral\", Georgia, serif); color: hsl(var(--foreground));" }
>
@templ.Raw(data.Body)
</div>
} else {
<p class="earthen-empty" style="color: hsl(var(--muted-foreground));">
No dispatch yet — add a body to share what you saw in the field.
</p>
}
</article>
}

View File

@ -1,175 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// fieldNoteComponent renders an article-style dispatch from the field.
func fieldNoteComponent(data FieldNoteData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<article data-block=\"earthen:field_note\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Empty {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " data-empty")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " class=\"earthen-field-note earthen-paper-frame mx-auto my-12 max-w-3xl p-8 rounded-lg\" style=\"background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border));\"><header class=\"mb-6\"><div class=\"earthen-byline flex flex-wrap gap-x-3 gap-y-1 uppercase tracking-wider text-xs mb-2\" style=\"color: hsl(var(--muted-foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Author != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"earthen-byline-author\">By ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Author)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `field_note.templ`, Line: 17, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Location != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"earthen-byline-location\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Location)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `field_note.templ`, Line: 20, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Dateline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"earthen-byline-date\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Dateline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `field_note.templ`, Line: 23, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><div class=\"botanical-rule\" aria-hidden=\"true\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = botanicalGlyph("seed", 28).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></header>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Image != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<figure class=\"my-6 earthen-paper-frame-inner\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(resolveMedia(data.Image))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `field_note.templ`, Line: 33, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" alt=\"\" class=\"w-full h-auto rounded\"></figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Body != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"earthen-body drop-cap prose-base\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-body, \"Spectral\", Georgia, serif); color: hsl(var(--foreground));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `field_note.templ`, Line: 42, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<p class=\"earthen-empty\" style=\"color: hsl(var(--muted-foreground));\">No dispatch yet — add a body to share what you saw in the field.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1 +1,18 @@
[]
[
{
"name": "Fraunces",
"family": "Fraunces",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/fraunces-latin-400.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/fraunces-latin-600.woff2" }
]
},
{
"name": "Work Sans",
"family": "Work Sans",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/worksans-latin-400.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/worksans-latin-600.woff2" }
]
}
]

View File

@ -1,69 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// FooterBlockMeta defines metadata for the earthen footer block.
var FooterBlockMeta = blocks.BlockMeta{
Key: "footer",
Title: "Site Footer",
Description: "Cream-on-moss footer with optional newsletter and column links",
Source: "earthen",
Category: blocks.CategoryNavigation,
}
// FooterBlock renders the earthen footer block.
// Content shape: {showNewsletter, tagline, columns:[{title,links:[{text,url}]}]}
func FooterBlock(ctx context.Context, content map[string]any) string {
rawColumns := getSlice(content, "columns")
columns := make([]FooterColumn, 0, len(rawColumns))
for _, c := range rawColumns {
rawLinks := getSlice(c, "links")
links := make([]FooterLink, 0, len(rawLinks))
for _, l := range rawLinks {
links = append(links, FooterLink{
Text: getString(l, "text"),
URL: getString(l, "url"),
})
}
columns = append(columns, FooterColumn{
Title: getString(c, "title"),
Links: links,
})
}
data := FooterData{
ShowNewsletter: getBool(content, "showNewsletter", false),
Tagline: getString(content, "tagline"),
Columns: columns,
Empty: len(content) == 0,
}
var buf bytes.Buffer
_ = footerComponent(data).Render(ctx, &buf)
return buf.String()
}
// FooterData contains data for the footer component.
type FooterData struct {
ShowNewsletter bool
Tagline string
Columns []FooterColumn
Empty bool
}
// FooterColumn is a single link column in the footer.
type FooterColumn struct {
Title string
Links []FooterLink
}
// FooterLink is a single link in a footer column.
type FooterLink struct {
Text string
URL string
}

View File

@ -1,104 +0,0 @@
package main
// footerComponent renders the cream-on-moss footer with optional newsletter and link columns.
templ footerComponent(data FooterData) {
<div
data-block="earthen:footer"
data-empty?={ data.Empty }
class="earthen-footer w-full"
style="background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground));"
>
<div class="max-w-6xl mx-auto px-6 py-16">
<div class="botanical-rule flex items-center gap-4 mb-10" aria-hidden="true">
<span class="flex-1 h-px" style="background-color: hsl(var(--primary-foreground) / 0.3);"></span>
@botanicalGlyph("fern", 32)
<span class="flex-1 h-px" style="background-color: hsl(var(--primary-foreground) / 0.3);"></span>
</div>
if data.Tagline != "" {
<p
class="earthen-display text-2xl md:text-3xl text-center mb-10"
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif);" }
>
{ data.Tagline }
</p>
}
if data.ShowNewsletter {
<form class="max-w-xl mx-auto mb-12 flex flex-col sm:flex-row gap-3">
<label class="sr-only" for="earthen-footer-email">Email address</label>
<input
id="earthen-footer-email"
type="email"
placeholder="you@example.org"
class="flex-1 rounded-lg px-4 py-3 text-base"
style="background-color: hsl(var(--primary-foreground) / 0.1); color: hsl(var(--primary-foreground)); border: 1px solid hsl(var(--primary-foreground) / 0.3);"
/>
<button
type="submit"
class="rounded-lg px-6 py-3 font-semibold"
style="background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground));"
>
Subscribe
</button>
</form>
}
if len(data.Columns) > 0 {
<div class={ "grid gap-8 mb-10", footerGridCols(len(data.Columns)) }>
for _, col := range data.Columns {
<div>
if col.Title != "" {
<h4
class="earthen-display text-lg mb-4"
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif);" }
>
{ col.Title }
</h4>
}
if len(col.Links) > 0 {
<ul class="space-y-2">
for _, link := range col.Links {
<li>
<a
href={ templ.SafeURL(footerLinkURL(link.URL)) }
class="earthen-footer-link transition-colors"
style="color: hsl(var(--primary-foreground) / 0.85);"
>
{ link.Text }
</a>
</li>
}
</ul>
}
</div>
}
</div>
}
<div
class="text-center text-sm pt-6"
style="border-top: 1px solid hsl(var(--primary-foreground) / 0.2); color: hsl(var(--primary-foreground) / 0.7);"
>
<span>Rooted in place. Made with care.</span>
</div>
</div>
</div>
}
// footerGridCols returns a responsive grid class for the footer column count.
func footerGridCols(count int) string {
switch count {
case 1:
return "grid-cols-1"
case 2:
return "grid-cols-1 sm:grid-cols-2"
case 3:
return "grid-cols-1 sm:grid-cols-3"
default:
return "grid-cols-2 lg:grid-cols-4"
}
}
func footerLinkURL(url string) string {
if url == "" {
return "#"
}
return url
}

View File

@ -1,234 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// footerComponent renders the cream-on-moss footer with optional newsletter and link columns.
func footerComponent(data FooterData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div data-block=\"earthen:footer\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Empty {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " data-empty")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " class=\"earthen-footer w-full\" style=\"background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground));\"><div class=\"max-w-6xl mx-auto px-6 py-16\"><div class=\"botanical-rule flex items-center gap-4 mb-10\" aria-hidden=\"true\"><span class=\"flex-1 h-px\" style=\"background-color: hsl(var(--primary-foreground) / 0.3);\"></span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = botanicalGlyph("fern", 32).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"flex-1 h-px\" style=\"background-color: hsl(var(--primary-foreground) / 0.3);\"></span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Tagline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"earthen-display text-2xl md:text-3xl text-center mb-10\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif);")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 20, Col: 100}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Tagline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 22, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ShowNewsletter {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<form class=\"max-w-xl mx-auto mb-12 flex flex-col sm:flex-row gap-3\"><label class=\"sr-only\" for=\"earthen-footer-email\">Email address</label> <input id=\"earthen-footer-email\" type=\"email\" placeholder=\"you@example.org\" class=\"flex-1 rounded-lg px-4 py-3 text-base\" style=\"background-color: hsl(var(--primary-foreground) / 0.1); color: hsl(var(--primary-foreground)); border: 1px solid hsl(var(--primary-foreground) / 0.3);\"> <button type=\"submit\" class=\"rounded-lg px-6 py-3 font-semibold\" style=\"background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground));\">Subscribe</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Columns) > 0 {
var templ_7745c5c3_Var4 = []any{"grid gap-8 mb-10", footerGridCols(len(data.Columns))}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var4).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, col := range data.Columns {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if col.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<h4 class=\"earthen-display text-lg mb-4\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif);")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 51, Col: 104}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(col.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 53, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(col.Links) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<ul class=\"space-y-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, link := range col.Links {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(footerLinkURL(link.URL)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 61, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"earthen-footer-link transition-colors\" style=\"color: hsl(var(--primary-foreground) / 0.85);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(link.Text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `footer.templ`, Line: 65, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"text-center text-sm pt-6\" style=\"border-top: 1px solid hsl(var(--primary-foreground) / 0.2); color: hsl(var(--primary-foreground) / 0.7);\"><span>Rooted in place. Made with care.</span></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// footerGridCols returns a responsive grid class for the footer column count.
func footerGridCols(count int) string {
switch count {
case 1:
return "grid-cols-1"
case 2:
return "grid-cols-1 sm:grid-cols-2"
case 3:
return "grid-cols-1 sm:grid-cols-3"
default:
return "grid-cols-2 lg:grid-cols-4"
}
}
func footerLinkURL(url string) string {
if url == "" {
return "#"
}
return url
}
var _ = templruntime.GeneratedTemplate

20
go.mod
View File

@ -1,20 +0,0 @@
module git.dev.alexdunmow.com/block/themes/earthen
go 1.26.4
require (
git.dev.alexdunmow.com/block/core v0.14.1
github.com/a-h/templ v0.3.1020
)
require (
connectrpc.com/connect v1.20.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.2 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

44
go.sum
View File

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

View File

@ -1,40 +0,0 @@
package main
import (
"bytes"
"context"
"strconv"
)
// EarthenHeadingBlock renders a heading with Earthen styling.
// Content expects: {"text": "Heading text", "level": 1-6, "textClass": "optional"}
func EarthenHeadingBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
textClass := getString(content, "textClass")
level := parseHeadingLevel(content)
var buf bytes.Buffer
_ = earthenHeadingComponent(level, text, textClass).Render(ctx, &buf)
return buf.String()
}
// parseHeadingLevel parses the level from content, defaulting to 2.
func parseHeadingLevel(content map[string]any) int {
if level, ok := content["level"].(float64); ok {
l := int(level)
if l >= 1 && l <= 6 {
return l
}
}
if level, ok := content["level"].(int); ok {
if level >= 1 && level <= 6 {
return level
}
}
if level, ok := content["level"].(string); ok {
if l, err := strconv.Atoi(level); err == nil && l >= 1 && l <= 6 {
return l
}
}
return 2
}

View File

@ -1,76 +0,0 @@
package main
// earthenHeadingBaseClass returns base classes for each heading level.
func earthenHeadingBaseClass(level int) string {
switch level {
case 1:
return "earthen-h1 text-5xl md:text-6xl font-semibold tracking-tight leading-tight"
case 2:
return "earthen-h2 text-3xl md:text-4xl font-semibold leading-snug crayon-underline"
case 3:
return "earthen-h3 text-2xl md:text-3xl font-semibold leading-snug"
case 4:
return "earthen-h4 text-xl md:text-2xl font-semibold"
case 5:
return "earthen-h5 text-lg font-semibold"
case 6:
return "earthen-h6 text-base font-semibold uppercase tracking-wider"
default:
return "earthen-h2 text-3xl md:text-4xl font-semibold"
}
}
// earthenHeadingComponent renders a heading with Earthen styling.
templ earthenHeadingComponent(level int, text, textClass string) {
switch level {
case 1:
<h1
class={ earthenHeadingBaseClass(1), textClass }
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));" }
>
{ text }
</h1>
case 2:
<h2
class={ earthenHeadingBaseClass(2), textClass }
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));" }
>
{ text }
</h2>
case 3:
<h3
class={ earthenHeadingBaseClass(3), textClass }
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--foreground));" }
>
{ text }
</h3>
case 4:
<h4
class={ earthenHeadingBaseClass(4), textClass }
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--foreground));" }
>
{ text }
</h4>
case 5:
<h5
class={ earthenHeadingBaseClass(5), textClass }
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--foreground));" }
>
{ text }
</h5>
case 6:
<h6
class={ earthenHeadingBaseClass(6), textClass }
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--muted-foreground));" }
>
{ text }
</h6>
default:
<h2
class={ earthenHeadingBaseClass(2), textClass }
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));" }
>
{ text }
</h2>
}
}

View File

@ -1,402 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// earthenHeadingBaseClass returns base classes for each heading level.
func earthenHeadingBaseClass(level int) string {
switch level {
case 1:
return "earthen-h1 text-5xl md:text-6xl font-semibold tracking-tight leading-tight"
case 2:
return "earthen-h2 text-3xl md:text-4xl font-semibold leading-snug crayon-underline"
case 3:
return "earthen-h3 text-2xl md:text-3xl font-semibold leading-snug"
case 4:
return "earthen-h4 text-xl md:text-2xl font-semibold"
case 5:
return "earthen-h5 text-lg font-semibold"
case 6:
return "earthen-h6 text-base font-semibold uppercase tracking-wider"
default:
return "earthen-h2 text-3xl md:text-4xl font-semibold"
}
}
// earthenHeadingComponent renders a heading with Earthen styling.
func earthenHeadingComponent(level int, text, textClass string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch level {
case 1:
var templ_7745c5c3_Var2 = []any{earthenHeadingBaseClass(1), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 29, Col: 127}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 31, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 2:
var templ_7745c5c3_Var6 = []any{earthenHeadingBaseClass(2), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var6).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 36, Col: 127}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 38, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 3:
var templ_7745c5c3_Var10 = []any{earthenHeadingBaseClass(3), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<h3 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var10).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--foreground));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 43, Col: 130}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 45, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 4:
var templ_7745c5c3_Var14 = []any{earthenHeadingBaseClass(4), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h4 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--foreground));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 50, Col: 130}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 52, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 5:
var templ_7745c5c3_Var18 = []any{earthenHeadingBaseClass(5), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<h5 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var18).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--foreground));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 57, Col: 130}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 59, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</h5>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 6:
var templ_7745c5c3_Var22 = []any{earthenHeadingBaseClass(6), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<h6 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var22).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--muted-foreground));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 64, Col: 136}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 66, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</h6>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
var templ_7745c5c3_Var26 = []any{earthenHeadingBaseClass(2), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var26...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var26).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 71, Col: 127}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 73, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,48 +0,0 @@
package main
// getString extracts a string value from content map.
func getString(content map[string]any, key string) string {
if v, ok := content[key].(string); ok {
return v
}
return ""
}
// getBool extracts a bool value from content map. Defaults to defaultVal.
func getBool(content map[string]any, key string, defaultVal bool) bool {
if v, ok := content[key].(bool); ok {
return v
}
return defaultVal
}
// getSlice extracts a slice of maps from content.
func getSlice(content map[string]any, key string) []map[string]any {
if v, ok := content[key].([]any); ok {
result := make([]map[string]any, 0, len(v))
for _, item := range v {
if m, ok := item.(map[string]any); ok {
result = append(result, m)
}
}
return result
}
return nil
}
// getNumberSlice extracts a slice of numbers from content. Handles JSON float64.
func getNumberSlice(content map[string]any, key string) []float64 {
if v, ok := content[key].([]any); ok {
result := make([]float64, 0, len(v))
for _, item := range v {
switch n := item.(type) {
case float64:
result = append(result, n)
case int:
result = append(result, float64(n))
}
}
return result
}
return nil
}

View File

@ -1,21 +0,0 @@
package main
import (
"bytes"
"context"
)
// EarthenImageBlock renders an image with paper-grain frame and optional caption.
// Content expects: {"src":"…","alt":"…","caption":"…"}
func EarthenImageBlock(ctx context.Context, content map[string]any) string {
src := getString(content, "src")
if src == "" {
src = getString(content, "url")
}
alt := getString(content, "alt")
caption := getString(content, "caption")
var buf bytes.Buffer
_ = earthenImageComponent(src, alt, caption).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,35 +0,0 @@
package main
// earthenImageComponent renders an image inside a paper-grain frame, with an optional handwritten caption.
templ earthenImageComponent(src, alt, caption string) {
<figure class="earthen-image my-8 mx-auto" style="max-width: 100%;">
if src != "" {
<div
class="earthen-paper-frame-inner rounded-lg overflow-hidden"
style="border: 1px solid hsl(var(--border)); padding: 6px; background-color: hsl(var(--card));"
>
<img
src={ resolveMedia(src) }
alt={ alt }
class="w-full h-auto rounded-md"
/>
</div>
} else {
<div
data-empty="true"
class="earthen-image-empty w-full h-48 rounded-lg flex items-center justify-center"
style="background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground)); border: 1.5px dashed hsl(var(--border));"
>
<span class="text-sm">Choose an image to display here</span>
</div>
}
if caption != "" {
<figcaption
class="earthen-caption text-center text-sm mt-3"
style="color: hsl(var(--muted-foreground)); font-style: italic;"
>
{ caption }
</figcaption>
}
</figure>
}

View File

@ -1,101 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// earthenImageComponent renders an image inside a paper-grain frame, with an optional handwritten caption.
func earthenImageComponent(src, alt, caption string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<figure class=\"earthen-image my-8 mx-auto\" style=\"max-width: 100%;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if src != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"earthen-paper-frame-inner rounded-lg overflow-hidden\" style=\"border: 1px solid hsl(var(--border)); padding: 6px; background-color: hsl(var(--card));\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(resolveMedia(src))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 12, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(alt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 13, Col: 14}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"w-full h-auto rounded-md\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div data-empty=\"true\" class=\"earthen-image-empty w-full h-48 rounded-lg flex items-center justify-center\" style=\"background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground)); border: 1.5px dashed hsl(var(--border));\"><span class=\"text-sm\">Choose an image to display here</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<figcaption class=\"earthen-caption text-center text-sm mt-3\" style=\"color: hsl(var(--muted-foreground)); font-style: italic;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(caption)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 31, Col: 13}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,58 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// ImpactMetricsBlockMeta defines metadata for the impact metrics block.
var ImpactMetricsBlockMeta = blocks.BlockMeta{
Key: "impact_metrics",
Title: "Impact Metrics",
Description: "Numerical impact metrics with optional botanical illustration",
Source: "earthen",
Category: blocks.CategoryContent,
}
// ImpactMetricsBlock renders a row of impact metrics.
// Content shape: {title, metrics:[{value,label,suffix}], illustration}
func ImpactMetricsBlock(ctx context.Context, content map[string]any) string {
rawMetrics := getSlice(content, "metrics")
items := make([]ImpactMetric, 0, len(rawMetrics))
for _, m := range rawMetrics {
items = append(items, ImpactMetric{
Value: getString(m, "value"),
Label: getString(m, "label"),
Suffix: getString(m, "suffix"),
})
}
data := ImpactMetricsData{
Title: getString(content, "title"),
Illustration: getString(content, "illustration"),
Metrics: items,
Empty: len(content) == 0,
}
var buf bytes.Buffer
_ = impactMetricsComponent(data).Render(ctx, &buf)
return buf.String()
}
// ImpactMetricsData contains data for the impact metrics component.
type ImpactMetricsData struct {
Title string
Illustration string
Metrics []ImpactMetric
Empty bool
}
// ImpactMetric is one numerical metric tile.
type ImpactMetric struct {
Value string
Label string
Suffix string
}

View File

@ -1,82 +0,0 @@
package main
// impactMetricsComponent renders a row of impact metrics with optional botanical illustration.
templ impactMetricsComponent(data ImpactMetricsData) {
<section
data-block="earthen:impact_metrics"
data-empty?={ data.Empty }
class="earthen-impact-metrics py-16 px-4"
style="background-color: hsl(var(--background)); color: hsl(var(--foreground));"
>
<div class="max-w-5xl mx-auto">
if data.Title != "" {
<h2
class="earthen-display text-3xl md:text-4xl text-center mb-12"
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));" }
>
{ data.Title }
</h2>
}
if data.Illustration != "" {
<div class="flex justify-center mb-8">
<img src={ resolveMedia(data.Illustration) } alt="" class="max-h-40 object-contain"/>
</div>
} else if data.Title != "" || len(data.Metrics) > 0 {
<div class="flex justify-center mb-8">
@botanicalGlyph("fern", 64)
</div>
}
if len(data.Metrics) > 0 {
<div class={ "grid gap-8", metricGridCols(len(data.Metrics)) }>
for _, m := range data.Metrics {
<div class="text-center px-4">
<div
class="earthen-display text-5xl font-semibold mb-2"
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--accent));" }
>
{ m.Value }{ m.Suffix }
</div>
<div
class="earthen-body uppercase tracking-wider text-sm"
style={ "font-family: var(--font-body, \"Spectral\", Georgia, serif); color: hsl(var(--muted-foreground));" }
>
{ m.Label }
</div>
</div>
}
</div>
} else {
<p
class="text-center"
style="color: hsl(var(--muted-foreground));"
>
Add impact metrics to bring this section to life.
</p>
}
</div>
</section>
}
// metricGridCols returns the responsive grid class for metric count.
func metricGridCols(count int) string {
switch count {
case 1:
return "grid-cols-1"
case 2:
return "grid-cols-1 sm:grid-cols-2"
case 3:
return "grid-cols-1 sm:grid-cols-3"
default:
return "grid-cols-2 lg:grid-cols-4"
}
}
// resolveMedia rewrites a media reference to its served URL. The wave-1
// implementation passes the path through verbatim and is wired so admins
// can drop in their own media adapter later without touching templates.
func resolveMedia(path string) string {
if path == "" {
return ""
}
return path
}

View File

@ -1,243 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// impactMetricsComponent renders a row of impact metrics with optional botanical illustration.
func impactMetricsComponent(data ImpactMetricsData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"earthen:impact_metrics\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Empty {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " data-empty")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " class=\"earthen-impact-metrics py-16 px-4\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\"><div class=\"max-w-5xl mx-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h2 class=\"earthen-display text-3xl md:text-4xl text-center mb-12\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `impact_metrics.templ`, Line: 15, Col: 128}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `impact_metrics.templ`, Line: 17, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Illustration != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"flex justify-center mb-8\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(resolveMedia(data.Illustration))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `impact_metrics.templ`, Line: 22, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" alt=\"\" class=\"max-h-40 object-contain\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if data.Title != "" || len(data.Metrics) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"flex justify-center mb-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = botanicalGlyph("fern", 64).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Metrics) > 0 {
var templ_7745c5c3_Var5 = []any{"grid gap-8", metricGridCols(len(data.Metrics))}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var5).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `impact_metrics.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, m := range data.Metrics {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"text-center px-4\"><div class=\"earthen-display text-5xl font-semibold mb-2\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--accent));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `impact_metrics.templ`, Line: 35, Col: 130}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(m.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `impact_metrics.templ`, Line: 37, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(m.Suffix)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `impact_metrics.templ`, Line: 37, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div><div class=\"earthen-body uppercase tracking-wider text-sm\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-body, \"Spectral\", Georgia, serif); color: hsl(var(--muted-foreground));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `impact_metrics.templ`, Line: 41, Col: 115}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `impact_metrics.templ`, Line: 43, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<p class=\"text-center\" style=\"color: hsl(var(--muted-foreground));\">Add impact metrics to bring this section to life.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// metricGridCols returns the responsive grid class for metric count.
func metricGridCols(count int) string {
switch count {
case 1:
return "grid-cols-1"
case 2:
return "grid-cols-1 sm:grid-cols-2"
case 3:
return "grid-cols-1 sm:grid-cols-3"
default:
return "grid-cols-2 lg:grid-cols-4"
}
}
// resolveMedia rewrites a media reference to its served URL. The wave-1
// implementation passes the path through verbatim and is wired so admins
// can drop in their own media adapter later without touching templates.
func resolveMedia(path string) string {
if path == "" {
return ""
}
return path
}
var _ = templruntime.GeneratedTemplate

237
manifest.yaml Normal file
View File

@ -0,0 +1,237 @@
# Earthen theme — codeless .bnp manifest (WO-WZ-021 Phase B1).
# 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]
system_templates:
- key: earthen
title: Earthen
description: "Botanical nonprofit theme in moss, clay, sage and cream"
page_templates:
- system: earthen
key: default
title: Default
description: "Standard nonprofit page with header, main and footer"
slots: [header, main, footer]
- system: earthen
key: landing
title: Landing
description: "Mission hero plus CTA bands for campaigns and donation drives"
slots: [hero, main, cta, footer]
- system: earthen
key: article
title: Article
description: "Long-form storytelling with byline rail"
slots: [header, lede, main, aside, footer]
- system: earthen
key: full-width
title: Full Width
description: "Edge-to-edge imagery for reports and galleries"
slots: [header, main, footer]
- system: earthen
key: blog-index
title: Field Journal
description: "Blog index with a botanical masthead, featured and post-grid slots"
slots: [header, featured, main, footer]
- system: earthen
key: contact
title: Get in Touch
description: "Contact layout with an optional aside for hours and location"
slots: [header, main, aside, footer]
- system: earthen
key: auth
title: Members
description: "Centred auth shell with wordmark and a fern crest"
slots: [main, footer]
template_overrides:
# Chrome (4)
- { template: earthen, block: navbar }
- { template: earthen, block: footer }
- { template: earthen, block: utility-bar }
- { template: earthen, block: announcement-bar }
# Marketing (12)
- { template: earthen, block: hero }
- { template: earthen, block: feature-grid }
- { template: earthen, block: rich-section }
- { template: earthen, block: stats }
- { template: earthen, block: testimonial }
- { template: earthen, block: logo-strip }
- { template: earthen, block: pricing }
- { template: earthen, block: team }
- { template: earthen, block: faq }
- { template: earthen, block: timeline }
- { template: earthen, block: cta }
- { template: earthen, block: countdown }
# Primitives (11)
- { template: earthen, block: heading }
- { template: earthen, block: text }
- { template: earthen, block: rich-text }
- { template: earthen, block: image }
- { template: earthen, block: quote }
- { template: earthen, block: divider }
- { template: earthen, block: button }
- { template: earthen, block: card }
- { template: earthen, block: gallery }
- { template: earthen, block: video-embed }
- { template: earthen, block: audio-embed }
# Commerce / local (6)
- { template: earthen, block: menu-list }
- { template: earthen, block: event-list }
- { template: earthen, block: contact-form }
- { template: earthen, block: contact-card }
- { template: earthen, block: hours-location }
- { template: earthen, block: social-links }
# Blog family (12)
- { template: earthen, block: blog-hero }
- { template: earthen, block: featured-posts }
- { template: earthen, block: related-posts }
- { template: earthen, block: popular-posts }
- { template: earthen, block: category-list }
- { template: earthen, block: post-hero }
- { template: earthen, block: post-metadata }
- { template: earthen, block: author-bio }
- { template: earthen, block: author-bio-hero }
- { template: earthen, block: newsletter-signup }
- { template: earthen, block: breadcrumbs }
- { template: earthen, block: sidebar-nav }
# Auth surface (3)
- { template: earthen, block: auth-form }
- { template: earthen, block: password-reset-form }
- { template: earthen, block: auth-status }
# System (1)
- { template: earthen, block: page-suggestions }
email_wrappers:
- earthen
css:
input_css_append: |
/* Earthen — theme utilities injected via CSSManifest.InputCSSAppend. */
/* All colour values reference the 19-token HSL custom properties so they */
/* shift with the active preset (mossbed, clay-field, wet-loam). */
/* Bundled faces. The host does not yet emit @font-face for fonts.json
* (LoadedPlugin.BundledFonts is inert), so the theme declares them itself
* against the served /templates/earthen/fonts/... paths. Presets set
* typography.fontHeading/body to template:earthen:<Family> so
* --font-heading/--font-body resolve to these families. Families/weights
* match fonts.json exactly. */
@font-face { font-family: 'Fraunces'; src: url('/templates/earthen/fonts/fraunces-latin-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Fraunces'; src: url('/templates/earthen/fonts/fraunces-latin-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: 'Work Sans'; src: url('/templates/earthen/fonts/worksans-latin-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Work Sans'; src: url('/templates/earthen/fonts/worksans-latin-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
/* Paper grain backdrop — a single inline SVG noise pattern keeps page weight
inside the marketplace gate (≤ 81920 bytes). */
.bg-paper-grain {
background-color: hsl(var(--background));
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27160%27%20height%3D%27160%27%20viewBox%3D%270%200%20160%20160%27%3E%3Cfilter%20id%3D%27n%27%3E%3CfeTurbulence%20type%3D%27fractalNoise%27%20baseFrequency%3D%270.9%27%20numOctaves%3D%272%27%20seed%3D%274%27%2F%3E%3CfeColorMatrix%20values%3D%270%200%200%200%200.36%200%200%200%200%200.32%200%200%200%200%200.24%200%200%200%200.05%200%27%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D%27160%27%20height%3D%27160%27%20filter%3D%27url%28%23n%29%27%2F%3E%3C%2Fsvg%3E");
background-repeat: repeat;
background-size: 240px 240px;
}
/* Botanical hairline divider — used inside .botanical-rule sections. */
.botanical-rule > span {
display: inline-block;
}
/* Hand-drawn crayon underline on h2 — uses currentColor and primary token. */
.crayon-underline {
position: relative;
padding-bottom: 0.35em;
}
.crayon-underline::after {
content: "";
position: absolute;
left: 0;
right: auto;
bottom: 0;
height: 6px;
width: 2.5em;
background-color: hsl(var(--accent) / 0.4);
border-radius: 4px 8px 6px 12px;
transform: skewY(-1deg);
}
/* Botanical drop cap — applied to the first paragraph of .drop-cap-host. */
.drop-cap-host > p:first-of-type::first-letter,
.drop-cap > p:first-of-type::first-letter,
.drop-cap::first-letter {
font-family: var(--font-heading, "Fraunces", "Playfair Display", Georgia, serif);
color: hsl(var(--primary));
float: left;
font-size: 3.4em;
line-height: 0.85;
margin: 0.15em 0.18em 0 0;
font-weight: 600;
}
/* Tile selection (donation CTA) — uses primary ring when checked. */
.earthen-tile input[type="radio"]:checked + span {
color: hsl(var(--primary));
}
.earthen-tile:has(input[type="radio"]:checked) {
box-shadow: 0 0 0 2px hsl(var(--primary));
}
/* Partner logos — grayscale at rest, full colour on hover. */
.earthen-partner-img,
.earthen-partner-fallback {
filter: grayscale(1);
opacity: 0.85;
transition: filter 200ms ease, opacity 200ms ease;
}
.earthen-partner-tile:hover .earthen-partner-img,
.earthen-partner-link:hover .earthen-partner-img,
.earthen-partner-link:focus-visible .earthen-partner-img,
.earthen-partner-tile:hover .earthen-partner-fallback,
.earthen-partner-link:hover .earthen-partner-fallback {
filter: grayscale(0);
opacity: 1;
}
/* Paper-grain frame around field-note imagery. */
.earthen-paper-frame {
box-shadow: inset 0 0 0 1px hsl(var(--border) / 0.6);
}
.earthen-paper-frame-inner {
box-shadow: 0 1px 2px hsl(var(--foreground) / 0.06);
}
/* Footer links — botanical underline on hover. */
.earthen-footer-link {
text-decoration: none;
border-bottom: 1px solid transparent;
transition: color 150ms ease, border-color 150ms ease;
}
.earthen-footer-link:hover,
.earthen-footer-link:focus-visible {
color: hsl(var(--primary-foreground));
border-bottom-color: hsl(var(--accent));
}
/* Buttons — focus ring follows the active preset. */
.earthen-button:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* Article aside — keeps the byline rail comfortable on mobile. */
.earthen-aside {
color: hsl(var(--muted-foreground));
}
/* Ensure article main column uses the Spectral body fallback. */
.earthen-article {
font-family: var(--font-body, "Spectral", Georgia, serif);
color: hsl(var(--foreground));
}
/* Divider glyph — softer stroke at rest. */
.earthen-glyph {
color: hsl(var(--primary));
}

32
master_pages.json Normal file
View File

@ -0,0 +1,32 @@
[
{
"key": "earthen:default-master",
"title": "Earthen Default Master",
"page_templates": ["default", "article", "contact", "full-width", "blog-index"],
"blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showNewsletter": true, "newsletterTitle": "Field notes in your inbox", "companyName": "Rootline Trust", "tagline": "Rooted in place. We restore land with the people who live on it.", "menuName": "main" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "earthen:landing-master",
"title": "Earthen Landing Master",
"page_templates": ["landing"],
"blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main" }, "slot": "hero", "sort_order": 0 },
{ "block_key": "slot", "title": "Hero Slot", "content": { "slotName": "hero" }, "slot": "hero", "sort_order": 10 },
{ "block_key": "slot", "title": "Main Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "earthen:donation_cta", "title": "Donation Appeal", "content": { "headline": "Plant the next acre.", "amounts": [25, 50, 100, 250] }, "slot": "cta", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showNewsletter": true, "newsletterTitle": "Field notes in your inbox", "companyName": "Rootline Trust", "tagline": "Rooted in place. We restore land with the people who live on it.", "menuName": "main" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "earthen:auth-master",
"title": "Earthen Auth Master",
"page_templates": ["auth"],
"blocks": [
{ "block_key": "slot", "title": "Auth Form Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }
]
}
]

View File

@ -1,55 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// PartnerLogosBlockMeta defines metadata for the partner logos block.
var PartnerLogosBlockMeta = blocks.BlockMeta{
Key: "partner_logos",
Title: "Partner Logos",
Description: "Grayscale partner logo grid with hover-to-color treatment",
Source: "earthen",
Category: blocks.CategoryContent,
}
// PartnerLogosBlock renders the partner logos block.
// Content shape: {title, logos:[{name,image,url}]}
func PartnerLogosBlock(ctx context.Context, content map[string]any) string {
rawLogos := getSlice(content, "logos")
logos := make([]PartnerLogo, 0, len(rawLogos))
for _, l := range rawLogos {
logos = append(logos, PartnerLogo{
Name: getString(l, "name"),
Image: getString(l, "image"),
URL: getString(l, "url"),
})
}
data := PartnerLogosData{
Title: getString(content, "title"),
Logos: logos,
Empty: len(content) == 0,
}
var buf bytes.Buffer
_ = partnerLogosComponent(data).Render(ctx, &buf)
return buf.String()
}
// PartnerLogosData contains data for the partner logos component.
type PartnerLogosData struct {
Title string
Logos []PartnerLogo
Empty bool
}
// PartnerLogo is a single partner row.
type PartnerLogo struct {
Name string
Image string
URL string
}

View File

@ -1,95 +0,0 @@
package main
// partnerLogosComponent renders a grayscale logo grid that restores colour on hover.
templ partnerLogosComponent(data PartnerLogosData) {
<section
data-block="earthen:partner_logos"
data-empty?={ data.Empty }
class="earthen-partner-logos py-16 px-4"
style="background-color: hsl(var(--background)); color: hsl(var(--foreground));"
>
<div class="max-w-5xl mx-auto">
if data.Title != "" {
<h2
class="earthen-display text-2xl md:text-3xl text-center mb-10"
style={ "font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));" }
>
{ data.Title }
</h2>
}
if len(data.Logos) > 0 {
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-8 items-center">
for _, logo := range data.Logos {
<div class="earthen-partner-tile flex justify-center items-center">
@partnerLogoTile(logo)
</div>
}
</div>
} else {
<p class="text-center" style="color: hsl(var(--muted-foreground));">
Add partner logos to fill this grid.
</p>
}
</div>
</section>
}
templ partnerLogoTile(logo PartnerLogo) {
if logo.URL != "" {
<a
href={ templ.SafeURL(logo.URL) }
class="earthen-partner-link block transition-colors"
title={ logo.Name }
aria-label={ logo.Name }
>
@partnerLogoImage(logo)
</a>
} else {
<div class="earthen-partner-link block" title={ logo.Name } aria-label={ logo.Name }>
@partnerLogoImage(logo)
</div>
}
}
templ partnerLogoImage(logo PartnerLogo) {
if logo.Image != "" {
<img
src={ resolveMedia(logo.Image) }
alt={ logo.Name }
class="earthen-partner-img max-h-12 object-contain w-full"
/>
} else {
<div
class="earthen-partner-fallback w-full h-12 flex items-center justify-center rounded"
style="background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground));"
>
<span class="text-sm">{ partnerInitials(logo.Name) }</span>
</div>
}
}
// partnerInitials returns up to two initials for an initials-only fallback tile.
func partnerInitials(name string) string {
if name == "" {
return "?"
}
letters := []rune{}
prevSpace := true
for _, r := range name {
if r == ' ' || r == '-' || r == '_' {
prevSpace = true
continue
}
if prevSpace {
letters = append(letters, r)
prevSpace = false
if len(letters) >= 2 {
break
}
}
}
if len(letters) == 0 {
return "?"
}
return string(letters)
}

View File

@ -1,334 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// partnerLogosComponent renders a grayscale logo grid that restores colour on hover.
func partnerLogosComponent(data PartnerLogosData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"earthen:partner_logos\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Empty {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " data-empty")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " class=\"earthen-partner-logos py-16 px-4\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\"><div class=\"max-w-5xl mx-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h2 class=\"earthen-display text-2xl md:text-3xl text-center mb-10\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("font-family: var(--font-heading, \"Fraunces\", \"Playfair Display\", Georgia, serif); color: hsl(var(--primary));")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `partner_logos.templ`, Line: 15, Col: 128}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `partner_logos.templ`, Line: 17, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Logos) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-8 items-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, logo := range data.Logos {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"earthen-partner-tile flex justify-center items-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = partnerLogoTile(logo).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"text-center\" style=\"color: hsl(var(--muted-foreground));\">Add partner logos to fill this grid.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func partnerLogoTile(logo PartnerLogo) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if logo.URL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(logo.URL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `partner_logos.templ`, Line: 40, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"earthen-partner-link block transition-colors\" title=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(logo.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `partner_logos.templ`, Line: 42, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" aria-label=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(logo.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `partner_logos.templ`, Line: 43, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = partnerLogoImage(logo).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"earthen-partner-link block\" title=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(logo.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `partner_logos.templ`, Line: 48, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" aria-label=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(logo.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `partner_logos.templ`, Line: 48, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = partnerLogoImage(logo).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func partnerLogoImage(logo PartnerLogo) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if logo.Image != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(resolveMedia(logo.Image))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `partner_logos.templ`, Line: 57, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(logo.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `partner_logos.templ`, Line: 58, Col: 18}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" class=\"earthen-partner-img max-h-12 object-contain w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"earthen-partner-fallback w-full h-12 flex items-center justify-center rounded\" style=\"background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground));\"><span class=\"text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(partnerInitials(logo.Name))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `partner_logos.templ`, Line: 66, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
// partnerInitials returns up to two initials for an initials-only fallback tile.
func partnerInitials(name string) string {
if name == "" {
return "?"
}
letters := []rune{}
prevSpace := true
for _, r := range name {
if r == ' ' || r == '-' || r == '_' {
prevSpace = true
continue
}
if prevSpace {
letters = append(letters, r)
prevSpace = false
if len(letters) >= 2 {
break
}
}
}
if len(letters) == 0 {
return "?"
}
return string(letters)
}
var _ = templruntime.GeneratedTemplate

View File

@ -2,11 +2,8 @@
name = "earthen"
display_name = "Earthen"
scope = "@themes"
version = "0.1.1"
version = "0.5.0"
description = "Botanical nonprofit theme in moss, clay, sage and cream — hand-illustrated motifs for cause-driven sites."
kind = "theme"
categories = ["templates"]
tags = ["earth", "nonprofit", "sustainability", "outdoor", "farm", "cause", "botanical", "conservation", "ngo"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"

View File

@ -5,6 +5,10 @@
"description": "Cream paper, deep moss primary, terracotta accent — the canonical Earthen look.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:earthen:Fraunces",
"fontBody": "template:earthen:Work Sans"
},
"lightColors": {
"background": "40 30% 96%",
"foreground": "120 25% 14%",
@ -55,6 +59,10 @@
"description": "Warm terracotta-led palette tuned for farm-stand and outdoor-brand sites.",
"theme": {
"mode": "light",
"typography": {
"fontHeading": "template:earthen:Fraunces",
"fontBody": "template:earthen:Work Sans"
},
"lightColors": {
"background": "30 35% 95%",
"foreground": "20 35% 16%",
@ -84,6 +92,10 @@
"description": "Deep night-soil tones for conservation orgs that need quiet authority.",
"theme": {
"mode": "dark",
"typography": {
"fontHeading": "template:earthen:Fraunces",
"fontBody": "template:earthen:Work Sans"
},
"darkColors": {
"background": "90 20% 6%",
"foreground": "60 20% 90%",
@ -106,5 +118,113 @@
"ring": "80 35% 55%"
}
}
},
{
"id": "sage-morning",
"name": "Sage Morning",
"description": "Pale sage paper and honey light, misty forest dark — a softer, hopeful reading of the palette.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:earthen:Fraunces",
"fontBody": "template:earthen:Work Sans"
},
"lightColors": {
"background": "70 25% 95%",
"foreground": "130 20% 16%",
"card": "70 22% 98%",
"cardForeground": "130 20% 16%",
"popover": "70 22% 98%",
"popoverForeground": "130 20% 16%",
"primary": "140 25% 34%",
"primaryForeground": "70 25% 96%",
"secondary": "75 18% 87%",
"secondaryForeground": "130 22% 20%",
"muted": "70 16% 90%",
"mutedForeground": "110 10% 40%",
"accent": "38 62% 48%",
"accentForeground": "40 30% 97%",
"destructive": "8 70% 45%",
"destructiveForeground": "40 30% 96%",
"border": "90 12% 80%",
"input": "90 12% 80%",
"ring": "140 25% 34%"
},
"darkColors": {
"background": "140 15% 9%",
"foreground": "70 18% 90%",
"card": "140 15% 12%",
"cardForeground": "70 18% 90%",
"popover": "140 15% 12%",
"popoverForeground": "70 18% 90%",
"primary": "95 28% 58%",
"primaryForeground": "140 15% 9%",
"secondary": "140 12% 18%",
"secondaryForeground": "70 18% 90%",
"muted": "140 10% 15%",
"mutedForeground": "90 10% 58%",
"accent": "40 65% 56%",
"accentForeground": "140 15% 9%",
"destructive": "8 70% 50%",
"destructiveForeground": "70 18% 90%",
"border": "140 10% 22%",
"input": "140 10% 22%",
"ring": "95 28% 58%"
}
}
},
{
"id": "ember-loam",
"name": "Ember Loam",
"description": "Terracotta-led warm clay in light, firelight ember over deep loam in dark — moss becomes the accent.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:earthen:Fraunces",
"fontBody": "template:earthen:Work Sans"
},
"lightColors": {
"background": "28 38% 95%",
"foreground": "18 40% 15%",
"card": "28 32% 98%",
"cardForeground": "18 40% 15%",
"popover": "28 32% 98%",
"popoverForeground": "18 40% 15%",
"primary": "14 62% 44%",
"primaryForeground": "28 38% 96%",
"secondary": "32 26% 88%",
"secondaryForeground": "18 40% 16%",
"muted": "28 22% 91%",
"mutedForeground": "22 20% 38%",
"accent": "95 24% 34%",
"accentForeground": "28 38% 96%",
"destructive": "6 72% 44%",
"destructiveForeground": "28 38% 96%",
"border": "28 18% 81%",
"input": "28 18% 81%",
"ring": "14 62% 44%"
},
"darkColors": {
"background": "20 22% 7%",
"foreground": "35 25% 90%",
"card": "20 22% 10%",
"cardForeground": "35 25% 90%",
"popover": "20 22% 10%",
"popoverForeground": "35 25% 90%",
"primary": "22 72% 56%",
"primaryForeground": "20 22% 7%",
"secondary": "20 16% 17%",
"secondaryForeground": "35 25% 90%",
"muted": "20 14% 14%",
"mutedForeground": "30 14% 58%",
"accent": "90 30% 52%",
"accentForeground": "20 22% 7%",
"destructive": "6 72% 52%",
"destructiveForeground": "35 25% 90%",
"border": "20 14% 21%",
"input": "20 14% 21%",
"ring": "22 72% 56%"
}
}
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@ -1,180 +0,0 @@
package main
import (
"context"
"github.com/a-h/templ"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// wrap adapts a templ-returning render function to templates.TemplateFunc.
// templ.Component already implements templates.HTMLComponent via Render.
func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc {
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
return f(ctx, doc)
}
}
// Register is the plugin entry point that registers the Earthen system template,
// four page templates, six theme blocks, five overrides, and the email wrapper.
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
Key: "earthen",
Title: "Earthen",
Description: "Botanical nonprofit theme in moss, clay, sage and cream",
})
if err := tr.RegisterPageTemplate("earthen", templates.PageTemplateMeta{
Key: "default",
Title: "Default",
Description: "Standard nonprofit page with header, main and footer",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderEarthenDefault)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("earthen", templates.PageTemplateMeta{
Key: "landing",
Title: "Landing",
Description: "Mission hero plus CTA bands for campaigns and donation drives",
Slots: []string{"hero", "main", "cta", "footer"},
}, wrap(RenderEarthenLanding)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("earthen", templates.PageTemplateMeta{
Key: "article",
Title: "Article",
Description: "Long-form storytelling with byline rail",
Slots: []string{"header", "lede", "main", "aside", "footer"},
}, wrap(RenderEarthenArticle)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("earthen", templates.PageTemplateMeta{
Key: "full-width",
Title: "Full Width",
Description: "Edge-to-edge imagery for reports and galleries",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderEarthenFullWidth)); err != nil {
return err
}
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
return err
}
br.Register(DonationCTABlockMeta, DonationCTABlock)
br.Register(ImpactMetricsBlockMeta, ImpactMetricsBlock)
br.Register(PartnerLogosBlockMeta, PartnerLogosBlock)
br.Register(FieldNoteBlockMeta, FieldNoteBlock)
br.Register(BotanicalDividerBlockMeta, BotanicalDividerBlock)
br.Register(FooterBlockMeta, FooterBlock)
br.RegisterTemplateOverride("earthen", "heading", EarthenHeadingBlock)
br.RegisterTemplateOverride("earthen", "text", EarthenTextBlock)
br.RegisterTemplateOverride("earthen", "button", EarthenButtonBlock)
br.RegisterTemplateOverride("earthen", "image", EarthenImageBlock)
br.RegisterTemplateOverride("earthen", "card", EarthenCardBlock)
tr.RegisterEmailWrapper("earthen", EarthenEmailWrapper)
return nil
}
// DefaultMasterPages returns the default master pages that Earthen provisions
// on first plugin load.
func DefaultMasterPages() []plugin.MasterPageDefinition {
return []plugin.MasterPageDefinition{
{
Key: "earthen:default-master",
Title: "Earthen Default Master",
PageTemplates: []string{"default", "article"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "navbar",
Title: "Main Navigation",
Content: map[string]any{"menuName": "main"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Main Content",
Content: map[string]any{"slotName": "main"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "earthen:donation_cta",
Title: "Donation Rail",
Content: map[string]any{
"amounts": []any{25.0, 50.0, 100.0},
"headline": "Plant the next acre.",
},
Slot: "main",
SortOrder: 100,
},
{
BlockKey: "earthen:footer",
Title: "Site Footer",
Content: map[string]any{
"showNewsletter": true,
"tagline": "Rooted in place.",
},
Slot: "footer",
SortOrder: 0,
},
},
},
{
Key: "earthen:landing-master",
Title: "Earthen Landing Master",
PageTemplates: []string{"landing", "full-width"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "navbar",
Title: "Main Navigation",
Content: map[string]any{"menuName": "main"},
Slot: "hero",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Hero Slot",
Content: map[string]any{"slotName": "hero"},
Slot: "hero",
SortOrder: 10,
},
{
BlockKey: "slot",
Title: "Main Slot",
Content: map[string]any{"slotName": "main"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "earthen:donation_cta",
Title: "Donation CTA",
Content: map[string]any{
"amounts": []any{25.0, 50.0, 100.0, 250.0},
},
Slot: "cta",
SortOrder: 0,
},
{
BlockKey: "earthen:footer",
Title: "Footer",
Content: map[string]any{
"showNewsletter": true,
},
Slot: "footer",
SortOrder: 0,
},
},
},
}
}

View File

@ -1,25 +0,0 @@
package main
import (
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// Registration is the compile-time plugin registration for the Earthen theme.
var Registration = plugin.PluginRegistration{
Name: "earthen",
Version: plugin.ParseModVersion(pluginModBytes),
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
return Register(tr, br)
},
Assets: func() http.Handler { return AssetsHandler() },
Schemas: func() fs.FS { return Schemas() },
ThemePresets: func() []byte { return ThemePresets() },
BundledFonts: func() []byte { return BundledFonts() },
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
}

View File

@ -1,16 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Botanical Divider",
"description": "SVG section break with hand-illustrated botanical motif",
"type": "object",
"properties": {
"motif": {
"type": "string",
"title": "Motif",
"description": "Botanical illustration used for the divider",
"x-editor": "select",
"enum": ["fern", "root", "seed"],
"default": "fern"
}
}
}

View File

@ -1,57 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Site Footer",
"description": "Cream-on-moss footer with optional newsletter and botanical rule",
"type": "object",
"properties": {
"showNewsletter": {
"type": "boolean",
"title": "Show Newsletter",
"description": "Show the newsletter signup form",
"x-editor": "select",
"default": true
},
"tagline": {
"type": "string",
"title": "Tagline",
"description": "Tagline above the columns",
"x-editor": "text"
},
"columns": {
"type": "array",
"title": "Columns",
"description": "Footer link columns",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Column Title",
"x-editor": "text"
},
"links": {
"type": "array",
"title": "Links",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"title": "Text",
"x-editor": "text"
},
"url": {
"type": "string",
"title": "URL",
"x-editor": "link"
}
}
}
}
}
}
}
}
}

View File

@ -1,49 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Impact Metrics",
"description": "Numerical impact metrics with optional illustration",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Section Title",
"description": "Heading above the metric tiles",
"x-editor": "text"
},
"metrics": {
"type": "array",
"title": "Metrics",
"description": "List of impact metrics",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string",
"title": "Value",
"description": "Numeric value (e.g. '12,000')",
"x-editor": "text"
},
"label": {
"type": "string",
"title": "Label",
"description": "Description of the metric",
"x-editor": "text"
},
"suffix": {
"type": "string",
"title": "Suffix",
"description": "Optional suffix (e.g. '+', '%')",
"x-editor": "text"
}
}
}
},
"illustration": {
"type": "string",
"title": "Illustration",
"description": "Optional botanical illustration shown alongside metrics",
"x-editor": "media"
}
}
}

View File

@ -1,40 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Partner Logos",
"description": "Grayscale partner logo grid with hover-to-color treatment",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Section Title",
"description": "Heading above the logo grid",
"x-editor": "text"
},
"logos": {
"type": "array",
"title": "Logos",
"description": "Partner logos",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Partner Name",
"x-editor": "text"
},
"image": {
"type": "string",
"title": "Logo Image",
"x-editor": "media"
},
"url": {
"type": "string",
"title": "Partner URL",
"x-editor": "link"
}
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
seed/media/botanical.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
seed/media/forest.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

BIN
seed/media/hands-soil.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

BIN
seed/media/leaf-veins.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

BIN
seed/media/seedling.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

BIN
seed/media/terracotta.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

583
seed/seed.json Normal file
View File

@ -0,0 +1,583 @@
{
"demo": true,
"settings": {
"merge": {
"companyName": "Rootline Trust",
"tagline": "We restore land with the people who live on it."
}
},
"media": [
{
"id": "41830fd8-442a-4255-bc96-9fb12e428ef1",
"file": "media/hands-soil.jpg",
"alt": "Hands lowering a young seedling into dark soil",
"folder": "earthen"
},
{
"id": "152f8386-ecf1-4f0c-899a-2c947766644d",
"file": "media/seedling.jpg",
"alt": "Cupped hands holding a seedling above turned earth",
"folder": "earthen"
},
{
"id": "c71264ea-0217-4826-a796-baffab1cf041",
"file": "media/forest.jpg",
"alt": "Sunlight through moss-covered trees in regrown forest",
"folder": "earthen"
},
{
"id": "603fdac3-2fa5-4511-8357-aa65550d9a31",
"file": "media/leaf-veins.jpg",
"alt": "Rain-washed leaf showing its veins in deep green",
"folder": "earthen"
},
{
"id": "02a30527-5464-45e2-a1f9-529fd9f46787",
"file": "media/terracotta.jpg",
"alt": "Terracotta pots of native tube stock ready for planting",
"folder": "earthen"
},
{
"id": "a8c164d1-5f0c-496d-bb77-d1e8ba769527",
"file": "media/botanical.jpg",
"alt": "Fern fronds unfurling on the forest floor",
"folder": "earthen"
}
],
"data_tables": [
{
"key": "contact_submissions",
"name": "Contact Enquiries",
"description": "Contact-form submissions for Rootline Trust. A submission becomes a row here and emails the site admins.",
"columns": [
{
"key": "name",
"label": "Name",
"type": "text",
"required": true
},
{
"key": "email",
"label": "Email",
"type": "email",
"required": true
},
{
"key": "region",
"label": "Region",
"type": "text",
"required": false
},
{
"key": "message",
"label": "Message",
"type": "text",
"required": true
}
]
}
],
"pages": [
{
"slug": "/",
"title": "Rootline Trust",
"template_key": "landing",
"blocks": [
{
"block_key": "hero",
"title": "Hero",
"slot": "hero",
"sort_order": 1,
"content": {
"template": "split",
"eyebrow": "Land restoration",
"headline": "Bring the land back to life.",
"subheadline": "We restore land with the people who live on it.",
"description": "Rootline Trust plants native cover, mends waterways and hands the care of the land back to local hands. Small work, done every season, in the places that need it most.",
"backgroundImage": "media:41830fd8-442a-4255-bc96-9fb12e428ef1",
"mediaPosition": "right",
"ctaPrimary": {
"text": "Donate",
"url": "/contact"
},
"ctaSecondary": {
"text": "See our work",
"url": "/about"
}
}
},
{
"block_key": "stats",
"title": "Season in numbers",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "The last season, in the ground",
"intro": "Every figure here is an acre planted, a bank mended, or a day given by someone who lives beside the work.",
"columns": 4,
"countUp": false,
"items": [
{
"value": "840",
"suffix": " acres",
"label": "Native cover planted"
},
{
"value": "19",
"suffix": " km",
"label": "Creek bank mended"
},
{
"value": "1,200",
"label": "Volunteer days given"
},
{
"value": "12",
"label": "Restoration sites"
}
]
}
},
{
"block_key": "feature-grid",
"title": "How we work",
"slot": "main",
"sort_order": 2,
"content": {
"eyebrow": "How we work",
"heading": "Three steps, every site",
"intro": "Restoration is not one grand act. It is the same patient sequence, repeated season after season, with the people who live on the land.",
"columns": 3,
"layout": "card",
"items": [
{
"icon": "lucide:sprout",
"title": "Plant native cover",
"text": "We raise local tube stock and plant the species that belong on each site — the ones that hold soil, feed pollinators and come back on their own."
},
{
"icon": "lucide:waves",
"title": "Mend the waterways",
"text": "We fence stock out of the creek lines, stabilise the banks and let the water run clean again. A mended creek carries a whole valley with it."
},
{
"icon": "lucide:heart-handshake",
"title": "Hand the land back",
"text": "From the first day we work beside local carers. By the third season we step back, and the people who live there keep it going."
}
]
}
},
{
"block_key": "divider",
"title": "Botanical divider",
"slot": "main",
"sort_order": 3,
"content": {
"variant": "ornament",
"width": "wide",
"icon": "lucide:leaf"
}
},
{
"block_key": "earthen:field_note",
"title": "Field Note",
"slot": "main",
"sort_order": 4,
"content": {
"author": "Mara Ellison",
"location": "Wirra Creek",
"dateline": "March 2025",
"image": "media:152f8386-ecf1-4f0c-899a-2c947766644d",
"body": "<p>The first rain after planting is the one that tells you. We walked the creek line at dawn and the tube stock had held — thousands of small green points, holding the bank against the next flood.</p><p>You plant, you fence, you wait. Restoration keeps its own slow clock. Then one ordinary morning the land answers back, and you understand you were only ever helping it remember what it already knew how to do.</p>"
}
},
{
"block_key": "newsletter-signup",
"title": "Newsletter",
"slot": "main",
"sort_order": 5,
"content": {
"title": "Field notes in your inbox",
"description": "A short dispatch from the sites each season — what we planted, what held, and where we are headed next. No noise.",
"listSlug": "main",
"source": "home",
"submitText": "Subscribe",
"placeholder": "you@example.com",
"layout": "card"
}
}
]
},
{
"slug": "/about",
"title": "Our work",
"template_key": "default",
"blocks": [
{
"block_key": "heading",
"title": "Heading",
"slot": "main",
"sort_order": 1,
"content": {
"text": "Our work",
"level": 1
}
},
{
"block_key": "image",
"title": "Forest regrowth",
"slot": "main",
"sort_order": 3,
"content": {
"src": "media:a8c164d1-5f0c-496d-bb77-d1e8ba769527",
"alt": "Fern fronds unfurling on the forest floor",
"caption": "Ferns returning to the understorey at Wirra Creek, three seasons after fencing.",
"size": "lg",
"aspectRatio": "wide"
}
},
{
"block_key": "rich-text",
"title": "Story",
"slot": "main",
"sort_order": 2,
"content": {
"html": "<p>Rootline Trust began with one degraded paddock and a handful of neighbours who wanted it back. We plant native cover, mend waterways and fence stock out of the creek lines. Then we hand the care of the land to the people who live beside it.</p><p>We work slowly and in public. Every site is chosen with local carers, planted from local stock, and reported honestly to the people who fund it. Nothing here happens in a season — it happens over three, or five, or ten, and it lasts because the people who live there stay long after we have gone.</p><p>We are a small trust on purpose. It keeps us close to the ground, close to the water, and close to the people whose land we are helping to mend.</p>"
}
},
{
"block_key": "earthen:impact_report",
"title": "Impact Report",
"slot": "main",
"sort_order": 4,
"content": {
"period": "Season 2025",
"title": "What we grew together",
"summary": "<p>Every acre here began as a pledge from someone who wanted a piece of degraded land to come back. This is where those pledges went last season — twelve sites, one patient sequence, repeated until the ground took hold.</p>",
"outcomes": [
{
"value": "840 acres",
"label": "Native cover planted",
"detail": "Across twelve restoration sites, raised from local tube stock."
},
{
"value": "19 km",
"label": "Creek bank mended",
"detail": "Fenced, replanted and handed to the carers who live beside it."
},
{
"value": "1,200",
"label": "Volunteer days",
"detail": "Given by people who live within a day's drive of the work."
}
]
}
},
{
"block_key": "divider",
"title": "Divider",
"slot": "main",
"sort_order": 5,
"content": {
"variant": "line",
"weight": "thin",
"width": "wide"
}
},
{
"block_key": "quote",
"title": "Quote",
"slot": "main",
"sort_order": 6,
"content": {
"style": "pull",
"align": "left",
"quote": "You plant, you fence, you wait. Then the land answers back.",
"attribution": "Mara Ellison",
"role": "Field Lead, Wirra Creek"
}
}
]
},
{
"slug": "/blog",
"title": "Field Journal",
"template_key": "blog-index",
"blocks": [
{
"block_key": "card",
"title": "Post: First Rain",
"slot": "main",
"sort_order": 1,
"content": {
"layout": "vertical",
"media": "media:603fdac3-2fa5-4511-8357-aa65550d9a31",
"title": "The first rain after planting",
"text": "<p>What a dawn walk along Wirra Creek tells you about a whole season of restoration.</p>",
"link": "/blog/the-first-rain-after-planting",
"linkLabel": "Read the dispatch"
}
},
{
"block_key": "card",
"title": "Post: Fencing",
"slot": "main",
"sort_order": 2,
"content": {
"layout": "vertical",
"media": "media:c71264ea-0217-4826-a796-baffab1cf041",
"title": "Why we fence before we plant",
"text": "<p>The least glamorous work on any site is the thing that protects everything that comes after it.</p>",
"link": "/blog/why-we-fence-before-we-plant",
"linkLabel": "Read the dispatch"
}
},
{
"block_key": "card",
"title": "Post: Handover",
"slot": "main",
"sort_order": 3,
"content": {
"layout": "vertical",
"media": "media:02a30527-5464-45e2-a1f9-529fd9f46787",
"title": "Handing the land back",
"text": "<p>Restoration is not finished when we leave. It is finished when the local carers take it on.</p>",
"link": "/blog/handing-the-land-back",
"linkLabel": "Read the dispatch"
}
}
]
},
{
"slug": "/blog/the-first-rain-after-planting",
"title": "The first rain after planting",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{
"block_key": "heading",
"title": "Title",
"slot": "main",
"sort_order": 1,
"content": {
"text": "The first rain after planting",
"level": 1
}
},
{
"block_key": "image",
"title": "Creek line at dawn",
"slot": "main",
"sort_order": 2,
"content": {
"src": "media:c71264ea-0217-4826-a796-baffab1cf041",
"alt": "Sunlight through moss-covered trees in regrown forest",
"caption": "The oldest reach at Wirra Creek, three seasons in — canopy closing, bank holding.",
"size": "lg",
"aspectRatio": "wide"
}
},
{
"block_key": "rich-text",
"title": "Body",
"slot": "main",
"sort_order": 3,
"content": {
"html": "<p>We walked the creek line at dawn, before the heat, the way we always do on the morning after the first real rain. The ground was dark and giving underfoot. Along the bank, where six weeks ago there had been bare clay and the stubble of old grazing, the tube stock had held — thousands of small green points, each one a native seedling with its roots now gripping the soil we had loosened for it.</p><p>The first rain after planting is the one that tells you. Everything before it is hope and hard work: you clear, you fence, you dig the holes on contour, you sink the tubes and firm the earth around each one. Then you wait, and you watch the sky, because a young planting that dries out in its first fortnight will not come back no matter how carefully it was put in. This rain came in the second week. It came slow and steady overnight, the kind that soaks in instead of running off, and by dawn the whole site had drunk it.</p><p>Restoration is slow, and it is meant to be. You cannot hurry a root system. You cannot argue a creek bank into stability. What you can do is give the land the conditions it needs and then get out of its way — fence the stock out, keep the weeds off the young growth, and let the seasons do the rest. We plant, we fence, we wait. It is the least heroic sequence imaginable, and it is the only one that works.</p><p>Wirra Creek was one of the worst sites we had taken on. Decades of stock had trodden the banks to bare earth; every flood carried another metre of topsoil downstream, and the water ran the colour of milky tea for days after any rain. When we first walked it with the carers who live along it, nobody pretended it would be quick. We agreed a ten-year horizon out loud, in the paddock, so that no one would mistake a single good season for the finish line.</p><p>The species matter more than people expect. We raise our tube stock from local seed — the sedges and wattles and paperbarks that already know this soil and this rainfall — because a plant that belongs on a site will hold on through the years we are not watching. The nursery work is unglamorous and it is where half the outcome is decided. A good seedling, hardened off properly, will take the first rain and run with it. A soft one, forced under lights and shipped in from far away, will sulk and die at the first hot week.</p><p>By mid-morning the sun was up and the carers had arrived to walk the line with us. There is a particular quiet that comes over people on a morning like that — nobody says much. You crouch by a tube, you feel the soil, you check that the guard has held and the stem is firm, and you move on to the next. Down the line someone laughs, because a seedling that was leaning last week has stood itself upright overnight, the way they do once the roots find water.</p><p>Not everything held. It never does. In the low corner where the flood sits longest we lost a run of tubes to waterlogging, and up on the dry western bank a few had been nosed out by something — a wallaby, most likely, testing the guards. We marked the gaps to replant in autumn. Losing some is part of the arithmetic; you plant knowing a portion will fail, and you plant enough that the survivors still close the canopy in time.</p><p>The fence is what makes all of it possible. Before a single tube goes in, we fence the stock out of the creek line and hold that line for years. It is the least glamorous work we do and it is the work that protects everything else. A good fence buys a young planting three quiet years to establish, and three years is usually all a native system needs to look after itself.</p><p>We finished the walk at the top of the reach, where the oldest planting — three seasons in now — has grown past head height and thrown enough shade that the bank beneath it stays damp and dark. Frogs have come back to that stretch. You can hear them after rain. It is the plainest possible proof that the work is working: you make a place liveable again, and the living things move back in without being asked.</p><p>That is the whole of it, really. You plant, you fence, you wait. Restoration keeps its own slow clock and there is no cheating it. Then one ordinary morning after rain the land answers back — a bank that holds, a creek that runs clear, a chorus of frogs where there was bare clay — and you understand that you were only ever helping the land remember what it already knew how to do.</p>"
}
}
]
},
{
"slug": "/blog/why-we-fence-before-we-plant",
"title": "Why we fence before we plant",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{
"block_key": "heading",
"title": "Title",
"slot": "main",
"sort_order": 1,
"content": {
"text": "Why we fence before we plant",
"level": 1
}
},
{
"block_key": "rich-text",
"title": "Body",
"slot": "main",
"sort_order": 2,
"content": {
"html": "<p>Stock will find new growth before it has a chance to root. A single mob of cattle can undo a season's planting in an afternoon, and they always seem to know exactly where the softest new leaves are. So we fence first. Every time. It is the least glamorous work we do and it is the thing that protects everything that comes after.</p><p>A good fence buys a young planting three years of quiet — long enough for the roots to reach down, the canopy to start closing, and the site to stop needing us. Three years is usually all a native system asks for. Get the fence right and hold the line, and the rest of restoration is mostly patience.</p><p>People ask why we do not just plant more to make up for what gets eaten. The answer is that a fence is cheaper than a failure, and far cheaper than the heartbreak of walking a chewed-out site the morning after. We would rather spend the first budget on posts and wire than replant the same bank three times over.</p>"
}
}
]
},
{
"slug": "/blog/handing-the-land-back",
"title": "Handing the land back",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{
"block_key": "heading",
"title": "Title",
"slot": "main",
"sort_order": 1,
"content": {
"text": "Handing the land back",
"level": 1
}
},
{
"block_key": "rich-text",
"title": "Body",
"slot": "main",
"sort_order": 2,
"content": {
"html": "<p>Restoration is not finished when we leave. It is finished when the local carers take it on. We plant with them from the first day — not as spectators, but with hands in the soil — so the knowledge stays on the land instead of leaving in the back of our ute.</p><p>By the third season we start to step back. The watering slows, the replanting tapers off, and the walks along the line become the carers' walks, not ours. The site keeps going because the people beside it keep going, and they keep going because it was theirs the whole way through.</p><p>We measure our success by how little we are needed at the end. A site we can walk away from is the goal. It sounds strange for an organisation to aim at its own irrelevance, but that is exactly the point: land that can look after itself, in the care of people who will be there long after the last of our fences has weathered grey.</p>"
}
}
]
},
{
"slug": "/contact",
"title": "Get in touch",
"template_key": "contact",
"blocks": [
{
"block_key": "contact-form",
"title": "Contact Enquiry",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "Work with us",
"description": "Tell us about your land, your region or how you would like to help. We read every message.",
"submitLabel": "Send message",
"successMessage": "Thank you. Your message has been received. We will reply by email.",
"fields": [
{
"name": "name",
"label": "Your name",
"type": "text",
"required": true,
"width": "half"
},
{
"name": "email",
"label": "Email",
"type": "email",
"required": true,
"width": "half"
},
{
"name": "region",
"label": "Region",
"type": "text",
"required": false,
"width": "half"
},
{
"name": "message",
"label": "How can we help?",
"type": "textarea",
"required": true,
"width": "full"
}
],
"formConfig": {
"enabled": true,
"captchaEnabled": false,
"targetTable": "contact_submissions"
}
}
},
{
"block_key": "hours-location",
"title": "Where to find us",
"slot": "aside",
"sort_order": 1,
"content": {
"heading": "Office hours",
"hours": [
{
"day": "Monday to Thursday",
"hours": "9am to 5pm"
},
{
"day": "Friday",
"hours": "9am to 3pm"
},
{
"day": "Saturday",
"hours": "Field days only"
},
{
"day": "Sunday",
"closed": true
}
],
"specialHoursNote": "Planting days run most weekends in autumn. Check the field journal.",
"address": "3 Wattle Road\nWirra Creek",
"phone": "+1 555 0142"
}
}
]
},
{
"slug": "/login",
"title": "Members",
"template_key": "auth",
"blocks": [
{
"block_key": "auth-form",
"title": "Sign in",
"slot": "main",
"sort_order": 1,
"content": {
"default_tab": "login",
"show_social": false
}
},
{
"block_key": "text",
"title": "Foot",
"slot": "footer",
"sort_order": 1,
"content": {
"text": "<p>Members track pledges and see each site's season report first.</p>"
}
}
]
}
],
"menu_items": [
{
"menu": "main",
"label": "Home",
"page_slug": "/",
"sort_order": 1
},
{
"menu": "main",
"label": "Our work",
"page_slug": "/about",
"sort_order": 2
},
{
"menu": "main",
"label": "Field Journal",
"page_slug": "/blog",
"sort_order": 3
},
{
"menu": "main",
"label": "Get in touch",
"page_slug": "/contact",
"sort_order": 4
}
]
}

19
seed/workflows.json Normal file
View File

@ -0,0 +1,19 @@
{
"workflows": [
{
"key": "contact_notify_admins",
"name": "Contact enquiry to admins",
"description": "On a contact-form submission, email the site admins.",
"enabled": true,
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
"steps": [
{
"type": "email",
"name": "Email admins",
"template": "Form Submission Notification",
"recipients": { "mode": "admins", "scope": "all" }
}
]
}
]
}

View File

@ -1,273 +0,0 @@
package main
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData contains the data for a rendered Earthen page.
type PageData struct {
Title string
Slots map[string]string
ThemeMode string
ThemeCSS string
SiteSettings bn.SiteSettingsData
PageMeta bn.PageMeta
StructuredData string
CSSHash string
PageviewNonce string
EngagementConfig bn.EngagementConfig
}
func parseEarthenPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: siteSettings,
PageMeta: pageMeta,
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: engagementConfig,
}
}
// EarthenDefault renders the default Earthen page (header, main, footer).
templ EarthenDefault(data PageData) {
<!DOCTYPE html>
<html lang="en" class="earthen">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/earthen/css/earthen.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body
class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col"
style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);"
>
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full max-w-5xl mx-auto px-4 py-10">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center" style="color: hsl(var(--muted-foreground));">
<p>No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// EarthenLanding renders the landing template (hero, main, cta, footer).
templ EarthenLanding(data PageData) {
<!DOCTYPE html>
<html lang="en" class="earthen">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/earthen/css/earthen.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body
class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col"
style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);"
>
@bn.AdminBypassBanner(data.SiteSettings)
<section class="w-full">
@templ.Raw(data.Slots["hero"])
</section>
<main class="flex-grow">
if main, ok := data.Slots["main"]; ok && main != "" {
<div class="max-w-5xl mx-auto px-4 py-16">
@templ.Raw(main)
</div>
}
</main>
<section class="w-full">
@templ.Raw(data.Slots["cta"])
</section>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// EarthenArticle renders the article template (header, lede, main, aside, footer).
templ EarthenArticle(data PageData) {
<!DOCTYPE html>
<html lang="en" class="earthen">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/earthen/css/earthen.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body
class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col"
style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);"
>
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
<div class="max-w-3xl mx-auto px-4">
@templ.Raw(data.Slots["header"])
</div>
</header>
if lede, ok := data.Slots["lede"]; ok && lede != "" {
<section class="w-full max-w-3xl mx-auto px-4 pt-12">
@templ.Raw(lede)
</section>
}
<div class="flex-grow w-full grid grid-cols-1 lg:grid-cols-[1fr,260px] gap-12 max-w-5xl mx-auto px-4 py-12">
<main>
if main, ok := data.Slots["main"]; ok && main != "" {
<article class="earthen-article">
@templ.Raw(main)
</article>
} else {
<p style="color: hsl(var(--muted-foreground));">No story body yet.</p>
}
</main>
if aside, ok := data.Slots["aside"]; ok && aside != "" {
<aside class="earthen-aside text-sm">
@templ.Raw(aside)
</aside>
}
</div>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// EarthenFullWidth renders edge-to-edge pages (header, main, footer).
templ EarthenFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en" class="earthen">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/earthen/css/earthen.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body
class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col"
style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);"
>
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="max-w-4xl mx-auto py-20 px-4 text-center" style="color: hsl(var(--muted-foreground));">
<p>No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// RenderEarthenDefault is the templates.TemplateFunc entry for "default".
func RenderEarthenDefault(ctx context.Context, doc map[string]any) templ.Component {
return EarthenDefault(parseEarthenPageData(doc))
}
// RenderEarthenLanding is the templates.TemplateFunc entry for "landing".
func RenderEarthenLanding(ctx context.Context, doc map[string]any) templ.Component {
return EarthenLanding(parseEarthenPageData(doc))
}
// RenderEarthenArticle is the templates.TemplateFunc entry for "article".
func RenderEarthenArticle(ctx context.Context, doc map[string]any) templ.Component {
return EarthenArticle(parseEarthenPageData(doc))
}
// RenderEarthenFullWidth is the templates.TemplateFunc entry for "full-width".
func RenderEarthenFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return EarthenFullWidth(parseEarthenPageData(doc))
}

View File

@ -1,543 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData contains the data for a rendered Earthen page.
type PageData struct {
Title string
Slots map[string]string
ThemeMode string
ThemeCSS string
SiteSettings bn.SiteSettingsData
PageMeta bn.PageMeta
StructuredData string
CSSHash string
PageviewNonce string
EngagementConfig bn.EngagementConfig
}
func parseEarthenPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: siteSettings,
PageMeta: pageMeta,
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: engagementConfig,
}
}
// EarthenDefault renders the default Earthen page (header, main, footer).
func EarthenDefault(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"earthen\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/earthen/css/earthen.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"bg-paper-grain earthen-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</header><main class=\"flex-grow w-full max-w-5xl mx-auto px-4 py-10\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"py-20 text-center\" style=\"color: hsl(var(--muted-foreground));\"><p>No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</main><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// EarthenLanding renders the landing template (hero, main, cta, footer).
func EarthenLanding(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<!doctype html><html lang=\"en\" class=\"earthen\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/earthen/css/earthen.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<body class=\"bg-paper-grain earthen-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<section class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["hero"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</section><main class=\"flex-grow\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"max-w-5xl mx-auto px-4 py-16\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</main><section class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["cta"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</section><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// EarthenArticle renders the article template (header, lede, main, aside, footer).
func EarthenArticle(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<!doctype html><html lang=\"en\" class=\"earthen\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/earthen/css/earthen.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<body class=\"bg-paper-grain earthen-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<header class=\"w-full\" style=\"border-bottom: 1px solid hsl(var(--border));\"><div class=\"max-w-3xl mx-auto px-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></header>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if lede, ok := data.Slots["lede"]; ok && lede != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<section class=\"w-full max-w-3xl mx-auto px-4 pt-12\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(lede).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"flex-grow w-full grid grid-cols-1 lg:grid-cols-[1fr,260px] gap-12 max-w-5xl mx-auto px-4 py-12\"><main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<article class=\"earthen-article\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p style=\"color: hsl(var(--muted-foreground));\">No story body yet.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if aside, ok := data.Slots["aside"]; ok && aside != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<aside class=\"earthen-aside text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(aside).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// EarthenFullWidth renders edge-to-edge pages (header, main, footer).
func EarthenFullWidth(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<!doctype html><html lang=\"en\" class=\"earthen\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/earthen/css/earthen.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<body class=\"bg-paper-grain earthen-body antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</header><main class=\"flex-grow w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"max-w-4xl mx-auto py-20 px-4 text-center\" style=\"color: hsl(var(--muted-foreground));\"><p>No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</main><footer class=\"w-full mt-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// RenderEarthenDefault is the templates.TemplateFunc entry for "default".
func RenderEarthenDefault(ctx context.Context, doc map[string]any) templ.Component {
return EarthenDefault(parseEarthenPageData(doc))
}
// RenderEarthenLanding is the templates.TemplateFunc entry for "landing".
func RenderEarthenLanding(ctx context.Context, doc map[string]any) templ.Component {
return EarthenLanding(parseEarthenPageData(doc))
}
// RenderEarthenArticle is the templates.TemplateFunc entry for "article".
func RenderEarthenArticle(ctx context.Context, doc map[string]any) templ.Component {
return EarthenArticle(parseEarthenPageData(doc))
}
// RenderEarthenFullWidth is the templates.TemplateFunc entry for "full-width".
func RenderEarthenFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return EarthenFullWidth(parseEarthenPageData(doc))
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en" class="earthen">
{{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
{% if slots.lede %}<section class="w-full max-w-3xl mx-auto px-4 pt-12">{{ slots.lede|safe }}</section>{% endif %}
<div class="flex-grow w-full grid grid-cols-1 lg:grid-cols-[1fr,260px] gap-12 max-w-5xl mx-auto px-4 py-12">
<main>{% if slots.main %}<article class="earthen-article">{{ slots.main|safe }}</article>{% else %}<p style="color: hsl(var(--muted-foreground));">No story body yet.</p>{% endif %}</main>
{% if slots.aside %}<aside class="earthen-aside text-sm">{{ slots.aside|safe }}</aside>{% endif %}
</div>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" class="earthen">
{{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
<main class="flex-grow w-full flex flex-col items-center justify-center px-4 py-12">
<div class="w-full max-w-md">
<a href="/" class="block text-center" aria-label="Home">
<svg aria-hidden="true" class="mx-auto mb-4 h-11 w-11" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" style="color:hsl(var(--primary));"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>
</a>
<div>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p style="color:hsl(var(--muted-foreground));">No form assigned to this page.</p></div>{% endif %}</div>
{% if slots.footer %}<div class="mt-6 text-center text-sm" style="color:hsl(var(--muted-foreground));">{{ slots.footer|safe }}</div>{% endif %}
</div>
</main>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en" class="earthen">
{{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">
<div class="max-w-6xl mx-auto px-4 py-12 md:py-16">
{% if title %}
<div class="text-center">
<svg aria-hidden="true" class="mx-auto mb-4 h-10 w-10" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" style="color:hsl(var(--primary));"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>
<h1 class="crayon-underline mx-auto w-fit text-4xl md:text-5xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ title }}</h1>
{% if page_meta.MetaDescription %}<p class="mt-4 text-lg" style="color:hsl(var(--muted-foreground));">{{ page_meta.MetaDescription }}</p>{% endif %}
</div>
{% endif %}
{% if slots.featured %}<section class="mt-12">{{ slots.featured|safe }}</section>{% endif %}
{% if slots.main %}<section class="mt-12">{{ slots.main|safe }}</section>{% else %}<div class="py-20 text-center"><p style="color:hsl(var(--muted-foreground));">No posts yet. New stories from the field will appear here soon.</p></div>{% endif %}
</div>
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en" class="earthen">
{{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">
<div class="max-w-6xl mx-auto px-4 py-12 md:py-16">
{% if slots.aside %}
<div class="grid gap-12 md:grid-cols-[2fr_1fr]">
<div>{% if slots.main %}{{ slots.main|safe }}{% endif %}</div>
<aside class="w-full md:border-l md:border-border md:pl-10">{{ slots.aside|safe }}</aside>
</div>
{% else %}
<div class="max-w-3xl mx-auto">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p style="color:hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}</div>
{% endif %}
</div>
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en" class="earthen">
{{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full max-w-5xl mx-auto px-4 py-10">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center" style="color: hsl(var(--muted-foreground));"><p>No content blocks assigned to this page.</p></div>{% endif %}</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en" class="earthen">
{{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ 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="max-w-4xl mx-auto py-20 px-4 text-center" style="color: hsl(var(--muted-foreground));"><p>No content blocks assigned to this page.</p></div>{% endif %}</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" class="earthen">
{{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
<section class="w-full">{{ slots.hero|safe }}</section>
<main class="flex-grow w-full">{% if slots.main %}{{ slots.main|safe }}{% endif %}</main>
<section class="w-full">{{ slots.cta|safe }}</section>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,9 @@
<!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"><!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]--><title>{{ site_name }}</title></head><body style="background-color: {{ colors.background|default:"#F6F1E6" }}; margin: 0; padding: 0; font-family: &amp;#39;Work Sans&amp;#39;, ui-sans-serif, system-ui, sans-serif; color: {{ colors.foreground|default:"#2D3A28" }};">{% 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" style="background-color: {{ colors.background|default:"#F6F1E6" }};"><tr><td align="center" style="padding: 32px 12px;"><table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#FCF9F2" }}; border: 1px solid {{ colors.border|default:"#D6D1C2" }}; border-radius: 12px; overflow: hidden;"><tr><td align="center" style="padding: 28px 32px; background-color: {{ colors.primary|default:"#3A5A36" }}; color: {{ colors.primaryForeground|default:"#F6F1E6" }};">{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 44px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-family: &amp;#39;Fraunces&amp;#39;, &amp;#39;Playfair Display&amp;#39;, Georgia, serif; font-size: 26px; font-weight: 600; letter-spacing: -0.01em; color: {{ colors.primaryForeground|default:"#F6F1E6" }};">{{ site_name }}</h1>{% endif %}</td></tr><tr><td style="padding: 36px 40px; font-size: 16px; line-height: 1.7; color: {{ colors.foreground|default:"#2D3A28" }}; font-family: &amp;#39;Work Sans&amp;#39;, ui-sans-serif, system-ui, sans-serif;">{{ body|safe }}</td></tr><tr><td style="padding: 24px 40px; background-color: {{ colors.muted|default:"#EFEBDF" }}; border-top: 1px solid {{ colors.border|default:"#D6D1C2" }};"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td align="center"><p style="margin: 0 0 8px; font-family: &amp;#39;Fraunces&amp;#39;, &amp;#39;Playfair Display&amp;#39;, Georgia, serif; font-size: 14px; font-weight: 600; color: {{ colors.foreground|default:"#2D3A28" }};">{{ site_name }}</p><p style="margin: 0 0 6px; font-size: 11px; color: {{ colors.mutedForeground|default:"#6E7560" }};">Registered charity. We respect your inbox.</p>{% if site_url %}<p style="margin: 0 0 12px; font-size: 12px; color: {{ colors.mutedForeground|default:"#6E7560" }};"><a href="{{ site_url }}" style="color: {{ colors.primary|default:"#3A5A36" }}; text-decoration: none; border-bottom: 1px dotted {{ colors.primary|default:"#3A5A36" }};">{{ site_url }}</a></p>{% endif %}{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px; color: {{ colors.mutedForeground|default:"#6E7560" }};"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#6E7560" }}; text-decoration: underline;">Unsubscribe</a></p>{% else %}<p style="margin: 0; font-size: 11px; color: {{ colors.mutedForeground|default:"#6E7560" }};"><a href="#" style="color: {{ colors.mutedForeground|default:"#6E7560" }}; text-decoration: underline;">Unsubscribe</a></p>{% endif %}</td></tr></table></td></tr></table></td></tr></table></body></html>

View File

@ -0,0 +1,21 @@
{# announcement-bar override — earthen banner on moss/clay tones. Dismiss <script>, data-bn-annkey content-hash and localStorage keying preserved verbatim from the builtin; only classes reskinned. Fields mirror builtin: variant, message, linkText, linkUrl, dismissable. #}
{% set anncls = "bg-primary/10 text-foreground" %}
{% if variant == "promo" %}{% set anncls = "bg-primary text-primary-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
<div data-block-override="earthen:announcement-bar" data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full border-b border-border {{ anncls }} {{ class }}">
<div class="max-w-7xl mx-auto px-10 py-2 relative flex items-center justify-center gap-2 text-sm text-center">
<p class="font-medium" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline 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 -translate-y-1/2 inline-flex items-center justify-center p-1 rounded-lg hover:bg-black/10 transition-colors"><svg width="16" height="16" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
</div>
</div>
{% if dismissable %}<script>
(function(){
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
bars.forEach(function(bar){
bar.setAttribute('data-bn-ann-wired','1');
var key='bnann:'+bar.getAttribute('data-bn-annkey');
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
var btn=bar.querySelector('[data-bn-ann-dismiss]');
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
});
})();
</script>{% endif %}

View File

@ -0,0 +1,16 @@
{# audio-embed override — cream paper player card. Fields + facade JS mirror builtin verbatim: url, media, artwork, title, artist, download. #}
<figure data-block-override="earthen:audio-embed" class="bn-audio earthen-panel earthen-blob-soft 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="earthen-blob-soft h-16 w-16 shrink-0 overflow-hidden bg-muted">{% 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="truncate text-sm font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</p>{% endif %}{% if artist %}<p class="truncate text-sm italic" style="color:hsl(var(--muted-foreground));">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="earthen-cta inline-flex items-center gap-2 px-4 py-1.5 text-sm font-semibold" aria-label="Play{% if title %}: {{ title }}{% endif %}">::lucide:play:sm:: Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="earthen-link inline-flex items-center gap-1 text-sm">::lucide:download:sm:: Download</a>{% endif %}</div></div></div></figure><script>
(function(){
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
e.preventDefault();
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
if(!src)return;
var slot=btn.closest('[data-bn-audio-slot]');
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
btn.remove();slot.insertBefore(a,slot.firstChild);
});
})();
</script>

View File

@ -0,0 +1,33 @@
{# auth-form override — earthen login/register on a paper panel. Wiring is UNTOUCHABLE and reproduced exactly from builtin auth_blocks.go: single shared #auth-message, /api/auth/login (email,password) and /api/auth/register (username,email,password), hx-target #auth-message. Captcha widget injected from _captcha_widget when captchaEnabled (server enforces). Fields: default_tab, show_social (social omitted — needs Go/site config; graceful). Only classes/styles/copy reskinned to earthen. #}
{% if context.isPublicLoggedIn %}
<div data-block-override="earthen:auth-form" class="auth-form-logged-in mx-auto max-w-md text-center py-8">
<p style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">You are signed in as <strong style="color:hsl(var(--foreground));">{{ context.publicUsername }}</strong></p>
<a href="/account" class="earthen-link mt-3 inline-block">My account</a>
</div>
{% else %}
<div data-block-override="earthen:auth-form" class="auth-form earthen-panel mx-auto max-w-md p-8">
<div id="auth-message"></div>
<div class="mb-6 flex border-b border-border">
<button type="button" onclick="document.getElementById('login-panel').classList.remove('hidden');document.getElementById('register-panel').classList.add('hidden');this.setAttribute('data-active','1');this.nextElementSibling.removeAttribute('data-active');" data-auth-tab {% if default_tab != "register" %}data-active="1"{% endif %} class="flex-1 py-2 text-center text-sm font-medium uppercase tracking-wide border-b-2 border-transparent data-[active]:border-[hsl(var(--primary))] data-[active]:text-primary" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">Sign in</button>
<button type="button" onclick="document.getElementById('register-panel').classList.remove('hidden');document.getElementById('login-panel').classList.add('hidden');this.setAttribute('data-active','1');this.previousElementSibling.removeAttribute('data-active');" data-auth-tab {% if default_tab == "register" %}data-active="1"{% endif %} class="flex-1 py-2 text-center text-sm font-medium uppercase tracking-wide border-b-2 border-transparent data-[active]:border-[hsl(var(--primary))] data-[active]:text-primary" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">Join us</button>
</div>
<div id="login-panel" class="{% if default_tab == "register" %}hidden{% endif %}">
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
<div class="mb-4"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Password</label><input type="password" name="password" required class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
<button type="submit" class="earthen-cta w-full" hx-disabled-elt="this">Sign in</button>
<p class="text-center mt-3 text-sm"><a href="/forgot-password" class="earthen-link">Forgot password?</a></p>
</form>
</div>
<div id="register-panel" class="{% if default_tab != "register" %}hidden{% endif %}">
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
<div class="mb-3"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
<div class="mb-4"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Password</label><input type="password" name="password" required minlength="8" class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
<button type="submit" class="earthen-cta w-full" hx-disabled-elt="this">Create account</button>
</form>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,31 @@
{# auth-status override — header login state. Logged-out: Sign in / Join links. Logged-in: moss monogram + dropdown (logout posts /api/auth/logout). Wiring UNTOUCHABLE: context.isPublicLoggedIn / context.publicDisplayName / context.publicUsername reads, #auth-dropdown-menu, onclick toggle, outside-click <script>, hx-post /api/auth/logout. Only classes/styles/copy reskinned to earthen. #}
{% if context.isPublicLoggedIn %}
<div data-block-override="earthen:auth-status" class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="flex items-center gap-2 text-sm font-medium opacity-90 hover:opacity-100 px-3 py-1.5 transition-colors">
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full text-[0.7rem] font-bold" style="background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));">{{ context.publicDisplayName|default:context.publicUsername|first|upper }}</span>
<span>{{ context.publicDisplayName|default:context.publicUsername }}</span>
<svg width="16" height="16" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div id="auth-dropdown-menu" class="hidden absolute right-0 top-full mt-1 w-48 earthen-panel py-1 z-50">
<a href="/account" class="block px-4 py-2 text-sm text-foreground hover:bg-muted">My profile</a>
<a href="/account/reviews" class="block px-4 py-2 text-sm text-foreground hover:bg-muted">My reviews</a>
<hr class="my-1 border-border"/>
<button hx-post="/api/auth/logout" hx-swap="none" class="block w-full text-left px-4 py-2 text-sm text-destructive hover:bg-muted">Sign out</button>
</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>
</div>
{% else %}
<div data-block-override="earthen:auth-status" class="auth-status flex items-center gap-2">
<a href="/login" class="text-sm px-3 py-1.5 opacity-80 hover:opacity-100 hover:text-primary transition-colors">Sign in</a>
<a href="/login?tab=register" class="earthen-cta text-sm">Join us</a>
</div>
{% endif %}

View File

@ -0,0 +1,22 @@
{# author-bio-hero (earthen) — author-page header from the `authors` provider. Guards, field reads, social icons (via <use>) and image wiring preserved; earthen skin only. #}
{% if author %}
<section data-block-override="earthen:author-bio-hero" class="border-b border-border bg-paper-grain 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="earthen-blob mb-6 h-24 w-24 overflow-hidden bg-muted">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
<svg class="earthen-glyph mb-4 h-8 w-8" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>
<h1 class="text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="mt-2 text-sm font-medium text-accent">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
{% if author.bio %}<p class="mt-5 max-w-2xl leading-relaxed text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ author.bio }}</p>{% endif %}
<div class="mt-6 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-6 flex items-center justify-center gap-4">
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Website"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Twitter"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="LinkedIn"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="GitHub"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,13 @@
{# author-bio (earthen) — byline card sourced from the `post` provider's author_* fields. Guards, field reads and image wiring preserved; earthen skin only. #}
{% if post.author_name %}
<aside data-block-override="earthen:author-bio" class="earthen-panel earthen-blob-soft p-6 text-card-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="mb-4 text-xs font-semibold uppercase tracking-wider text-accent">{{ heading }}</p>{% endif %}
<div class="flex items-start gap-4">
{% if showAvatar and post.author_avatar %}<div class="earthen-blob-soft h-14 w-14 flex-shrink-0 overflow-hidden bg-muted">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="min-w-0">
<h3 class="text-base font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--card-foreground));">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-primary">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
{% if post.author_bio %}<p class="mt-2 text-sm text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -0,0 +1,24 @@
{# blog-hero (earthen) — blog-index header + featured posts from the `posts` provider. Wiring, field reads and loops preserved from builtin; earthen skin only. #}
<section data-block-override="earthen:blog-hero" class="bg-paper-grain py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<div class="mx-auto max-w-2xl text-center">
<svg class="earthen-glyph mx-auto mb-4 h-9 w-9" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>
{% if title %}<h1 class="crayon-underline inline-block text-4xl md:text-5xl" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ title }}</h1>{% endif %}
{% if subtitle %}<p class="mt-4 text-lg text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ subtitle }}</p>{% endif %}
</div>
{% if posts %}
<div class="mt-12 grid grid-cols-1 gap-8 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-8{% endif %}">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="earthen-blob-soft mb-4 block overflow-hidden bg-muted{% 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 object-cover transition-transform duration-500 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
<h2 class="{% if forloop.First %}text-2xl{% else %}text-lg{% endif %} font-semibold leading-snug" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h2>
{% if showExcerpt and post.excerpt %}<p class="mt-3 text-sm text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ post.excerpt }}</p>{% endif %}
<div class="mt-4 flex flex-wrap items-center gap-x-3 text-xs text-muted-foreground">{% if post.author_name %}<span class="font-medium text-accent">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>
</section>

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