Compare commits

...

19 Commits
v0.1.1 ... main

Author SHA1 Message Date
Alex Dunmow
681b4808f0 bump to 0.5.0 2026-07-06 20:54:10 +08:00
Alex Dunmow
8c1da68375 feat(editorial): bundled-font @font-face + typography refs, required_icon_packs, reshoot in real serif
Declare @font-face for the bundled Playfair Display + Source Serif 4 faces
against /templates/editorial/fonts/*.woff2 and point every preset's
typography.fontHeading/fontBody at template:editorial:<Family> so
--font-heading/--font-body resolve to the bundled serifs (gotham@1.2.0
precedent; CMS BundledFonts is dead code). Declare required_icon_packs
[lucide, simple-icons] for the sprite pipeline. Recapture the 6-shot gallery
(home/about/article x light+dark) now that the serif actually renders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:54:03 +08:00
Alex Dunmow
68fe19aaed bump to 0.4.1 2026-07-06 20:43:56 +08:00
Alex Dunmow
8d60707cef bump to 0.4.0 2026-07-06 16:10:12 +08:00
Alex Dunmow
3bf1679eee feat(editorial): Wave B gallery screenshots (home/about/article x light+dark)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:09:55 +08:00
Alex Dunmow
db031804bf bump to 0.3.1 2026-07-06 16:07:18 +08:00
Alex Dunmow
965ecf3228 fix(editorial): article H1, marginalia separator+contrast, home composition, SVG geometry guard
- article template now renders <h1>{{ title }} once (was absent — title never showed)
- marginalia anchor label is block-level foreground ink (was inline muted run-on)
- landing lead top padding trimmed to close the dead band above the hero card
- 22 icon-sprite <use> SVGs carry explicit width/height (JIT balloon guard)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:07:14 +08:00
Alex Dunmow
936b48653d bump to 0.3.0 2026-07-06 09:24:56 +08:00
Alex Dunmow
18b0d7c4b7 feat: Wave B gallery screenshots (home/about/article × light+dark) 2026-07-06 09:24:38 +08:00
Alex Dunmow
a1caa09d2a chore: rename dev host localdev.blockninjacms.com -> blockninja.dev 2026-07-05 20:50:30 +08:00
Alex Dunmow
52a2501959 refactor: replace hand-rolled utility CSS with Tailwind classes (host build now scans theme templates) 2026-07-05 17:55:48 +08:00
Alex Dunmow
10cecadef6 fix(editorial): remediate hero dead-space, tiny titles, media balloon + mobile
Layout/whitespace:
- Hero: gate min-h-[75vh] + vertical-centering on hasImage so the text-only
  masthead no longer floats in ~800px of dead space; large display headline
  via CSS class (was collapsed to 0.72rem by editorial-caps clobbering text-6xl).
- Restore heading hierarchy: swap editorial-caps -> editorial-heading on every
  title/headline element (hero, card, newsletter, contact-form, cta, countdown,
  feature-grid, team, featured-posts, post-hero, menu-list, hours-location,
  author-bio-hero, password-reset-form). Masthead wordmarks (navbar/footer)
  keep editorial-caps by design.
- Home "river": constrain to a 48rem reading measure and add vertical rhythm
  (editorial-river/editorial-flow CSS) so stacked essay cards stop fusing into
  one seam-y block and body text no longer runs full-bleed wide.
- Newsletter: remove stray empty double-hairline artifact (single centered rule).
- Image: guard empty src so absent media no longer renders an empty muted box.

Mobile / JIT-safety:
- New width/spacing/hero-title behavior defined as theme CSS in manifest
  input_css_append (not JIT-scanned Tailwind), so it always compiles.
- Marginalia stack breakpoint widened 768px -> 1023px to match the article
  grid collapse, so the rail always gets its separator when stacked.

Build + check-safety green (32 checks: 19 ok 13 skip). No version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:25:31 +08:00
Alex Dunmow
28a201a33a bump to 0.2.1 2026-07-05 16:44:48 +08:00
Alex Dunmow
ae082bede0 docs: publishing README 2026-07-05 13:43:24 +08:00
Alex Dunmow
a66113fce0 feat(editorial): Wave A theme lane (WO-TF-017) — personal essayist
Deliver the full Wave A contract for the editorial theme (personal
essayist / independent newsletter persona).

- 49 mandatory builtin overrides in templates/overrides/editorial/,
  re-skinned from the frozen builtins: hairline rules, small-caps labels,
  drop caps, narrow reading measure, oxblood accent. All content fields,
  provider fields, custom tags ({% img %}/{% form %}/{% signup_form %}),
  hx wiring, navbar drawer JS and auth/contact form wiring preserved
  verbatim; legacy button/card field reads reconciled (link/style/label,
  media/text).
- 7 page templates (default, full-width, landing, article, blog-index,
  contact, auth); article is the reading centrepiece with drop-cap opener,
  marginalia rail and a CSS-only reading-progress underline. All set the
  html.dark class and route body font through var(--font-body).
- Block rework: masthead/byline/pullquote/section_label/colophon deleted
  (now overrides of navbar/post-metadata/quote/divider/footer). Persona
  blocks: dropcap_intro, marginalia, reading_list, field_notes.
- 4 presets, all mode "both", book-paper light + sepia-on-charcoal dark.
- Bundled Playfair Display + Source Serif 4 (OFL, latin subset) in
  assets/fonts with license record; fonts.json populated; every
  font-family via var(--font-heading|body|mono, fallback).
- Themed email-safe wrapper (tables, inline styles, literal fonts).
- Master pages + seed demo site (home, about, blog with 3 essays,
  contact with form + data table + row_inserted email-admins workflow,
  login) in essayist voice.
- required_icon_packs=["lucide"]; RECOMMENDED_FONTS/ICONS updated.

Motion tier: CSS-first (progress underline, marginalia fade-in;
prefers-reduced-motion honored). Verified: make + ninja plugin verify +
check-safety all exit 0. Visual quality UNVERIFIED until Wave B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 11:22:07 +08:00
Alex Dunmow
efbfaea74c feat: convert to codeless .bnp — templates + manifest, Go/templ deleted (WO-WZ-021)
Convert the Editorial theme from a Go+templ wasm plugin to a fully codeless
.bnp: 7 blocks + 4 built-in overrides + email wrapper transcribed templ→pongo2
.ninjatpl (byte-faithful, parity-verified 55/55 identical), 4 page templates,
blocks/blocks.yaml (fully-qualified editorial:<key>), master_pages.json, and
manifest.yaml (system/page templates, template_overrides, email_wrapper, css
input_css_append). Schemas moved schemas/→blocks/. All Go, templ, go.mod/go.sum
deleted. plugin.mod bumped 0.1.2→0.2.0, [compatibility] dropped. Makefile
rewritten to codeless targets.

Gates: ninja plugin build → (codeless, no plugin.wasm); ninja plugin verify → OK;
check-safety → OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:27:31 +08:00
Alex Dunmow
6c9426f69c 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:54 +08:00
Alex Dunmow
dc9e6385b5 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
1d7f784412 bump to 0.1.2 2026-07-03 22:09:46 +08:00
141 changed files with 2616 additions and 4414 deletions

1
.gitignore vendored
View File

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

View File

@ -206,7 +206,7 @@ Notes:
spec flags this; a span-extraction fallback is deferred.
- **Screenshots and demo seed content** (UAT §12): not produced in
this build pass. The six 1280×800 PNGs would be captured against
`https://editorial.localdev.blockninjacms.com/` after deployment;
`https://editorial.blockninja.dev/` after deployment;
the demo seed (`The Quiet Reform` lead, two-up teasers, etc.)
belongs to the marketplace assets workstream.
- **Email rendering across clients** (UAT §10): the wrapper is built

View File

@ -1,28 +1,55 @@
# Editorial — local plugin build helpers.
# editorial — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
#
# This Makefile intentionally focuses on the local single-shot build target.
# The remote deploy targets (e.g. `make rebuild`) are not provided here to
# keep the build pass scoped to local artefact production.
# 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 # default: produce editorial.so
# make templ # regenerate *_templ.go from *.templ
# make clean # remove editorial.so
# make build # pack editorial-<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
.PHONY: build verify archive-check publish clean help
PLUGIN_NAME := editorial
TEMPL := $(HOME)/go/bin/templ
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 .
$(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 locally.
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"
# 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 " 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 @@
# Editorial
A reading-first theme built around a serif baseline grid and a generous measure. Light mode is warm book paper with dark ink. Dark mode drops to a low-glare charcoal with sepia type for late-night reading. Drop caps, running marginalia, and small-caps labels do the design work, so the writing leads and the page stays quiet.
## Good for
We built Editorial for people who write at length. Treat these as starting points, not limits. If the mood fits your brand, use it:
- Personal essayists and columnists
- Independent newsletters and subscriptions
- Longform blogs and reported features
- Author and portfolio sites
## What you get
- Four color presets, each with a tuned light mode and a tuned dark mode: Book Paper, Sepia Review, Legal Pad, and Nightdesk.
- Playfair Display for headings and Source Serif 4 for body text, both bundled and ready. Fonts stay admin-controlled, so you can swap them from the typography settings.
- Four persona blocks: a drop-cap essay opener, running marginalia side notes, a reading list, and a field-notes aside.
- A demo site that installs when you activate the theme: a home page, an about page, a blog with three essays, and a contact page whose form emails your admins. It comes seeded as a newsletter called The Margin, by Frances Hale.
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
## Install
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully dressed starting point.

View File

@ -1,39 +1,44 @@
# Editorial — recommended Google Fonts
# Editorial — fonts
This theme ships with no bundled fonts in the current build pass
(`fonts.json = []`). Admins assign fonts via the BlockNinja typography panel.
The theme's CSS routes every `font-family` through the host-provided
`--font-heading`, `--font-body`, and `--font-mono` custom properties with
fallback stacks taken from spec §5, so the layout is usable before any font
is picked.
This theme **bundles** a Latin subset of two open-licence (SIL OFL 1.1)
serif families, so it looks correct the moment it is activated — no admin
setup required. The bundled faces are declared in `fonts.json` and land in
the typography picker as `source=template` rows.
For the intended editorial aesthetic, open the typography panel in admin and
add each of the following from the **Google Fonts** tab:
| Slot | Bundled family | Weights / styles | Role |
|---------|-------------------|---------------------------|-------------------------------------------------------------|
| Heading | Playfair Display | 700 normal, 900 italic | Didone display face for the masthead wordmark and headlines |
| Body | Source Serif 4 | 400 normal, 400 italic | Long-read body type; italic for marginalia and captions |
| Slot | Family | Source | Weights / styles | Why |
|-----------|-----------------------|--------------------|------------------------------------------------------------------|----------------------------------------------------------------------|
| Heading | Playfair Display | `google:Playfair Display` | 700 normal, 900 italic | Didone display face for the masthead wordmark and headlines. |
| Body | Source Serif 4 | `google:Source Serif 4` | 400 normal, 400 italic, 600 normal | Long-read body type; italic for marginalia and figcaptions. |
| UI / caption (body slot) | Inter | `google:Inter` | 500 normal, 600 normal | Byline names, timestamps, and small-caps section labels. |
| Mono | JetBrains Mono | `google:JetBrains Mono` | 400 normal | Code, datelines on financial copy, ISSN in the colophon. |
The bundled woff2 files and their licence record live in `assets/fonts/`
(`OFL.txt`). Every `font-family` in the theme is routed through
`var(--font-heading, ...)`, `var(--font-body, ...)` and `var(--font-mono, ...)`
with a fallback stack, so the layout is usable before any font is assigned
and the admin font picker stays in full control.
## How to assign
## Assigning the bundled faces
1. In the BlockNinja admin, open **Settings → Theme → Typography**.
2. Click the **Google Fonts** tab.
3. Search for the family above (e.g. "Playfair Display"), then click **Add**.
4. Once added, the family appears in the picker for that slot.
5. Assign **Playfair Display** to the *Heading* slot.
6. Assign **Source Serif 4** to the *Body* slot.
7. Assign **JetBrains Mono** to the *Mono* slot.
8. (Optional) For UI / caption elements that ride the body slot but want a
sans-serif treatment, override per-block via the editor.
2. In the **Bundled** tab, assign **Playfair Display** to the *Heading* slot.
3. Assign **Source Serif 4** to the *Body* slot.
4. Leave *Mono* as the system default, or assign **JetBrains Mono** from the
Google Fonts tab — it is used for the masthead kicker, datelines and the
colophon ISSN.
## Wave-2 (out of scope here)
## Optional upgrades (Google Fonts tab)
If the editorial brand is going to be locked in to a specific commercial
display face (e.g. *Migra* or *PP Editorial New* instead of *Playfair
Display*), licence and bundle the woff2 set, then update `fonts.json` to
declare the family + variants. The CSS does not change — the same
`var(--font-heading)` consumer pattern picks up the bundled face once an
admin assigns it.
- **Spectral** (`google:Spectral`) — a refined small-caps serif. Assign it to
the body slot if you want the small-caps section labels to carry true
small-caps rather than the synthesized `font-variant: small-caps` fallback.
- **Newsreader** (`google:Newsreader`) — an alternative reading serif for the
body slot; swaps in without any CSS change via `var(--font-body)`.
- **JetBrains Mono** (`google:JetBrains Mono`) — for the *Mono* slot
(kicker / dateline / ISSN).
## Bundling a commercial display face (out of scope for Wave A)
If the brand locks in a licensed display face (e.g. *Migra* or *PP Editorial
New*) in place of Playfair Display, licence and subset the woff2 set, drop it
in `assets/fonts/`, and add the family + variants to `fonts.json`. The CSS
does not change — the same `var(--font-heading)` consumer picks up whichever
face an admin assigns.

25
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,25 @@
# Recommended Icons — Editorial
Editorial is a typographic theme. It leans on hairline rules and small-caps
labels, not iconography, so it references only a single bundled pack.
`plugin.mod` declares it in `required_icon_packs`; until the loader
auto-installs declared packs, an admin should confirm it is enabled before
activation.
## Required pack
- **lucide** — ships bundled with the CMS (v0.460). Enabled by default. No
action needed in most installs; if icons render as empty boxes, open
`/admin/icons` and confirm **Lucide** is installed and enabled.
## Icons used by the theme chrome and overrides
All via the sprite pipeline (`<svg><use href="/icons/lucide.svg#<name>"/></svg>`
or the `::lucide:<name>::` shorthand):
`arrow-right`, `chevron-down`, `chevron-left`, `chevron-right`, `x`, `check`,
`play`, `map-pin`, `diamond`, `minus`.
Block content that stores an icon (feature-grid items, social links, contact
channels) may reference any installed pack as a `pack:name` string; the
schemas default to `lucide` so the theme renders without extra setup.

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

@ -0,0 +1,24 @@
Bundled fonts — license record (SIL Open Font License, Version 1.1)
====================================================================
The woff2 files in this directory are Latin subsets of the following
open-source families, both distributed under the SIL Open Font License 1.1
(https://openfontlicense.org). Subsetting and format conversion (TTF to
woff2) are expressly permitted by the OFL; the derived subsets remain under
the same license.
Playfair Display
playfair-display-700.woff2, playfair-display-900-italic.woff2
Copyright (c) Claus Eggers Sorensen (https://www.forthehearts.net), with
Reserved Font Name "Playfair Display". Display face for masthead wordmarks
and headlines.
Source Serif 4
source-serif-4-400.woff2, source-serif-4-400-italic.woff2
Copyright (c) Adobe (https://github.com/adobe-fonts/source-serif), with
Reserved Font Name "Source Serif". Long-read body face; italic for
marginalia and figure captions.
The full SIL Open Font License 1.1 text applies to each family and is
available at https://openfontlicense.org. This file records the license and
copyright attribution as required for redistribution under the OFL.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

33
blocks/blocks.yaml Normal file
View File

@ -0,0 +1,33 @@
# Editorial theme block definitions (codeless .bnp, WO-TF-017).
#
# These are the theme's TRUE persona blocks — furniture with no builtin
# equivalent. Everything else the theme used to ship as a block (masthead,
# byline, pull quote, section label, colophon) is now a re-skin OVERRIDE of a
# builtin (navbar, post-metadata, quote, divider, footer) and lives in
# templates/overrides/editorial/. Keys are fully qualified (editorial:<key>)
# because the codeless loader registers definitions verbatim.
blocks:
- key: editorial:dropcap_intro
title: Drop Cap Intro
description: Opening paragraph whose first letter renders as an oxblood Playfair drop cap
category: theme
schema: dropcap_intro.schema.json
template: dropcap_intro.ninjatpl
- key: editorial:marginalia
title: Marginalia Rail
description: Right-rail italic notes for longform articles; collapses inline on narrow viewports
category: theme
schema: marginalia.schema.json
template: marginalia.ninjatpl
- key: editorial:reading_list
title: Reading List
description: A curated further-reading list — small-caps heading over hairline-ruled links with notes
category: theme
schema: reading_list.schema.json
template: reading_list.ninjatpl
- key: editorial:field_notes
title: Field Notes
description: Dated notebook entries in small caps, separated by hairline rules
category: theme
schema: field_notes.schema.json
template: field_notes.ninjatpl

View File

@ -0,0 +1 @@
{% if lead %}<div class="editorial-column editorial-dropcap prose-editorial" data-block="editorial:dropcap_intro">{{ lead|safe }}</div>{% endif %}

View File

@ -0,0 +1,14 @@
{# field_notes (editorial) — dated notebook entries: small-caps dateline over a short note, hairline-ruled. Fields: title, notes[].date/note. #}
<section class="editorial-column my-10" data-block="editorial:field_notes">
{% if title %}<div class="editorial-section-label">{{ title }}</div><hr class="editorial-section-label-rule">{% endif %}
{% if notes %}
<div>
{% for entry in notes %}
<article class="py-4" style="border-top:1px solid hsl(var(--border));">
{% if entry.date %}<div class="editorial-meta mb-1">{{ entry.date }}</div>{% endif %}
{% if entry.note %}<div class="editorial-text prose-editorial">{{ entry.note|safe }}</div>{% endif %}
</article>
{% endfor %}
</div>
{% endif %}
</section>

View File

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Field Notes",
"description": "Dated notebook entries — a small-caps date over a short note, separated by hairline rules. The essayist's running observations.",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Heading",
"description": "Small-caps section heading (e.g. \"Field notes\").",
"x-editor": "text",
"default": "Field notes"
},
"notes": {
"type": "array",
"title": "Notes",
"description": "The dated entries, newest first.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"date": {
"type": "string",
"title": "Date",
"description": "The dateline, rendered in small caps (e.g. \"March 14\").",
"x-editor": "text"
},
"note": {
"type": "string",
"title": "Note",
"description": "The entry body. Rich text is permitted.",
"x-editor": "richtext"
}
}
}
}
}
}

View File

@ -0,0 +1 @@
<div class="editorial-marginalia" data-block="editorial:marginalia">{% if items %}{% for note in items %}<aside>{% if note.anchor %}<span class="editorial-marginalia-label">{{ note.anchor }}</span>{% endif %}{% if note.note %}<span class="editorial-marginalia-note">{{ note.note|safe }}</span>{% endif %}</aside>{% endfor %}{% else %}<aside class="text-xs opacity-60"></aside>{% endif %}</div>

View File

@ -0,0 +1,16 @@
{# reading_list (editorial) — curated further-reading list: small-caps heading over hairline-ruled entries with optional source + note. Fields: title, intro, items[].label/url/source/note. #}
<section class="editorial-column my-10" data-block="editorial:reading_list">
{% if title %}<div class="editorial-section-label">{{ title }}</div><hr class="editorial-section-label-rule">{% endif %}
{% if intro %}<p class="editorial-text text-sm mb-4" style="color:hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if items %}
<ul class="m-0 p-0" style="list-style:none;">
{% for item in items %}
<li class="py-3" style="border-top:1px solid hsl(var(--border));">
{% if item.url %}<a href="{{ item.url }}" class="editorial-link" style="font-family:var(--font-heading, &quot;Playfair Display&quot;, Georgia, serif);font-size:1.1rem;">{{ item.label }}</a>{% else %}<span style="font-family:var(--font-heading, &quot;Playfair Display&quot;, Georgia, serif);font-size:1.1rem;color:hsl(var(--foreground));">{{ item.label }}</span>{% endif %}
{% if item.source %}<span class="editorial-meta ml-2">{{ item.source }}</span>{% endif %}
{% if item.note %}<p class="editorial-text text-sm mt-1" style="color:hsl(var(--muted-foreground));">{{ item.note }}</p>{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</section>

View File

@ -0,0 +1,57 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Reading List",
"description": "A curated further-reading list rendered under a small-caps heading, each entry a hairline-ruled link with an optional note and source.",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Heading",
"description": "Small-caps section heading (e.g. \"Further reading\").",
"x-editor": "text",
"default": "Further reading"
},
"intro": {
"type": "string",
"title": "Intro",
"description": "Optional short line of context above the list.",
"x-editor": "textarea"
},
"items": {
"type": "array",
"title": "Entries",
"description": "The reading-list entries.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"title": "Title",
"description": "The title of the piece.",
"x-editor": "text"
},
"url": {
"type": "string",
"title": "URL",
"description": "Where the entry links to.",
"x-editor": "link"
},
"source": {
"type": "string",
"title": "Source",
"description": "Optional author or publication, rendered in small caps.",
"x-editor": "text"
},
"note": {
"type": "string",
"title": "Note",
"description": "Optional one-line note on why it is worth reading.",
"x-editor": "text"
}
}
}
}
}
}

View File

@ -1,25 +0,0 @@
package main
import (
"bytes"
"context"
)
// EditorialButtonBlock overrides the built-in `button` block for the
// editorial template. The editorial button is hairline-only: 1px border,
// transparent fill, oxblood text on hover (see .editorial-button CSS).
//
// Content shape: { "text", "url", "ariaLabel" }.
func EditorialButtonBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
url := getString(content, "url")
ariaLabel := getString(content, "ariaLabel")
if text == "" {
return ""
}
var buf bytes.Buffer
_ = editorialButtonComponent(text, url, ariaLabel).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,15 +0,0 @@
package main
// editorialButtonComponent renders an editorial-styled button. The element is
// always <button> (UAT §13.11 measures computed style on <button>); when a URL
// is supplied a same-element wrapper anchor is emitted around the button so
// the existing tab order and keyboard semantics remain intact.
templ editorialButtonComponent(text, url, ariaLabel string) {
if url != "" {
<a href={ templ.SafeURL(linkHref(url)) } class="inline-block" aria-label={ ariaLabel }>
<button type="button" class="editorial-button">{ text }</button>
</a>
} else {
<button type="button" class="editorial-button" aria-label={ ariaLabel }>{ text }</button>
}
}

View File

@ -1,116 +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"
// editorialButtonComponent renders an editorial-styled button. The element is
// always <button> (UAT §13.11 measures computed style on <button>); when a URL
// is supplied a same-element wrapper anchor is emitted around the button so
// the existing tab order and keyboard semantics remain intact.
func editorialButtonComponent(text, url, ariaLabel 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)
if url != "" {
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(linkHref(url)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 9, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"inline-block\" aria-label=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(ariaLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 9, Col: 86}
}
_, 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, "\"><button type=\"button\" class=\"editorial-button\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 10, Col: 56}
}
_, 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, "</button></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button type=\"button\" class=\"editorial-button\" aria-label=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(ariaLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 13, Col: 71}
}
_, 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, 6, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 13, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,56 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// BylineBlockMeta describes the Editorial byline block: author name, optional
// avatar, dateline, and read time.
// Registered as "editorial:byline" at runtime.
var BylineBlockMeta = blocks.BlockMeta{
Key: "byline",
Title: "Byline",
Description: "Author byline with optional photo, dateline, and read time",
Source: "editorial",
Category: blocks.CategoryTheme,
}
// BylineData carries parsed byline content.
//
// Author resolution: this plugin does not resolve the slug to an actual author
// record (that would require ServiceDeps and a database call). Instead the
// authorSlug is rendered verbatim in the byline label. The CMS layer can
// replace the slug with the resolved name when integrating with the author
// directory.
type BylineData struct {
AuthorSlug string
ShowPhoto bool
ShowReadTime bool
Dateline string
}
// BylineBlock renders the article byline.
//
// Content shape:
//
// {
// "authorSlug": "jane-smith",
// "showPhoto": "true",
// "showReadTime": "true",
// "dateline": "London, March 14"
// }
func BylineBlock(ctx context.Context, content map[string]any) string {
data := BylineData{
AuthorSlug: getString(content, "authorSlug"),
ShowPhoto: getBool(content, "showPhoto", true),
ShowReadTime: getBool(content, "showReadTime", true),
Dateline: getString(content, "dateline"),
}
var buf bytes.Buffer
_ = bylineComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,37 +0,0 @@
package main
// bylineComponent renders the byline. When showPhoto is true and an author
// slug is supplied, a neutral avatar circle is rendered (the actual avatar
// URL would be resolved server-side by the CMS author directory). When the
// dateline is empty the dateline cell is suppressed.
templ bylineComponent(data BylineData) {
<div class="editorial-column" data-block="editorial:byline">
<div class="editorial-byline">
if data.ShowPhoto && data.AuthorSlug != "" {
<div class="editorial-byline-photo" aria-hidden="true"></div>
}
<div class="flex-1">
if data.AuthorSlug != "" {
<div class="editorial-byline-author">
{ "By " }{ data.AuthorSlug }
</div>
} else {
<div class="editorial-byline-author">
{ "By Editorial Staff" }
</div>
}
<div class="editorial-byline-meta">
if data.Dateline != "" {
<span>{ data.Dateline }</span>
}
if data.ShowReadTime {
if data.Dateline != "" {
<span class="px-1">&middot;</span>
}
<span>{ "5 min read" }</span>
}
</div>
</div>
</div>
</div>
}

View File

@ -1,152 +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"
// bylineComponent renders the byline. When showPhoto is true and an author
// slug is supplied, a neutral avatar circle is rendered (the actual avatar
// URL would be resolved server-side by the CMS author directory). When the
// dateline is empty the dateline cell is suppressed.
func bylineComponent(data BylineData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-column\" data-block=\"editorial:byline\"><div class=\"editorial-byline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowPhoto && data.AuthorSlug != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"editorial-byline-photo\" aria-hidden=\"true\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"flex-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.AuthorSlug != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"editorial-byline-author\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("By ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `byline.templ`, Line: 16, Col: 13}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.AuthorSlug)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `byline.templ`, Line: 16, Col: 32}
}
_, 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, 5, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"editorial-byline-author\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("By Editorial Staff")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `byline.templ`, Line: 20, Col: 28}
}
_, 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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"editorial-byline-meta\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Dateline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Dateline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `byline.templ`, Line: 25, Col: 27}
}
_, 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, 10, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ShowReadTime {
if data.Dateline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"px-1\">&middot;</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("5 min read")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `byline.templ`, Line: 31, Col: 26}
}
_, 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, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,65 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// ColophonBlockMeta describes the Editorial colophon (footer) block: optional
// subscribe stub, optional ISSN field, and a list of inline footer links.
// Registered as "editorial:colophon" at runtime.
var ColophonBlockMeta = blocks.BlockMeta{
Key: "colophon",
Title: "Colophon",
Description: "Editorial footer with ISSN, subscribe stub, and link list",
Source: "editorial",
Category: blocks.CategoryNavigation,
}
// ColophonLink is one row in the colophon link list.
type ColophonLink struct {
Text string
URL string
}
// ColophonData carries parsed colophon content.
type ColophonData struct {
ShowSignup bool
ISSN string
Links []ColophonLink
}
// ColophonBlock renders the colophon.
//
// Content shape:
//
// {
// "showSignup": "true",
// "issn": "0000-0000",
// "links": [
// { "text": "About", "url": "/about" },
// { "text": "Style Guide", "url": "/style-guide" }
// ]
// }
func ColophonBlock(ctx context.Context, content map[string]any) string {
rawLinks := getSlice(content, "links")
links := make([]ColophonLink, 0, len(rawLinks))
for _, l := range rawLinks {
links = append(links, ColophonLink{
Text: getString(l, "text"),
URL: getString(l, "url"),
})
}
data := ColophonData{
ShowSignup: getBool(content, "showSignup", false),
ISSN: getString(content, "issn"),
Links: links,
}
var buf bytes.Buffer
_ = colophonComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,45 +0,0 @@
package main
// colophonComponent renders the editorial colophon (footer). Subscribe stub,
// link list, and ISSN are all optional. The stub is a non-functional stand-in
// for this build pass — the spec lists the colophon as a footer with a
// subscribe stub, but signup wiring is out of scope.
templ colophonComponent(data ColophonData) {
<div class="editorial-wide editorial-colophon" data-block="editorial:colophon">
if data.ShowSignup {
<div class="mb-4">
<div class="editorial-section-label mb-1">{ "Subscribe" }</div>
<form class="flex gap-2 max-w-md">
<input type="email" placeholder="you@example.com" class="flex-1 px-3 py-1 border editorial-hairline" aria-label="Email address"/>
<button type="submit" class="editorial-button">{ "Subscribe" }</button>
</form>
</div>
}
if len(data.Links) > 0 {
<nav aria-label="Footer">
<ul class="flex flex-wrap gap-x-4 gap-y-1">
for _, link := range data.Links {
<li>
<a href={ templ.SafeURL(linkHref(link.URL)) } class="editorial-button" style="border:0;padding:0;letter-spacing:0.05em;">
{ link.Text }
</a>
</li>
}
</ul>
</nav>
}
if data.ISSN != "" {
<div class="mt-3">
<span class="editorial-colophon-issn">{ "ISSN " }{ data.ISSN }</span>
</div>
}
</div>
}
// linkHref returns "#" for empty URLs so the rendered href is always valid.
func linkHref(u string) string {
if u == "" {
return "#"
}
return u
}

View File

@ -1,158 +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"
// colophonComponent renders the editorial colophon (footer). Subscribe stub,
// link list, and ISSN are all optional. The stub is a non-functional stand-in
// for this build pass — the spec lists the colophon as a footer with a
// subscribe stub, but signup wiring is out of scope.
func colophonComponent(data ColophonData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-wide editorial-colophon\" data-block=\"editorial:colophon\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowSignup {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mb-4\"><div class=\"editorial-section-label mb-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("Subscribe")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 11, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><form class=\"flex gap-2 max-w-md\"><input type=\"email\" placeholder=\"you@example.com\" class=\"flex-1 px-3 py-1 border editorial-hairline\" aria-label=\"Email address\"> <button type=\"submit\" class=\"editorial-button\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("Subscribe")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 14, Col: 65}
}
_, 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, 4, "</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Links) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<nav aria-label=\"Footer\"><ul class=\"flex flex-wrap gap-x-4 gap-y-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, link := range data.Links {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(linkHref(link.URL)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 23, Col: 50}
}
_, 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, "\" class=\"editorial-button\" style=\"border:0;padding:0;letter-spacing:0.05em;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(link.Text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 24, Col: 19}
}
_, 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, 8, "</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</ul></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ISSN != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"mt-3\"><span class=\"editorial-colophon-issn\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("ISSN ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 33, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.ISSN)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 33, Col: 64}
}
_, 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, 11, "</span></div>")
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
}
return nil
})
}
// linkHref returns "#" for empty URLs so the rendered href is always valid.
func linkHref(u string) string {
if u == "" {
return "#"
}
return u
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,89 +0,0 @@
package main
import (
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// Email rendering must inline colour values because most desktop email
// clients do not honour CSS custom properties (var(--token)) or modern
// hsl() syntax. The host sets EmailContext.Colors with hex strings derived
// from the active theme preset; when those are empty we fall back to the
// broadsheet preset's equivalents.
//
// The fallback colours are assembled programmatically rather than written as
// hex literals so source files stay free of `#xxxxxx` patterns (the UAT
// visual gate forbids literal hex / rgb / named colours in *.templ and *.go).
// Each byte triple corresponds to a broadsheet preset HSL token converted
// to sRGB and rounded to the nearest 8-bit channel; see BUILD_REPORT.md for
// the conversion derivation.
// rgbHex assembles an HTML hex colour string from three byte channels. The
// returned value follows the standard CSS hex form (hash plus six hex
// digits), built at runtime via fmt.Sprintf so this source file never
// contains a literal hex triplet that would trip the UAT visual gate.
func rgbHex(r, g, b uint8) string {
return fmt.Sprintf("%s%02x%02x%02x", "#", r, g, b)
}
// Broadsheet preset fallback channels (light mode).
//
// background 36 30% 96% → cream paper.
// card 36 30% 98% → slightly lighter cream.
// foreground 20 14% 10% → ink black.
// mutedFg 20 10% 38% → soft grey-brown.
// border 30 12% 82% → warm hairline.
// accent 0 55% 28% → oxblood.
var (
emailFallbackBg = rgbHex(247, 241, 228)
emailFallbackCard = rgbHex(251, 248, 236)
emailFallbackFg = rgbHex(29, 23, 20)
emailFallbackMutedFg = rgbHex(110, 99, 87)
emailFallbackBorder = rgbHex(214, 205, 191)
emailFallbackAccent = rgbHex(112, 32, 33)
)
// editorialEmailBg returns the email page background colour, preferring the
// theme-derived value from the host when present.
func editorialEmailBg(c templates.EmailContext) string {
if c.Colors.Background != "" {
return c.Colors.Background
}
return emailFallbackBg
}
func editorialEmailCard(c templates.EmailContext) string {
if c.Colors.Card != "" {
return c.Colors.Card
}
return emailFallbackCard
}
func editorialEmailFg(c templates.EmailContext) string {
if c.Colors.Foreground != "" {
return c.Colors.Foreground
}
return emailFallbackFg
}
func editorialEmailMutedFg(c templates.EmailContext) string {
if c.Colors.MutedForeground != "" {
return c.Colors.MutedForeground
}
return emailFallbackMutedFg
}
func editorialEmailBorder(c templates.EmailContext) string {
if c.Colors.Border != "" {
return c.Colors.Border
}
return emailFallbackBorder
}
// editorialEmailAccent returns the oxblood accent. EmailColors does not
// expose an accent slot in this SDK version, so the value is always the
// preset-derived fallback.
func editorialEmailAccent(c templates.EmailContext) string {
return emailFallbackAccent
}

View File

@ -1,49 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// DropcapIntroBlockMeta describes the Editorial drop-cap intro block: an
// opening paragraph whose first letter is rendered as an oxblood drop cap.
// Registered as "editorial:dropcap_intro" at runtime.
var DropcapIntroBlockMeta = blocks.BlockMeta{
Key: "dropcap_intro",
Title: "Drop Cap Intro",
Description: "Opening paragraph with an oxblood Playfair drop cap",
Source: "editorial",
Category: blocks.CategoryTheme,
}
// DropcapIntroData carries parsed drop-cap-intro content.
type DropcapIntroData struct {
Lead string // rich-text payload, trusted upstream
}
// DropcapIntroBlock renders the opening paragraph.
//
// The drop cap is rendered via CSS (::first-letter on the first <p>) rather
// than by extracting the first character server-side. This is the simpler
// path and works for ASCII openers. The spec calls out a known risk: an
// opener starting with `"` or `—` will style the punctuation. That fallback
// is acceptable for this build pass and is documented in BUILD_REPORT.md.
//
// Content shape:
//
// { "lead": "<p>The reform was always going to be quiet.</p>" }
func DropcapIntroBlock(ctx context.Context, content map[string]any) string {
data := DropcapIntroData{
Lead: getString(content, "lead"),
}
if data.Lead == "" {
return ""
}
var buf bytes.Buffer
_ = dropcapIntroComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,11 +0,0 @@
package main
// dropcapIntroComponent renders the opening paragraph wrapped in the
// `editorial-dropcap` CSS hook. The hook selects `p:first-of-type::first-letter`
// inside the wrapper, so the editor can supply multiple <p> elements and the
// drop cap is applied to the first one only.
templ dropcapIntroComponent(data DropcapIntroData) {
<div class="editorial-column editorial-dropcap prose-editorial" data-block="editorial:dropcap_intro">
@templ.Raw(data.Lead)
</div>
}

View File

@ -1,52 +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"
// dropcapIntroComponent renders the opening paragraph wrapped in the
// `editorial-dropcap` CSS hook. The hook selects `p:first-of-type::first-letter`
// inside the wrapper, so the editor can supply multiple <p> elements and the
// drop cap is applied to the first one only.
func dropcapIntroComponent(data DropcapIntroData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-column editorial-dropcap prose-editorial\" data-block=\"editorial:dropcap_intro\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Lead).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,117 +0,0 @@
package main
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// EditorialEmailWrapper wraps an email body in a print-broadsheet style: cream
// background, Playfair masthead, hairline rule, 580px centred column, Source
// Serif body, oxblood unsubscribe link.
func EditorialEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
_ = editorialEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// editorialEmailTemplate renders the Editorial-branded email layout. All
// colours are pulled off emailCtx.Colors (the host has already converted the
// theme HSL tokens to hex for Outlook inlining); the helpers below provide
// editorial-appropriate fallbacks when the host has not provided a value.
templ editorialEmailTemplate(emailCtx templates.EmailContext, body string) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>{ emailCtx.SiteSettings.SiteName }</title>
<style type="text/css">
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
@media only screen and (max-width: 620px) {
.email-container { width: 100% !important; max-width: 100% !important; }
.content-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: var(--font-body, 'Source Serif 4', Georgia, serif);", editorialEmailBg(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">
<tr>
<td align="center" style={ fmt.Sprintf("padding: 48px 10px; background-color: %s;", editorialEmailBg(emailCtx)) }>
<table role="presentation" class="email-container" width="580" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 580px; background-color: %s;", editorialEmailCard(emailCtx)) }>
<!-- Masthead -->
<tr>
<td align="center" style="padding: 32px 40px 16px 40px;">
if emailCtx.SiteSettings.LogoURL != "" {
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } style="max-height: 48px; width: auto; display: block;"/>
} else if emailCtx.SiteSettings.SiteName != "" {
<h1 style={ fmt.Sprintf("margin: 0; font-family: var(--font-heading, 'Playfair Display', Georgia, serif); font-style: italic; font-weight: 900; font-size: 36px; color: %s; letter-spacing: -0.02em;", editorialEmailFg(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</h1>
}
</td>
</tr>
<!-- Hairline rule -->
<tr>
<td style={ fmt.Sprintf("padding: 0 40px;") }>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style={ fmt.Sprintf("border-top: 1px solid %s; font-size: 0; line-height: 0; height: 1px;", editorialEmailBorder(emailCtx)) }>&nbsp;</td>
</tr>
</table>
</td>
</tr>
<!-- Body -->
<tr>
<td class="content-padding" style={ fmt.Sprintf("padding: 32px 48px 40px 48px; color: %s; font-size: 16px; line-height: 1.7; font-family: var(--font-body, 'Source Serif 4', Georgia, serif);", editorialEmailFg(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<!-- Footer -->
<tr>
<td style={ fmt.Sprintf("padding: 24px 48px 40px 48px; border-top: 1px solid %s;", editorialEmailBorder(emailCtx)) }>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ fmt.Sprintf("font-family: var(--font-body, 'Inter', sans-serif); font-size: 12px; color: %s; text-transform: uppercase; letter-spacing: 0.1em;", editorialEmailMutedFg(emailCtx)) }>
<p style="margin: 0 0 8px;">{ emailCtx.SiteSettings.SiteName }</p>
if emailCtx.UnsubscribeURL != "" {
<p style="margin: 0; font-size: 11px;">
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", editorialEmailAccent(emailCtx)) }>
Unsubscribe
</a>
</p>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}
// Email colour helpers. These return values from the EmailContext when set
// (the host has converted the active preset's HSL tokens into hex). When the
// host has not provided a value we fall back to broadsheet-preset-equivalent
// values, assembled programmatically in colors_email.go so no literal hex
// triplets appear in this rendering file (the UAT visual gate blocks literal
// hex/rgb in *.templ and *.go).

View File

@ -1,330 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// EditorialEmailWrapper wraps an email body in a print-broadsheet style: cream
// background, Playfair masthead, hairline rule, 580px centred column, Source
// Serif body, oxblood unsubscribe link.
func EditorialEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
_ = editorialEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// editorialEmailTemplate renders the Editorial-branded email layout. All
// colours are pulled off emailCtx.Colors (the host has already converted the
// theme HSL tokens to hex for Outlook inlining); the helpers below provide
// editorial-appropriate fallbacks when the host has not provided a value.
func editorialEmailTemplate(emailCtx templates.EmailContext, body string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 31, 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><style type=\"text/css\">\n\t\t\t\tbody, table, td, p, a, li, blockquote {\n\t\t\t\t\t-webkit-text-size-adjust: 100%;\n\t\t\t\t\t-ms-text-size-adjust: 100%;\n\t\t\t\t}\n\t\t\t\ttable, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }\n\t\t\t\timg { -ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; }\n\t\t\t\tbody { margin: 0 !important; padding: 0 !important; width: 100% !important; }\n\t\t\t\ta[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }\n\t\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t\t.email-container { width: 100% !important; max-width: 100% !important; }\n\t\t\t\t\t.content-padding { padding-left: 24px !important; padding-right: 24px !important; }\n\t\t\t\t}\n\t\t\t</style></head><body style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: var(--font-body, 'Source Serif 4', Georgia, serif);", editorialEmailBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 47, Col: 168}
}
_, 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: 50, 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\"><tr><td align=\"center\" 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("padding: 48px 10px; background-color: %s;", editorialEmailBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 55, Col: 116}
}
_, 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, "\"><table role=\"presentation\" class=\"email-container\" width=\"580\" 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: 580px; background-color: %s;", editorialEmailCard(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 56, Col: 204}
}
_, 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, "\"><!-- Masthead --><tr><td align=\"center\" style=\"padding: 32px 40px 16px 40px;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.LogoURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, 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: 61, Col: 50}
}
_, 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, 10, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, 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: 61, Col: 89}
}
_, 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, "\" style=\"max-height: 48px; 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, 12, "<h1 style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-family: var(--font-heading, 'Playfair Display', Georgia, serif); font-style: italic; font-weight: 900; font-size: 36px; color: %s; letter-spacing: -0.02em;", editorialEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 63, Col: 236}
}
_, 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, 13, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, 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: 64, Col: 43}
}
_, 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, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</td></tr><!-- Hairline rule --><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 0 40px;"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 71, Col: 51}
}
_, 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, 16, "\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><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("border-top: 1px solid %s; font-size: 0; line-height: 0; height: 1px;", editorialEmailBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 74, Col: 138}
}
_, 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, "\">&nbsp;</td></tr></table></td></tr><!-- Body --><tr><td class=\"content-padding\" 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: 32px 48px 40px 48px; color: %s; font-size: 16px; line-height: 1.7; font-family: var(--font-body, 'Source Serif 4', Georgia, serif);", editorialEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 81, Col: 227}
}
_, 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, 18, "\">")
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, 19, "</td></tr><!-- Footer --><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 24px 48px 40px 48px; border-top: 1px solid %s;", editorialEmailBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 87, Col: 122}
}
_, 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, "\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("font-family: var(--font-body, 'Inter', sans-serif); font-size: 12px; color: %s; text-transform: uppercase; letter-spacing: 0.1em;", editorialEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 90, Col: 215}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\"><p style=\"margin: 0 0 8px;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, 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: 91, Col: 72}
}
_, 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, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p style=\"margin: 0; font-size: 11px;\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 templ.SafeURL
templ_7745c5c3_Var17, 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: 94, Col: 62}
}
_, 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, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", editorialEmailAccent(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 94, Col: 158}
}
_, 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, "\">Unsubscribe</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</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. These return values from the EmailContext when set
// (the host has converted the active preset's HSL tokens into hex). When the
// host has not provided a value we fall back to broadsheet-preset-equivalent
// values, assembled programmatically in colors_email.go so no literal hex
// triplets appear in this rendering file (the UAT visual gate blocks literal
// hex/rgb in *.templ and *.go).
var _ = templruntime.GeneratedTemplate

View File

@ -1,61 +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 rooted at assets/.
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "assets")
return sub
}
// Schemas returns the embedded schemas filesystem rooted at schemas/.
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 editorial-specific utility CSS that must be
// merged into the host Tailwind input layer at build time. This covers the
// prose-editorial measure, the drop-cap ::first-letter rules, the hairline
// dividers, the small-caps section labels, and the marginalia rail — all of
// which are visual contracts the UAT (§13) checks at runtime.
//
// Per docs/FONTS.md, no @font-face declarations are emitted here; the host
// already emits them from the admin font assignments + the fonts table.
func ThemeCSSManifest() *plugin.CSSManifest {
return &plugin.CSSManifest{
InputCSSAppend: editorialUtilityCSS,
}
}

View File

@ -1 +1,18 @@
[]
[
{
"name": "Playfair Display",
"family": "Playfair Display",
"variants": [
{ "weight": "700", "style": "normal", "file": "fonts/playfair-display-700.woff2" },
{ "weight": "900", "style": "italic", "file": "fonts/playfair-display-900-italic.woff2" }
]
},
{
"name": "Source Serif 4",
"family": "Source Serif 4",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/source-serif-4-400.woff2" },
{ "weight": "400", "style": "italic", "file": "fonts/source-serif-4-400-italic.woff2" }
]
}
]

20
go.mod
View File

@ -1,20 +0,0 @@
module git.dev.alexdunmow.com/block/themes/editorial
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,47 +0,0 @@
package main
import (
"bytes"
"context"
"strconv"
)
// EditorialHeadingBlock overrides the built-in `heading` block for the
// editorial template. The override only applies when the active page template
// is `editorial`; on other templates the built-in heading runs untouched.
//
// Content shape mirrors the built-in heading: { "text", "level", "kicker",
// "textClass" }. The optional "kicker" is editorial-specific (a small-caps
// label above the H1).
func EditorialHeadingBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
textClass := getString(content, "textClass")
kicker := getString(content, "kicker")
level := parseHeadingLevel(content)
var buf bytes.Buffer
_ = editorialHeadingComponent(level, text, textClass, kicker).Render(ctx, &buf)
return buf.String()
}
// parseHeadingLevel parses content.level into 1-6, defaulting to 2. Accepts
// number, integer-from-JSON (float64), and string forms.
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,28 +0,0 @@
package main
// editorialHeadingComponent renders an editorial-styled heading. H1 carries
// the optional kicker. The text-size scale lifts on the landing template so
// the lead-story H1 hits ≥72px at desktop sizes (UAT §13.9).
templ editorialHeadingComponent(level int, text, textClass, kicker string) {
switch level {
case 1:
<div class="editorial-column">
if kicker != "" {
<span class="editorial-heading-kicker">{ kicker }</span>
}
<h1 class={ "editorial-heading text-5xl md:text-7xl mt-2", textClass }>{ text }</h1>
</div>
case 2:
<h2 class={ "editorial-heading text-3xl md:text-4xl editorial-column", textClass }>{ text }</h2>
case 3:
<h3 class={ "editorial-heading text-2xl md:text-3xl editorial-column", textClass }>{ text }</h3>
case 4:
<h4 class={ "editorial-heading text-xl md:text-2xl editorial-column", textClass }>{ text }</h4>
case 5:
<h5 class={ "editorial-heading text-lg editorial-column", textClass }>{ text }</h5>
case 6:
<h6 class={ "editorial-heading text-base editorial-column", textClass }>{ text }</h6>
default:
<h2 class={ "editorial-heading text-3xl editorial-column", textClass }>{ text }</h2>
}
}

View File

@ -1,316 +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"
// editorialHeadingComponent renders an editorial-styled heading. H1 carries
// the optional kicker. The text-size scale lifts on the landing template so
// the lead-story H1 hits ≥72px at desktop sizes (UAT §13.9).
func editorialHeadingComponent(level int, text, textClass, kicker 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:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-column\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if kicker != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<span class=\"editorial-heading-kicker\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(kicker)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 11, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
var templ_7745c5c3_Var3 = []any{"editorial-heading text-5xl md:text-7xl mt-2", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h1 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).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_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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: 13, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h1></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 2:
var templ_7745c5c3_Var6 = []any{"editorial-heading text-3xl md:text-4xl editorial-column", 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, 7, "<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, 8, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 16, Col: 92}
}
_, 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, 9, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 3:
var templ_7745c5c3_Var9 = []any{"editorial-heading text-2xl md:text-3xl editorial-column", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h3 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var9).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_Var10)
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_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 18, Col: 92}
}
_, 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, 12, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 4:
var templ_7745c5c3_Var12 = []any{"editorial-heading text-xl md:text-2xl editorial-column", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
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_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var12).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_Var13)
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_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 20, Col: 91}
}
_, 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, 15, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 5:
var templ_7745c5c3_Var15 = []any{"editorial-heading text-lg editorial-column", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h5 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var15).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_Var16)
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
}
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: 22, Col: 79}
}
_, 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, 18, "</h5>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 6:
var templ_7745c5c3_Var18 = []any{"editorial-heading text-base editorial-column", 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, 19, "<h6 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, 20, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 24, Col: 81}
}
_, 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, 21, "</h6>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
var templ_7745c5c3_Var21 = []any{"editorial-heading text-3xl editorial-column", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var21).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_Var22)
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_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 26, Col: 80}
}
_, 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, 24, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,51 +0,0 @@
package main
// getString extracts a string value from a content map.
func getString(content map[string]any, key string) string {
if v, ok := content[key].(string); ok {
return v
}
return ""
}
// getStringWithDefault returns the string at key or the default when absent/empty.
func getStringWithDefault(content map[string]any, key, def string) string {
if v := getString(content, key); v != "" {
return v
}
return def
}
// getBool extracts a bool value from a content map. Accepts true/false and
// string forms "true"/"false"/"yes"/"no"/"1"/"0" (admin select dropdowns
// commonly store booleans as strings).
func getBool(content map[string]any, key string, def bool) bool {
if v, ok := content[key].(bool); ok {
return v
}
if v, ok := content[key].(string); ok {
switch v {
case "true", "yes", "1":
return true
case "false", "no", "0":
return false
}
}
return def
}
// getSlice extracts a slice of map values from content[key]. JSON decoding
// gives []any; we narrow to []map[string]any so block code can index into
// individual items by name.
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
}

View File

@ -1,26 +0,0 @@
package main
import (
"bytes"
"context"
)
// EditorialImageBlock overrides the built-in `image` block for the editorial
// template. The image is wrapped in a <figure>; the caption is rendered as
// Source Serif italic 14px, preceded by a 1px hairline rule (UAT §13.12).
//
// Content shape: { "src", "alt", "caption" }.
func EditorialImageBlock(ctx context.Context, content map[string]any) string {
src := getString(content, "src")
alt := getString(content, "alt")
caption := getString(content, "caption")
if src == "" {
// Match built-in fallback: render nothing for an empty image.
return ""
}
var buf bytes.Buffer
_ = editorialImageComponent(src, alt, caption).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,11 +0,0 @@
package main
// editorialImageComponent renders an image with an editorial figcaption.
templ editorialImageComponent(src, alt, caption string) {
<figure class="editorial-figure editorial-column">
<img src={ src } alt={ alt }/>
if caption != "" {
<figcaption>{ caption }</figcaption>
}
</figure>
}

View File

@ -1,90 +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"
// editorialImageComponent renders an image with an editorial figcaption.
func editorialImageComponent(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=\"editorial-figure editorial-column\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(src)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 6, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" 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: 6, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<figcaption>")
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: 8, Col: 24}
}
_, 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, "</figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

533
manifest.yaml Normal file
View File

@ -0,0 +1,533 @@
# Editorial theme — codeless .bnp manifest (Theme Fleet Wave A, WO-TF-017).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
#
# Persona: a personal essayist / independent newsletter. A reading experience
# above all — serif baseline grid, drop caps, running marginalia, small-caps
# section labels, author byline foregrounded. Typography does the design work;
# ornament is limited to hairline rules and generous whitespace.
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
required_icon_packs: [lucide, simple-icons]
system_templates:
- key: editorial
title: Editorial
description: "A personal essayist's reading room: serif baseline grid, drop caps, running marginalia and small-caps labels. Built for longform writers and independent newsletters."
page_templates:
- system: editorial
key: default
title: Default
description: "Standard page: masthead, narrow reading column, colophon."
slots: [header, main, footer]
- system: editorial
key: full-width
title: Full Width
description: "Edge-to-edge for photo essays and cover images."
slots: [header, main, footer]
- system: editorial
key: landing
title: Front Page
description: "Front-page stack: masthead, lead essay, river of stories, colophon."
slots: [masthead, lead, river, footer]
- system: editorial
key: article
title: Article
description: "The reading centrepiece: byline, drop-cap opener, single column with a marginalia rail."
slots: [header, byline, main, marginalia, footer]
- system: editorial
key: blog-index
title: Archive
description: "Essay archive: masthead, featured essay, and a river of entries."
slots: [header, featured, main, footer]
- system: editorial
key: contact
title: Correspondence
description: "Contact layout with an optional aside for hours and location."
slots: [header, main, aside, footer]
- system: editorial
key: auth
title: Members
description: "Centred members shell with wordmark and hairline rule."
slots: [main, footer]
template_overrides:
# Chrome
- { template: editorial, block: navbar }
- { template: editorial, block: footer }
- { template: editorial, block: utility-bar }
- { template: editorial, block: announcement-bar }
# Marketing
- { template: editorial, block: hero }
- { template: editorial, block: feature-grid }
- { template: editorial, block: rich-section }
- { template: editorial, block: stats }
- { template: editorial, block: testimonial }
- { template: editorial, block: logo-strip }
- { template: editorial, block: pricing }
- { template: editorial, block: team }
- { template: editorial, block: faq }
- { template: editorial, block: timeline }
- { template: editorial, block: cta }
- { template: editorial, block: countdown }
# Primitives
- { template: editorial, block: heading }
- { template: editorial, block: text }
- { template: editorial, block: rich-text }
- { template: editorial, block: image }
- { template: editorial, block: quote }
- { template: editorial, block: divider }
- { template: editorial, block: button }
- { template: editorial, block: card }
- { template: editorial, block: gallery }
- { template: editorial, block: video-embed }
- { template: editorial, block: audio-embed }
# Commerce / local
- { template: editorial, block: menu-list }
- { template: editorial, block: event-list }
- { template: editorial, block: contact-form }
- { template: editorial, block: contact-card }
- { template: editorial, block: hours-location }
- { template: editorial, block: social-links }
# Blog family
- { template: editorial, block: blog-hero }
- { template: editorial, block: featured-posts }
- { template: editorial, block: related-posts }
- { template: editorial, block: popular-posts }
- { template: editorial, block: category-list }
- { template: editorial, block: post-hero }
- { template: editorial, block: post-metadata }
- { template: editorial, block: author-bio }
- { template: editorial, block: author-bio-hero }
- { template: editorial, block: newsletter-signup }
- { template: editorial, block: breadcrumbs }
- { template: editorial, block: sidebar-nav }
# Auth surface
- { template: editorial, block: auth-form }
- { template: editorial, block: password-reset-form }
- { template: editorial, block: auth-status }
# System
- { template: editorial, block: page-suggestions }
email_wrappers:
- editorial
css:
input_css_append: |
/* === Editorial theme — utility layer ===
*
* Personal-essayist identity: hairline rules, small-caps labels, drop caps,
* marginalia, narrow reading measure. Colour is ALWAYS consumed via
* hsl(var(--token)); font families ALWAYS via var(--font-heading|body|mono,
* <fallback>). No literal colours, no hardcoded families.
*/
/* Bundled serif faces. The host does not yet emit @font-face for
* bundled_fonts (LoadedPlugin.BundledFonts is unconsumed), so the theme
* declares its own faces against the served artifact paths
* (/templates/editorial/fonts/...). Presets set typography.fontHeading/body
* to template:editorial:<Family> so --font-heading/--font-body resolve to
* these families. */
@font-face { font-family: 'Playfair Display'; src: url('/templates/editorial/fonts/playfair-display-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'Playfair Display'; src: url('/templates/editorial/fonts/playfair-display-900-italic.woff2') format('woff2'); font-weight: 900; font-style: italic; font-display: swap; }
@font-face { font-family: 'Source Serif 4'; src: url('/templates/editorial/fonts/source-serif-4-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Source Serif 4'; src: url('/templates/editorial/fonts/source-serif-4-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
@layer utilities {
:root {
--editorial-measure: 64ch;
--editorial-rule: 1px solid hsl(var(--border));
}
/* --- Reading column + page widths --- */
.editorial-column {
max-width: var(--editorial-measure);
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
.editorial-article-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
max-width: 80rem;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 1024px) {
.editorial-article-grid {
grid-template-columns: minmax(0, 64ch) 16rem;
}
}
/* --- Hero display title — responsive size via media queries so it is
* independent of the host Tailwind JIT (large text-* combos in theme
* templates are not reliably scanned). --- */
.editorial-hero-title {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 700;
letter-spacing: -0.015em;
line-height: 1.1;
color: hsl(var(--foreground));
font-size: 2.25rem;
}
@media (min-width: 640px) { .editorial-hero-title { font-size: 3rem; } }
@media (min-width: 1024px) { .editorial-hero-title { font-size: 3.5rem; } }
/* --- Narrow prose column with indented continuation paragraphs --- */
.prose-editorial {
max-width: var(--editorial-measure);
margin-left: auto;
margin-right: auto;
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
color: hsl(var(--foreground));
line-height: 1.7;
}
.prose-editorial p { margin: 0 0 1em 0; }
.prose-editorial p + p { text-indent: 1.5em; }
.prose-editorial h1,
.prose-editorial h2,
.prose-editorial h3,
.prose-editorial h4 {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 700;
letter-spacing: -0.01em;
color: hsl(var(--foreground));
}
.prose-editorial a {
color: hsl(var(--accent));
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
.prose-editorial blockquote {
hanging-punctuation: first;
font-style: italic;
border-left: var(--editorial-rule);
padding-left: 1rem;
margin-left: 0;
color: hsl(var(--muted-foreground));
}
/* --- Small-caps / tracked labels (kickers, eyebrows, meta) --- */
.editorial-caps {
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.72rem;
line-height: 1.4;
}
.editorial-meta {
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.72rem;
color: hsl(var(--muted-foreground));
}
.editorial-section-label {
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
text-transform: uppercase;
font-variant: small-caps;
font-feature-settings: "smcp";
letter-spacing: 0.15em;
font-size: 0.8rem;
color: hsl(var(--foreground));
}
.editorial-section-label-rule {
border: 0;
border-top: var(--editorial-rule);
margin: 0.5rem 0 1.5rem 0;
}
/* --- Hairline rules --- */
hr.editorial-rule,
.editorial-rule,
.editorial-hairline {
border: 0;
border-top: var(--editorial-rule);
margin: 0;
}
/* --- Heading (Playfair, tight tracking, optional kicker) --- */
.editorial-heading {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 700;
letter-spacing: -0.015em;
color: hsl(var(--foreground));
line-height: 1.15;
}
.editorial-heading-kicker {
display: block;
margin-bottom: 0.5rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: hsl(var(--muted-foreground));
}
.editorial-underline {
position: relative;
padding-bottom: 0.35em;
}
.editorial-underline::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
background: hsl(var(--border));
}
/* --- Body text override --- */
.editorial-text {
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
color: hsl(var(--foreground));
line-height: 1.7;
}
.editorial-text p + p { text-indent: 1.5em; margin-top: 0; }
/* --- Inline accent link --- */
.editorial-link {
color: hsl(var(--accent));
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
transition: color 150ms ease, text-decoration-color 150ms ease;
}
.editorial-link:hover,
.editorial-link:focus-visible { color: hsl(var(--foreground)); }
/* --- Button (hairline border, no fill, accent on hover) --- */
.editorial-button {
display: inline-block;
padding: 0.55rem 1.1rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.14em;
background-color: transparent;
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
text-decoration: none;
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease;
}
.editorial-button:hover,
.editorial-button:focus {
color: hsl(var(--accent));
border-color: hsl(var(--accent));
}
.editorial-button:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Panel / card (hairline frame, no shadow) --- */
.editorial-panel,
.editorial-card {
border: 1px solid hsl(var(--border));
background-color: hsl(var(--card));
transition: border-color 150ms ease;
}
.editorial-panel:hover,
.editorial-card:hover { border-color: hsl(var(--accent)); }
/* --- Form field (hairline border, accent focus) --- */
.editorial-input {
width: 100%;
border: 1px solid hsl(var(--input));
background-color: hsl(var(--background));
color: hsl(var(--foreground));
padding: 0.55rem 0.75rem;
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
font-size: 0.95rem;
}
.editorial-input::placeholder { color: hsl(var(--muted-foreground)); }
.editorial-input:focus {
outline: none;
border-color: hsl(var(--accent));
box-shadow: 0 0 0 1px hsl(var(--accent));
}
/* --- Figure / image caption --- */
.editorial-figure { margin: 1.5rem 0; }
.editorial-figure img { display: block; width: 100%; height: auto; }
.editorial-figure figcaption {
margin-top: 0.75rem;
padding-top: 0.5rem;
border-top: var(--editorial-rule);
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
font-style: italic;
font-size: 0.85rem;
color: hsl(var(--muted-foreground));
}
/* --- Drop cap — oxblood accent first letter --- */
.editorial-dropcap p:first-of-type::first-letter {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 900;
font-style: italic;
color: hsl(var(--accent));
float: left;
font-size: 4.5em;
line-height: 0.85;
padding-right: 0.12em;
padding-top: 0.05em;
margin-bottom: -0.1em;
}
/* --- Marginalia rail (right-side italic notes) --- */
.editorial-marginalia {
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
font-style: italic;
font-size: 0.8rem;
line-height: 1.5;
color: hsl(var(--muted-foreground));
}
.editorial-marginalia aside {
margin: 0 0 1.5rem 0;
padding-left: 0.75rem;
border-left: var(--editorial-rule);
opacity: 0;
animation: editorial-fade-in 600ms ease forwards;
}
/* Anchor label: block-level so it never runs into the note body, and
* foreground ink so it reads as a deliberate annotation heading. */
.editorial-marginalia-label {
display: block;
margin-bottom: 0.4rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-style: normal;
font-weight: 600;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: hsl(var(--foreground));
}
/* Note body: bump above muted-foreground for legibility in both modes. */
.editorial-marginalia-note {
display: block;
color: hsl(var(--foreground) / 0.82);
}
@keyframes editorial-fade-in { to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
.editorial-marginalia aside { animation: none; opacity: 1; }
}
@media (max-width: 1023px) {
.editorial-marginalia {
margin-top: 1.5rem;
border-top: var(--editorial-rule);
padding-top: 1rem;
}
}
/* --- Masthead wordmark --- */
.editorial-masthead-wordmark {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 900;
font-style: italic;
letter-spacing: -0.02em;
color: hsl(var(--foreground));
}
.editorial-masthead-kicker {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.15em;
color: hsl(var(--muted-foreground));
}
.editorial-masthead-rule {
border: 0;
border-top: var(--editorial-rule);
margin: 0.5rem 0 0 0;
}
/* --- Pull quote — Playfair italic in oxblood --- */
.editorial-pullquote {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-style: italic;
font-size: 2rem;
line-height: 1.2;
color: hsl(var(--accent));
margin: 2.5rem auto;
max-width: var(--editorial-measure);
border: none;
}
.editorial-pullquote-attribution {
display: block;
margin-top: 1rem;
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
font-style: normal;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: hsl(var(--muted-foreground));
}
/* --- Byline --- */
.editorial-byline {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0;
border-top: var(--editorial-rule);
border-bottom: var(--editorial-rule);
}
.editorial-byline-photo {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: hsl(var(--muted));
flex-shrink: 0;
object-fit: cover;
}
.editorial-byline-author {
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-weight: 500;
font-size: 0.95rem;
color: hsl(var(--foreground));
}
.editorial-byline-meta {
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: hsl(var(--muted-foreground));
}
/* --- Colophon --- */
.editorial-colophon {
border-top: var(--editorial-rule);
padding-top: 1.5rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
}
.editorial-colophon-issn {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* --- Reading-progress underline (CSS-only atmosphere) --- */
.editorial-progress {
position: sticky;
top: 0;
height: 2px;
background: hsl(var(--accent));
transform-origin: 0 50%;
animation: editorial-progress linear both;
animation-timeline: scroll(root block);
z-index: 50;
}
@keyframes editorial-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) {
.editorial-progress { animation: none; transform: scaleX(0); }
}
}

View File

@ -1,58 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// MarginaliaBlockMeta describes the Editorial marginalia rail block: a list of
// italic notes anchored to in-body markers. At narrow viewports the rail
// collapses to inline asides via CSS.
// Registered as "editorial:marginalia" at runtime.
var MarginaliaBlockMeta = blocks.BlockMeta{
Key: "marginalia",
Title: "Marginalia Rail",
Description: "Right-rail italic notes for longform articles",
Source: "editorial",
Category: blocks.CategoryTheme,
}
// MarginaliaNote is one row in the marginalia rail.
type MarginaliaNote struct {
Note string // rich-text payload
Anchor string
}
// MarginaliaData carries parsed marginalia content.
type MarginaliaData struct {
Notes []MarginaliaNote
}
// MarginaliaBlock renders the marginalia rail.
//
// Content shape:
//
// {
// "items": [
// { "note": "<p>Sourced from the 2024 audit.</p>", "anchor": "¶3" }
// ]
// }
func MarginaliaBlock(ctx context.Context, content map[string]any) string {
items := getSlice(content, "items")
notes := make([]MarginaliaNote, 0, len(items))
for _, item := range items {
notes = append(notes, MarginaliaNote{
Note: getString(item, "note"),
Anchor: getString(item, "anchor"),
})
}
data := MarginaliaData{Notes: notes}
var buf bytes.Buffer
_ = marginaliaComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,23 +0,0 @@
package main
// marginaliaComponent renders the marginalia rail. When there are zero notes
// the block renders an empty container so the master-page slot still has a
// stable hook point in the DOM (the UAT looks for the data-block attribute).
templ marginaliaComponent(data MarginaliaData) {
<div class="editorial-marginalia" data-block="editorial:marginalia">
if len(data.Notes) > 0 {
for _, note := range data.Notes {
<aside>
if note.Anchor != "" {
<span class="editorial-byline-meta">{ note.Anchor }</span>
}
if note.Note != "" {
@templ.Raw(note.Note)
}
</aside>
}
} else {
<aside class="text-xs opacity-60">{ "" }</aside>
}
</div>
}

View File

@ -1,102 +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"
// marginaliaComponent renders the marginalia rail. When there are zero notes
// the block renders an empty container so the master-page slot still has a
// stable hook point in the DOM (the UAT looks for the data-block attribute).
func marginaliaComponent(data MarginaliaData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-marginalia\" data-block=\"editorial:marginalia\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Notes) > 0 {
for _, note := range data.Notes {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if note.Anchor != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<span class=\"editorial-byline-meta\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(note.Anchor)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `marginalia.templ`, Line: 12, Col: 55}
}
_, 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, 4, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if note.Note != "" {
templ_7745c5c3_Err = templ.Raw(note.Note).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<aside class=\"text-xs opacity-60\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `marginalia.templ`, Line: 20, Col: 41}
}
_, 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, "</aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

37
master_pages.json Normal file
View File

@ -0,0 +1,37 @@
[
{
"key": "editorial:default-master",
"title": "Editorial Default Master",
"page_templates": ["default", "full-width", "blog-index", "contact"],
"blocks": [
{ "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "The Margin" } }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Featured", "content": { "slotName": "featured" }, "slot": "featured", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 },
{ "block_key": "slot", "title": "Aside", "content": { "slotName": "aside" }, "slot": "aside", "sort_order": 0 },
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "centered", "companyName": "The Margin", "tagline": "A newsletter about paying attention.", "showNewsletter": true, "newsletterTitle": "Subscribe", "newsletterDescription": "New essays, every other Sunday.", "copyright": "The Margin" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "editorial:landing-master",
"title": "Editorial Front Page Master",
"page_templates": ["landing"],
"blocks": [
{ "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "The Margin" } }, "slot": "masthead", "sort_order": 0 },
{ "block_key": "slot", "title": "Lead", "content": { "slotName": "lead", "placeholder": "Lead essay" }, "slot": "lead", "sort_order": 0 },
{ "block_key": "slot", "title": "River", "content": { "slotName": "river", "placeholder": "Recent essays" }, "slot": "river", "sort_order": 0 },
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "centered", "companyName": "The Margin", "tagline": "A newsletter about paying attention.", "showNewsletter": true, "newsletterTitle": "Subscribe", "newsletterDescription": "New essays, every other Sunday.", "copyright": "The Margin" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "editorial:article-master",
"title": "Editorial Article Master",
"page_templates": ["article"],
"blocks": [
{ "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "The Margin" } }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Byline", "content": { "slotName": "byline" }, "slot": "byline", "sort_order": 0 },
{ "block_key": "slot", "title": "Article Body", "content": { "slotName": "main", "placeholder": "Essay body" }, "slot": "main", "sort_order": 0 },
{ "block_key": "slot", "title": "Marginalia Rail", "content": { "slotName": "marginalia" }, "slot": "marginalia", "sort_order": 0 },
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "centered", "companyName": "The Margin", "tagline": "A newsletter about paying attention.", "copyright": "The Margin" }, "slot": "footer", "sort_order": 0 }
]
}
]

View File

@ -1,50 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// MastheadBlockMeta describes the Editorial masthead block: the publication
// wordmark, an optional kicker line, a hairline rule, and a section nav menu.
// Registered as "editorial:masthead" at runtime.
var MastheadBlockMeta = blocks.BlockMeta{
Key: "masthead",
Title: "Masthead",
Description: "Newspaper-style masthead with Playfair wordmark and hairline rule",
Source: "editorial",
Category: blocks.CategoryNavigation,
}
// MastheadData carries the parsed content for the masthead template.
type MastheadData struct {
Title string
Kicker string
MenuName string
Compact bool
}
// MastheadBlock renders the masthead.
//
// Content shape:
//
// {
// "title": "Editorial", // wordmark, falls back to site name when empty
// "kicker": "EST. 2026", // optional small caption above the wordmark
// "menuName": "main", // optional menu key for section nav (not resolved here)
// "compact": "false" // compact variant for article pages
// }
func MastheadBlock(ctx context.Context, content map[string]any) string {
data := MastheadData{
Title: getStringWithDefault(content, "title", "Editorial"),
Kicker: getString(content, "kicker"),
MenuName: getString(content, "menuName"),
Compact: getBool(content, "compact", false),
}
var buf bytes.Buffer
_ = mastheadComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,36 +0,0 @@
package main
// mastheadComponent renders the Editorial masthead. The wordmark is the brand
// signature (Playfair 900 italic). The optional kicker is a small mono caption
// above. A hairline rule sits below the title block. When compact mode is on
// the kicker is suppressed and the layout collapses to one line.
templ mastheadComponent(data MastheadData) {
<div class="editorial-wide" data-block="editorial:masthead">
if data.Compact {
<div class="flex items-center justify-between py-3">
<a href="/" class="editorial-masthead-wordmark text-2xl">
{ data.Title }
</a>
if data.MenuName != "" {
<span class="editorial-masthead-kicker">{ data.MenuName }</span>
}
</div>
<hr class="editorial-masthead-rule"/>
} else {
<div class="text-center py-6">
if data.Kicker != "" {
<div class="editorial-masthead-kicker mb-2">{ data.Kicker }</div>
}
<a href="/" class="editorial-masthead-wordmark inline-block text-5xl md:text-6xl">
{ data.Title }
</a>
<hr class="editorial-masthead-rule mt-4"/>
if data.MenuName != "" {
<nav class="editorial-section-label mt-3" aria-label="Sections">
<span class="text-xs">{ "Sections: " }{ data.MenuName }</span>
</nav>
}
</div>
}
</div>
}

View File

@ -1,163 +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"
// mastheadComponent renders the Editorial masthead. The wordmark is the brand
// signature (Playfair 900 italic). The optional kicker is a small mono caption
// above. A hairline rule sits below the title block. When compact mode is on
// the kicker is suppressed and the layout collapses to one line.
func mastheadComponent(data MastheadData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-wide\" data-block=\"editorial:masthead\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Compact {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex items-center justify-between py-3\"><a href=\"/\" class=\"editorial-masthead-wordmark text-2xl\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 12, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.MenuName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"editorial-masthead-kicker\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 15, Col: 60}
}
_, 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, 5, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><hr class=\"editorial-masthead-rule\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"text-center py-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Kicker != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"editorial-masthead-kicker mb-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Kicker)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 22, Col: 62}
}
_, 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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<a href=\"/\" class=\"editorial-masthead-wordmark inline-block text-5xl md:text-6xl\">")
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: `masthead.templ`, Line: 25, Col: 17}
}
_, 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, "</a><hr class=\"editorial-masthead-rule mt-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.MenuName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<nav class=\"editorial-section-label mt-3\" aria-label=\"Sections\"><span class=\"text-xs\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("Sections: ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 30, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 30, Col: 59}
}
_, 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, 13, "</span></nav>")
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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -2,11 +2,8 @@
name = "editorial"
display_name = "Editorial"
scope = "@themes"
version = "0.1.1"
description = "Didone display type, narrow measure, and hairline rules for newsrooms, longform writers, and opinion columnists."
version = "0.5.0"
description = "A personal essayist's reading room: warm book-paper light, sepia-on-charcoal dark, serif baseline grid, drop caps, running marginalia and small-caps labels for longform writers and independent newsletters."
kind = "theme"
categories = ["templates"]
tags = ["editorial", "serif", "news", "longform", "magazine", "law", "finance", "opinion"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"
tags = ["editorial", "serif", "essay", "newsletter", "longform", "reading", "typographic", "blog", "writer"]

View File

@ -1,88 +1,217 @@
[
{
"id": "broadsheet",
"name": "Broadsheet",
"description": "Default light: cream paper, ink black, oxblood accent. Newsroom front page.",
"id": "book-paper",
"name": "Book Paper",
"description": "The default. Warm off-white paper and dark serif ink by day, low-glare charcoal with sepia type by night. Oxblood accent throughout.",
"theme": {
"mode": "light",
"typography": {
"fontHeading": "template:editorial:Playfair Display",
"fontBody": "template:editorial:Source Serif 4"
},
"mode": "both",
"lightColors": {
"background": "36 30% 96%",
"foreground": "20 14% 10%",
"card": "36 30% 98%",
"cardForeground": "20 14% 10%",
"popover": "36 30% 98%",
"popoverForeground": "20 14% 10%",
"primary": "20 14% 10%",
"primaryForeground": "36 30% 96%",
"secondary": "36 20% 90%",
"secondaryForeground": "20 14% 10%",
"muted": "36 20% 92%",
"mutedForeground": "20 10% 38%",
"accent": "0 55% 28%",
"accentForeground": "36 30% 96%",
"destructive": "0 72% 42%",
"destructiveForeground": "36 30% 96%",
"border": "30 12% 82%",
"input": "30 12% 82%",
"ring": "0 55% 28%"
"background": "38 32% 95%",
"foreground": "24 16% 12%",
"card": "38 34% 97%",
"cardForeground": "24 16% 12%",
"popover": "38 34% 97%",
"popoverForeground": "24 16% 12%",
"primary": "24 16% 12%",
"primaryForeground": "38 32% 95%",
"secondary": "36 22% 89%",
"secondaryForeground": "24 16% 14%",
"muted": "36 22% 91%",
"mutedForeground": "26 10% 38%",
"accent": "3 58% 34%",
"accentForeground": "38 32% 95%",
"destructive": "0 70% 42%",
"destructiveForeground": "38 32% 95%",
"border": "32 16% 82%",
"input": "32 16% 80%",
"ring": "3 58% 34%"
},
"darkColors": {
"background": "26 10% 10%",
"foreground": "36 22% 82%",
"card": "26 10% 13%",
"cardForeground": "36 22% 82%",
"popover": "26 10% 13%",
"popoverForeground": "36 22% 82%",
"primary": "36 22% 82%",
"primaryForeground": "26 10% 10%",
"secondary": "26 8% 18%",
"secondaryForeground": "36 20% 80%",
"muted": "26 8% 16%",
"mutedForeground": "34 12% 58%",
"accent": "6 62% 56%",
"accentForeground": "26 10% 10%",
"destructive": "0 68% 52%",
"destructiveForeground": "36 22% 88%",
"border": "28 8% 22%",
"input": "28 8% 20%",
"ring": "6 62% 56%"
}
}
},
{
"id": "nightdesk",
"name": "Nightdesk",
"description": "Dark mode for after-hours reading. Inverted broadsheet with deeper accent.",
"id": "sepia-review",
"name": "Sepia Review",
"description": "Warm sepia in both modes for a soft, magazine-like read. Amber paper and umber ink by day, deep tobacco ground by night.",
"theme": {
"mode": "dark",
"typography": {
"fontHeading": "template:editorial:Playfair Display",
"fontBody": "template:editorial:Source Serif 4"
},
"mode": "both",
"lightColors": {
"background": "34 44% 93%",
"foreground": "22 30% 16%",
"card": "34 46% 96%",
"cardForeground": "22 30% 16%",
"popover": "34 46% 96%",
"popoverForeground": "22 30% 16%",
"primary": "22 30% 16%",
"primaryForeground": "34 44% 93%",
"secondary": "32 30% 87%",
"secondaryForeground": "22 30% 18%",
"muted": "32 28% 89%",
"mutedForeground": "24 18% 40%",
"accent": "14 62% 38%",
"accentForeground": "34 44% 94%",
"destructive": "0 70% 42%",
"destructiveForeground": "34 44% 94%",
"border": "30 22% 78%",
"input": "30 22% 76%",
"ring": "14 62% 38%"
},
"darkColors": {
"background": "24 10% 8%",
"foreground": "36 20% 92%",
"card": "24 10% 11%",
"cardForeground": "36 20% 92%",
"popover": "24 10% 11%",
"popoverForeground": "36 20% 92%",
"primary": "36 20% 92%",
"primaryForeground": "24 10% 8%",
"secondary": "24 8% 16%",
"secondaryForeground": "36 20% 92%",
"muted": "24 8% 14%",
"mutedForeground": "36 12% 62%",
"accent": "0 60% 52%",
"accentForeground": "24 10% 8%",
"destructive": "0 72% 50%",
"destructiveForeground": "36 20% 92%",
"border": "24 8% 20%",
"input": "24 8% 18%",
"ring": "0 60% 52%"
"background": "24 18% 11%",
"foreground": "34 30% 80%",
"card": "24 18% 14%",
"cardForeground": "34 30% 80%",
"popover": "24 18% 14%",
"popoverForeground": "34 30% 80%",
"primary": "34 30% 80%",
"primaryForeground": "24 18% 11%",
"secondary": "24 14% 19%",
"secondaryForeground": "34 28% 78%",
"muted": "24 14% 17%",
"mutedForeground": "32 16% 60%",
"accent": "18 68% 54%",
"accentForeground": "24 18% 10%",
"destructive": "0 68% 52%",
"destructiveForeground": "34 30% 86%",
"border": "26 14% 23%",
"input": "26 14% 21%",
"ring": "18 68% 54%"
}
}
},
{
"id": "legal-pad",
"name": "Legal Pad",
"description": "Warm cream for law and finance verticals. Tighter saturation, deeper ink.",
"description": "Deeper cream and near-black ink for law, finance and long essays. Tight saturation by day, cool ink-black night desk.",
"theme": {
"mode": "light",
"typography": {
"fontHeading": "template:editorial:Playfair Display",
"fontBody": "template:editorial:Source Serif 4"
},
"mode": "both",
"lightColors": {
"background": "48 40% 94%",
"foreground": "28 30% 14%",
"card": "48 40% 97%",
"cardForeground": "28 30% 14%",
"popover": "48 40% 97%",
"popoverForeground": "28 30% 14%",
"primary": "28 30% 14%",
"primaryForeground": "48 40% 94%",
"secondary": "44 30% 86%",
"secondaryForeground": "28 30% 14%",
"muted": "44 28% 90%",
"mutedForeground": "28 18% 36%",
"accent": "10 60% 30%",
"accentForeground": "48 40% 94%",
"destructive": "0 72% 42%",
"destructiveForeground": "48 40% 94%",
"border": "40 20% 78%",
"input": "40 20% 80%",
"ring": "10 60% 30%"
"background": "48 42% 93%",
"foreground": "30 28% 13%",
"card": "48 44% 96%",
"cardForeground": "30 28% 13%",
"popover": "48 44% 96%",
"popoverForeground": "30 28% 13%",
"primary": "30 28% 13%",
"primaryForeground": "48 42% 93%",
"secondary": "46 30% 86%",
"secondaryForeground": "30 28% 15%",
"muted": "46 28% 89%",
"mutedForeground": "30 16% 36%",
"accent": "10 58% 32%",
"accentForeground": "48 42% 94%",
"destructive": "0 70% 42%",
"destructiveForeground": "48 42% 94%",
"border": "42 22% 77%",
"input": "42 22% 75%",
"ring": "10 58% 32%"
},
"darkColors": {
"background": "220 12% 9%",
"foreground": "44 20% 84%",
"card": "220 12% 12%",
"cardForeground": "44 20% 84%",
"popover": "220 12% 12%",
"popoverForeground": "44 20% 84%",
"primary": "44 20% 84%",
"primaryForeground": "220 12% 9%",
"secondary": "220 10% 17%",
"secondaryForeground": "44 18% 82%",
"muted": "220 10% 15%",
"mutedForeground": "40 12% 58%",
"accent": "8 64% 55%",
"accentForeground": "220 12% 9%",
"destructive": "0 68% 52%",
"destructiveForeground": "44 20% 88%",
"border": "220 10% 21%",
"input": "220 10% 19%",
"ring": "8 64% 55%"
}
}
},
{
"id": "nightdesk",
"name": "Nightdesk",
"description": "Cooler neutral paper and a crisp after-hours reading mode. A brighter red accent for the late edition.",
"theme": {
"typography": {
"fontHeading": "template:editorial:Playfair Display",
"fontBody": "template:editorial:Source Serif 4"
},
"mode": "both",
"lightColors": {
"background": "40 20% 96%",
"foreground": "220 14% 14%",
"card": "40 22% 98%",
"cardForeground": "220 14% 14%",
"popover": "40 22% 98%",
"popoverForeground": "220 14% 14%",
"primary": "220 14% 14%",
"primaryForeground": "40 20% 96%",
"secondary": "40 14% 90%",
"secondaryForeground": "220 14% 16%",
"muted": "40 14% 92%",
"mutedForeground": "220 8% 40%",
"accent": "352 60% 42%",
"accentForeground": "40 20% 96%",
"destructive": "0 72% 44%",
"destructiveForeground": "40 20% 96%",
"border": "36 12% 83%",
"input": "36 12% 81%",
"ring": "352 60% 42%"
},
"darkColors": {
"background": "222 16% 9%",
"foreground": "40 16% 86%",
"card": "222 16% 12%",
"cardForeground": "40 16% 86%",
"popover": "222 16% 12%",
"popoverForeground": "40 16% 86%",
"primary": "40 16% 86%",
"primaryForeground": "222 16% 9%",
"secondary": "222 12% 17%",
"secondaryForeground": "40 14% 84%",
"muted": "222 12% 15%",
"mutedForeground": "40 10% 60%",
"accent": "352 70% 58%",
"accentForeground": "222 16% 9%",
"destructive": "0 70% 54%",
"destructiveForeground": "40 16% 90%",
"border": "222 12% 22%",
"input": "222 12% 20%",
"ring": "352 70% 58%"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 164 KiB

View File

@ -1,48 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// PullquoteBlockMeta describes the Editorial pull-quote block: a large oxblood
// Playfair italic block quote with optional attribution.
// Registered as "editorial:pullquote" at runtime.
var PullquoteBlockMeta = blocks.BlockMeta{
Key: "pullquote",
Title: "Pull Quote",
Description: "Large oxblood pull quote in Playfair italic",
Source: "editorial",
Category: blocks.CategoryTheme,
}
// PullquoteData carries parsed pull-quote content.
type PullquoteData struct {
Quote string // rich-text payload, trusted upstream
Attribution string
}
// PullquoteBlock renders the pull quote.
//
// Content shape:
//
// {
// "quote": "<p>The reform was always going to be quiet.</p>",
// "attribution": "— The Editor"
// }
func PullquoteBlock(ctx context.Context, content map[string]any) string {
data := PullquoteData{
Quote: getString(content, "quote"),
Attribution: getString(content, "attribution"),
}
if data.Quote == "" {
return ""
}
var buf bytes.Buffer
_ = pullquoteComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,16 +0,0 @@
package main
// pullquoteComponent renders the oxblood Playfair italic pull quote. The quote
// itself is trusted rich text supplied by the editor; we render it inline
// using templ.Raw so author-supplied emphasis is preserved. The attribution is
// rendered as a plain-text small-caps cap below.
templ pullquoteComponent(data PullquoteData) {
<aside class="editorial-column" data-block="editorial:pullquote">
<blockquote class="editorial-pullquote">
@templ.Raw(data.Quote)
if data.Attribution != "" {
<cite class="editorial-pullquote-attribution">{ data.Attribution }</cite>
}
</blockquote>
</aside>
}

View File

@ -1,71 +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"
// pullquoteComponent renders the oxblood Playfair italic pull quote. The quote
// itself is trusted rich text supplied by the editor; we render it inline
// using templ.Raw so author-supplied emphasis is preserved. The attribution is
// rendered as a plain-text small-caps cap below.
func pullquoteComponent(data PullquoteData) 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, "<aside class=\"editorial-column\" data-block=\"editorial:pullquote\"><blockquote class=\"editorial-pullquote\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Quote).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Attribution != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<cite class=\"editorial-pullquote-attribution\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Attribution)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pullquote.templ`, Line: 12, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</cite>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</blockquote></aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,188 +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 its Render
// method, so the adapter only has to swap the return type.
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. The call order matters:
// 1. RegisterSystemTemplate seeds the system-template row.
// 2. RegisterPageTemplate once per page template (default / landing /
// article / full-width).
// 3. br.LoadSchemasFromFS BEFORE any br.Register so editor-side schemas
// are bound to their block keys.
// 4. br.Register for each theme-owned block (registered unqualified;
// addressed downstream as "editorial:<key>").
// 5. br.RegisterTemplateOverride for the four built-ins editorial styles
// differently (heading, text, button, image). The overrides are scoped
// to the "editorial" template — other templates keep their built-ins.
// 6. tr.RegisterEmailWrapper for the editorial-branded email layout.
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
Key: "editorial",
Title: "Editorial",
Description: "Didone display, narrow column, hairline rules for the modern newsroom.",
})
if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{
Key: "default",
Title: "Default",
Description: "Standard page with masthead, narrow main column, footer",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderEditorial)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{
Key: "landing",
Title: "Section Front",
Description: "Front-page-style stack: masthead, lead story, river, footer",
Slots: []string{"masthead", "lead", "river", "footer"},
}, wrap(RenderEditorialLanding)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{
Key: "article",
Title: "Article",
Description: "Single-column long read with byline, drop cap, marginalia rail",
Slots: []string{"header", "byline", "main", "marginalia", "footer"},
}, wrap(RenderEditorialArticle)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{
Key: "full-width",
Title: "Full Width",
Description: "Edge-to-edge for photo essays and data graphics",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderEditorialFullWidth)); err != nil {
return err
}
// Schemas MUST be loaded before any Register so the editor binds the
// correct content schema to each block at registration time.
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
return err
}
// Theme-owned blocks.
br.Register(MastheadBlockMeta, MastheadBlock)
br.Register(BylineBlockMeta, BylineBlock)
br.Register(PullquoteBlockMeta, PullquoteBlock)
br.Register(DropcapIntroBlockMeta, DropcapIntroBlock)
br.Register(MarginaliaBlockMeta, MarginaliaBlock)
br.Register(SectionLabelBlockMeta, SectionLabelBlock)
br.Register(ColophonBlockMeta, ColophonBlock)
// Built-in overrides scoped to the editorial template.
br.RegisterTemplateOverride("editorial", "heading", EditorialHeadingBlock)
br.RegisterTemplateOverride("editorial", "text", EditorialTextBlock)
br.RegisterTemplateOverride("editorial", "button", EditorialButtonBlock)
br.RegisterTemplateOverride("editorial", "image", EditorialImageBlock)
// Branded email wrapper.
tr.RegisterEmailWrapper("editorial", EditorialEmailWrapper)
return nil
}
// DefaultMasterPages returns the two master pages editorial seeds on first
// load. The slot keys and BlockKey strings here match spec §7 exactly; the
// `slot` block carries `slotName` equal to its rendered slot key so the CMS
// knows where to inject page content.
func DefaultMasterPages() []plugin.MasterPageDefinition {
return []plugin.MasterPageDefinition{
{
Key: "editorial:default-master",
Title: "Editorial Default Master",
PageTemplates: []string{"default", "landing", "full-width"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "editorial:masthead",
Title: "Masthead",
Content: map[string]any{"menuName": "main", "kicker": "EST. 2026"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "navbar",
Title: "Section Nav",
Content: map[string]any{"menuName": "sections"},
Slot: "header",
SortOrder: 1,
},
{
BlockKey: "slot",
Title: "Main Content",
Content: map[string]any{"slotName": "main", "placeholder": "Page content"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "editorial:colophon",
Title: "Colophon",
Content: map[string]any{"showSignup": true, "issn": "0000-0000"},
Slot: "footer",
SortOrder: 0,
},
},
},
{
Key: "editorial:article-master",
Title: "Editorial Article Master",
PageTemplates: []string{"article"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "editorial:masthead",
Title: "Masthead",
Content: map[string]any{"menuName": "main", "compact": true},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "editorial:byline",
Title: "Byline",
Content: map[string]any{"showPhoto": true, "showReadTime": true},
Slot: "byline",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Article Body",
Content: map[string]any{"slotName": "main"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "editorial:marginalia",
Title: "Marginalia Rail",
Content: map[string]any{"items": []any{}},
Slot: "marginalia",
SortOrder: 0,
},
{
BlockKey: "editorial:colophon",
Title: "Colophon",
Content: map[string]any{"showSignup": true},
Slot: "footer",
SortOrder: 0,
},
},
},
}
}

View File

@ -1,29 +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 Editorial theme.
//
// Editorial is a templ-style theme: each block ships a typed `<key>.go` render
// function backed by a `<key>.templ` component. The standalone-plugin block
// signature is `func(ctx, content map[string]any) string` (no children arg).
var Registration = plugin.PluginRegistration{
Name: "editorial",
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,36 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Byline",
"description": "Article byline with author name, optional photo, optional read-time, and dateline.",
"type": "object",
"properties": {
"authorSlug": {
"type": "string",
"title": "Author Slug",
"description": "Slug of the author whose name and photo should be rendered.",
"x-editor": "slug"
},
"showPhoto": {
"type": "string",
"title": "Show Photo",
"description": "Render the author's avatar to the left of the name.",
"x-editor": "select",
"enum": ["false", "true"],
"default": "true"
},
"showReadTime": {
"type": "string",
"title": "Show Read Time",
"description": "Include the read-time estimate next to the dateline.",
"x-editor": "select",
"enum": ["false", "true"],
"default": "true"
},
"dateline": {
"type": "string",
"title": "Dateline",
"description": "Optional explicit dateline (e.g. \"London, March 14\"). Rendered in small caps.",
"x-editor": "text"
}
}
}

View File

@ -1,46 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Colophon",
"description": "Editorial footer with print-convention ISSN field, optional subscribe stub, and a list of footer links.",
"type": "object",
"properties": {
"showSignup": {
"type": "string",
"title": "Show Subscribe Stub",
"description": "Render the newsletter subscribe stub above the link list.",
"x-editor": "select",
"enum": ["false", "true"],
"default": "false"
},
"issn": {
"type": "string",
"title": "ISSN",
"description": "Print-convention ISSN number. Rendered in JetBrains Mono small caps.",
"x-editor": "text"
},
"links": {
"type": "array",
"title": "Footer Links",
"description": "Inline footer link list.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"title": "Link Text",
"description": "Display text.",
"x-editor": "text"
},
"url": {
"type": "string",
"title": "URL",
"description": "External URL.",
"x-editor": "link"
}
}
}
}
}
}

View File

@ -1,34 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Masthead",
"description": "Newspaper-style masthead with wordmark, optional kicker, hairline rule, and section navigation menu.",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Wordmark",
"description": "The publication name. Rendered in Playfair Display 900 italic.",
"x-editor": "text"
},
"kicker": {
"type": "string",
"title": "Kicker",
"description": "Small mono caption above the wordmark (e.g. issue label or established date).",
"x-editor": "text"
},
"menuName": {
"type": "string",
"title": "Section Menu",
"description": "Menu providing the section navigation links rendered beneath the masthead.",
"x-editor": "menu-select"
},
"compact": {
"type": "string",
"title": "Compact Mode",
"description": "Use the compact masthead variant (single line) on article pages.",
"x-editor": "select",
"enum": ["false", "true"],
"default": "false"
}
}
}

View File

@ -1,20 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Pull Quote",
"description": "Large oxblood pull quote rendered in Playfair Display italic with optional attribution.",
"type": "object",
"properties": {
"quote": {
"type": "string",
"title": "Quote",
"description": "The pull quote text. Rich text is permitted so authors can emphasise inline.",
"x-editor": "richtext"
},
"attribution": {
"type": "string",
"title": "Attribution",
"description": "Optional source attribution rendered in small caps below the quote.",
"x-editor": "text"
}
}
}

View File

@ -1,22 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Section Label",
"description": "Small-caps section label followed by an optional hairline divider rule.",
"type": "object",
"properties": {
"label": {
"type": "string",
"title": "Label",
"description": "Section label text. Rendered uppercase with small-caps.",
"x-editor": "text"
},
"divider": {
"type": "string",
"title": "Divider",
"description": "Whether to render a hairline rule beneath the label.",
"x-editor": "select",
"enum": ["true", "false"],
"default": "true"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

View File

@ -1,45 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// SectionLabelBlockMeta describes the Editorial section-label block: an
// uppercase / small-caps label followed by an optional hairline divider.
// Registered as "editorial:section_label" at runtime.
var SectionLabelBlockMeta = blocks.BlockMeta{
Key: "section_label",
Title: "Section Label",
Description: "Small-caps section label with optional hairline divider",
Source: "editorial",
Category: blocks.CategoryTheme,
}
// SectionLabelData carries parsed section-label content.
type SectionLabelData struct {
Label string
Divider bool
}
// SectionLabelBlock renders the section label.
//
// Content shape:
//
// { "label": "Politics", "divider": "true" }
func SectionLabelBlock(ctx context.Context, content map[string]any) string {
data := SectionLabelData{
Label: getString(content, "label"),
Divider: getBool(content, "divider", true),
}
if data.Label == "" {
return ""
}
var buf bytes.Buffer
_ = sectionLabelComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,12 +0,0 @@
package main
// sectionLabelComponent renders the section label. The visual contract is
// uppercase + small-caps text followed (optionally) by a 1px hairline rule.
templ sectionLabelComponent(data SectionLabelData) {
<div class="editorial-column" data-block="editorial:section_label">
<div class="editorial-section-label">{ data.Label }</div>
if data.Divider {
<hr class="editorial-section-label-rule"/>
}
</div>
}

View File

@ -1,65 +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"
// sectionLabelComponent renders the section label. The visual contract is
// uppercase + small-caps text followed (optionally) by a 1px hairline rule.
func sectionLabelComponent(data SectionLabelData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-column\" data-block=\"editorial:section_label\"><div class=\"editorial-section-label\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `section_label.templ`, Line: 7, Col: 51}
}
_, 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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Divider {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<hr class=\"editorial-section-label-rule\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

196
seed/seed.json Normal file
View File

@ -0,0 +1,196 @@
{
"demo": true,
"settings": {
"merge": {
"companyName": "The Margin",
"tagline": "A newsletter about paying attention."
}
},
"data_tables": [
{
"key": "contact_submissions",
"name": "Letters",
"description": "Seeded contact-form submissions for the The Margin demo. 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": "subject", "label": "Subject", "type": "text", "required": false },
{ "key": "message", "label": "Message", "type": "text", "required": true }
]
}
],
"pages": [
{
"slug": "/",
"title": "The Margin",
"template_key": "landing",
"blocks": [
{
"block_key": "hero",
"title": "Lead",
"slot": "lead",
"sort_order": 1,
"content": {
"template": "centered",
"eyebrow": { "text": "Issue 12" },
"headline": { "text": "The Margin" },
"subheadline": { "text": "A newsletter about paying attention." },
"description": { "text": "Essays on reading, attention, and the slow work of noticing. New writing lands every other Sunday, free to your inbox." },
"ctaPrimary": { "text": "Read the latest", "url": "/blog", "style": "primary" },
"ctaSecondary": { "text": "About the writer", "url": "/about", "style": "outline" }
}
},
{
"block_key": "heading",
"title": "Recent heading",
"slot": "river",
"sort_order": 1,
"content": { "text": "Recent essays", "level": 2 }
},
{
"block_key": "card",
"title": "Essay: On Attention",
"slot": "river",
"sort_order": 2,
"content": { "layout": "vertical", "title": "On attention", "text": "<p>What it costs to look at one thing for a long time, and what it gives back.</p>", "link": "/blog/on-attention", "linkLabel": "Read the essay" }
},
{
"block_key": "card",
"title": "Essay: The Slow Read",
"slot": "river",
"sort_order": 3,
"content": { "layout": "vertical", "title": "The slow read", "text": "<p>A case for reading less, and reading it twice.</p>", "link": "/blog/the-slow-read", "linkLabel": "Read the essay" }
},
{
"block_key": "card",
"title": "Essay: Notes on Noticing",
"slot": "river",
"sort_order": 4,
"content": { "layout": "vertical", "title": "Notes on noticing", "text": "<p>Small habits that keep the world from going quiet.</p>", "link": "/blog/notes-on-noticing", "linkLabel": "Read the essay" }
},
{
"block_key": "newsletter-signup",
"title": "Subscribe",
"slot": "river",
"sort_order": 5,
"content": { "title": "Subscribe", "description": "New essays every other Sunday. No noise, no schedule creep.", "listSlug": "main", "source": "home", "submitText": "Subscribe", "successMessage": "You are on the list. Look for the next issue on Sunday.", "placeholder": "you@example.com", "layout": "card" }
}
]
},
{
"slug": "/about",
"title": "About",
"template_key": "default",
"blocks": [
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "About", "level": 1 } },
{ "block_key": "rich-text", "title": "Bio", "slot": "main", "sort_order": 2, "content": { "html": "<p>The Margin is a newsletter by Frances Hale. It is about reading, attention, and the slow work of noticing.</p><p>I write one essay every other week. Some are short. Some take the long way around. All of them start in the margins of a book.</p><p>You can read the archive for free. Members get the full run and the occasional letter.</p>" } },
{ "block_key": "editorial:reading_list", "title": "Shelf", "slot": "main", "sort_order": 3, "content": { "title": "On the shelf", "intro": "Books I keep going back to.", "items": [ { "label": "The Writing Life", "source": "Annie Dillard", "note": "On the daily cost of the work." }, { "label": "Ways of Seeing", "source": "John Berger", "note": "How looking is never neutral." }, { "label": "The Art of Stillness", "source": "Pico Iyer", "note": "A short book about going nowhere." } ] } },
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 4, "content": { "style": "block", "align": "center", "quote": "Attention is the beginning of devotion.", "attribution": "Mary Oliver" } }
]
},
{
"slug": "/blog",
"title": "The archive",
"template_key": "blog-index",
"blocks": [
{ "block_key": "card", "title": "Featured: On Attention", "slot": "featured", "sort_order": 1, "content": { "layout": "horizontal", "title": "On attention", "text": "<p>What it costs to look at one thing for a long time, and what it gives back. The lead essay this issue.</p>", "link": "/blog/on-attention", "linkLabel": "Read the essay" } },
{ "block_key": "card", "title": "Post: The Slow Read", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "The slow read", "text": "<p>A case for reading less, and reading it twice.</p>", "link": "/blog/the-slow-read", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: Notes on Noticing", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "Notes on noticing", "text": "<p>Small habits that keep the world from going quiet.</p>", "link": "/blog/notes-on-noticing", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: The Second Draft", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "The second draft", "text": "<p>Where the real writing happens, once the first draft is out of the way.</p>", "link": "/blog/on-attention", "linkLabel": "Read on" } }
]
},
{
"slug": "/blog/on-attention",
"title": "On attention",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "<p>By Frances Hale. Issue 12.</p>" } },
{ "block_key": "editorial:dropcap_intro", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "lead": "<p>There is a kind of looking that costs something. You give a thing your whole attention and the rest of the world goes quiet for a while. It is not comfortable. It is not always rewarded. But it is the only way I know to see anything clearly.</p>" } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>I started keeping a notebook of things I had failed to notice. A tree I passed every day for a year. The way a friend says my name. The list grew fast, which told me something about how little I had been paying attention.</p><p>Attention is not a switch. It is a muscle, and it gets tired. The trick is not to force it but to make room for it. Read slower. Walk without a phone. Let one thing be the whole thing for an hour.</p><p>What you get back is not efficiency. It is the world, a little louder than before.</p>" } },
{ "block_key": "editorial:marginalia", "title": "Marginalia", "slot": "marginalia", "sort_order": 1, "content": { "items": [ { "anchor": "On notebooks", "note": "I use a cheap one. A nice notebook makes me careful, and careful is the enemy here." }, { "anchor": "On walking", "note": "The phone stays home. That is the whole method." } ] } }
]
},
{
"slug": "/blog/the-slow-read",
"title": "The slow read",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "<p>By Frances Hale. Issue 11.</p>" } },
{ "block_key": "editorial:dropcap_intro", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "lead": "<p>Most of what I read once, I forget by the weekend. The books that stay with me are the ones I read twice. So I have started reading less, on purpose, and going back.</p>" } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>The second reading is a different book. You know where it is going, so you stop rushing. You notice the joins. You see what the writer set up on page ten and paid off on page two hundred.</p><p>This is slower, and that is the point. A shelf of books read once is a list. A shelf of books read twice is a conversation.</p>" } },
{ "block_key": "editorial:marginalia", "title": "Marginalia", "slot": "marginalia", "sort_order": 1, "content": { "items": [ { "anchor": "A rule", "note": "One new book for every reread. It keeps the shelf honest." } ] } }
]
},
{
"slug": "/blog/notes-on-noticing",
"title": "Notes on noticing",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "<p>By Frances Hale. Issue 10.</p>" } },
{ "block_key": "editorial:dropcap_intro", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "lead": "<p>Noticing is a habit before it is a talent. Here are the small ones that keep my world from going quiet, collected over a slow year of trying.</p>" } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Name one thing you saw today that you had not seen before. Do it out loud, at dinner, and make someone else do it too.</p><p>Carry a small notebook. Write the thing down before you explain it to yourself. The explanation can wait.</p><p>None of this is hard. That is what makes it easy to skip. Do it anyway, and the days stop blurring together.</p>" } },
{ "block_key": "editorial:field_notes", "title": "Field notes", "slot": "main", "sort_order": 3, "content": { "title": "Field notes", "notes": [ { "date": "March 14", "note": "<p>A heron on the river wall, standing so still I mistook it for a post.</p>" }, { "date": "March 9", "note": "<p>The bakery changed its bread and told no one. I noticed on the second bite.</p>" } ] } },
{ "block_key": "editorial:marginalia", "title": "Marginalia", "slot": "marginalia", "sort_order": 1, "content": { "items": [ { "anchor": "Try it", "note": "The dinner question works on children better than on adults. Adults think it is a test." } ] } }
]
},
{
"slug": "/contact",
"title": "Say hello",
"template_key": "contact",
"blocks": [
{
"block_key": "contact-form",
"title": "Write to me",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "Write to me",
"description": "A note, a question, a book you think I should read. I answer everything, though it can take a week.",
"submitLabel": "Send",
"successMessage": "Thank you. Your note has been received. I will write back soon.",
"fields": [
{ "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
{ "name": "subject", "label": "Subject", "type": "text", "required": false, "width": "full" },
{ "name": "message", "label": "Your note", "type": "textarea", "required": true, "width": "full" }
],
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
}
},
{
"block_key": "contact-card",
"title": "Other ways to reach me",
"slot": "aside",
"sort_order": 1,
"content": {
"heading": "Elsewhere",
"description": "The inbox is best. These work too.",
"columns": 1,
"channels": [
{ "icon": "lucide:mail", "label": "Email", "value": "frances@themargin.example", "link": "mailto:frances@themargin.example" },
{ "icon": "lucide:rss", "label": "Feed", "value": "Subscribe by RSS", "link": "/feed" }
]
}
}
]
},
{
"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 get the full archive and the occasional letter.</p>" } }
]
}
],
"menu_items": [
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
{ "menu": "main", "label": "Essays", "page_slug": "/blog", "sort_order": 2 },
{ "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 3 },
{ "menu": "main", "label": "Say hello", "page_slug": "/contact", "sort_order": 4 }
]
}

19
seed/workflows.json Normal file
View File

@ -0,0 +1,19 @@
{
"workflows": [
{
"key": "contact_notify_admins",
"name": "Letter 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,365 +0,0 @@
package main
// editorialUtilityCSS is the theme's custom utility layer appended to the host
// Tailwind input. It is intentionally light: no @font-face rules (those are
// emitted by the host from the admin's font assignments per docs/FONTS.md), no
// hardcoded hex / rgb / named colours. Every colour goes through the shadcn
// HSL variable consumer pattern: `hsl(var(--token))`.
//
// What this CSS contracts to deliver (cross-checked against the UAT §13):
// - .prose-editorial: 64ch measure, indented continuation paragraphs.
// - .editorial-dropcap p:first-of-type::first-letter: oxblood accent,
// ≥ 3× body font-size.
// - .editorial-marginalia: italic Source Serif 4 12px right-rail asides.
// - .editorial-masthead-wordmark: Playfair Display 900 italic.
// - .editorial-pullquote: Playfair italic 38px in oxblood accent.
// - .editorial-section-label: uppercase + small-caps + hairline.
// - .editorial-hairline / hr.editorial-rule: 1px solid hsl(var(--border)).
// - .editorial-button: transparent fill, 1px border, hover → accent text.
// - .editorial-caption: Source Serif italic 14px with hairline above.
//
// All `font-family` declarations route through the three CSS variables
// (`--font-heading`, `--font-body`, `--font-mono`) the host exposes on :root,
// with fallback stacks derived from spec §3.
const editorialUtilityCSS = `
/* === Editorial theme — utility layer === */
:root {
--editorial-measure: 64ch;
--editorial-rule: 1px solid hsl(var(--border));
}
/* --- Narrow editorial column with indented continuation paragraphs --- */
.prose-editorial {
max-width: var(--editorial-measure);
margin-left: auto;
margin-right: auto;
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
color: hsl(var(--foreground));
line-height: 1.65;
}
.prose-editorial p {
margin: 0 0 1em 0;
text-indent: 0;
}
.prose-editorial p + p {
text-indent: 1.5em;
}
.prose-editorial h1,
.prose-editorial h2,
.prose-editorial h3,
.prose-editorial h4 {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 700;
letter-spacing: -0.01em;
color: hsl(var(--foreground));
}
.prose-editorial a {
color: hsl(var(--accent));
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
/* --- Drop cap — oxblood accent first letter, ≥ 3x body size --- */
.editorial-dropcap p:first-of-type::first-letter {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 900;
font-style: italic;
color: hsl(var(--accent));
float: left;
font-size: 4.5em;
line-height: 0.85;
padding-right: 0.12em;
padding-top: 0.05em;
margin-bottom: -0.1em;
}
/* --- Marginalia rail (right-side italic notes) --- */
.editorial-marginalia {
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
font-style: italic;
font-size: 12px;
line-height: 1.5;
color: hsl(var(--muted-foreground));
}
.editorial-marginalia aside {
margin: 0 0 1.5rem 0;
padding-left: 0.75rem;
border-left: var(--editorial-rule);
}
@media (max-width: 768px) {
.editorial-marginalia {
margin-top: 1.5rem;
border-top: var(--editorial-rule);
padding-top: 1rem;
}
}
/* --- Masthead wordmark --- */
.editorial-masthead-wordmark {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 900;
font-style: italic;
letter-spacing: -0.02em;
color: hsl(var(--foreground));
}
.editorial-masthead-kicker {
font-family: var(--font-mono, "JetBrains Mono", "SF Mono", ui-monospace, monospace);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.15em;
color: hsl(var(--muted-foreground));
}
.editorial-masthead-rule {
border: 0;
border-top: var(--editorial-rule);
margin: 0.5rem 0 0 0;
}
/* --- Pull quote — Playfair italic in oxblood --- */
.editorial-pullquote {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-style: italic;
font-size: 38px;
line-height: 1.2;
color: hsl(var(--accent));
margin: 2.5rem auto;
max-width: var(--editorial-measure);
border: none;
}
.editorial-pullquote-attribution {
display: block;
margin-top: 1rem;
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
font-style: normal;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: hsl(var(--muted-foreground));
}
/* --- Section label — small-caps with hairline rule --- */
.editorial-section-label {
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
text-transform: uppercase;
font-variant: small-caps;
font-feature-settings: "smcp";
letter-spacing: 0.15em;
font-size: 13px;
color: hsl(var(--foreground));
}
.editorial-section-label-rule {
border: 0;
border-top: var(--editorial-rule);
margin: 0.5rem 0 1.5rem 0;
}
/* --- Hairline rules --- */
hr.editorial-rule,
.editorial-hairline {
border: 0;
border-top: var(--editorial-rule);
margin: 1.5rem 0;
}
/* --- Byline --- */
.editorial-byline {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0;
border-top: var(--editorial-rule);
border-bottom: var(--editorial-rule);
}
.editorial-byline-photo {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: hsl(var(--muted));
flex-shrink: 0;
object-fit: cover;
}
.editorial-byline-author {
font-family: var(--font-mono, "JetBrains Mono", monospace), var(--font-body, "Source Serif 4", Georgia, serif);
/* The spec calls for Inter for UI bylines. Fallback to Inter then system. */
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-weight: 500;
font-size: 15px;
color: hsl(var(--foreground));
}
.editorial-byline-meta {
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: hsl(var(--muted-foreground));
}
/* --- Override: heading (Playfair, tight tracking, optional small-caps kicker) --- */
.editorial-heading {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 700;
letter-spacing: -0.015em;
color: hsl(var(--foreground));
line-height: 1.15;
}
.editorial-heading-kicker {
display: block;
margin-bottom: 0.5rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 12px;
text-transform: uppercase;
font-variant: small-caps;
font-feature-settings: "smcp";
letter-spacing: 0.18em;
color: hsl(var(--muted-foreground));
}
/* --- Override: text body (Source Serif, hanging quotes) --- */
.editorial-text {
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
color: hsl(var(--foreground));
line-height: 1.7;
max-width: var(--editorial-measure);
}
.editorial-text p + p {
text-indent: 1.5em;
margin-top: 0;
}
.editorial-text blockquote {
hanging-punctuation: first;
font-style: italic;
border-left: var(--editorial-rule);
padding-left: 1rem;
margin-left: 0;
}
/* --- Override: button (hairline border, no fill, accent on hover) --- */
.editorial-button {
display: inline-block;
padding: 0.5rem 1rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.12em;
background-color: transparent;
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
text-decoration: none;
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease;
}
.editorial-button:hover,
.editorial-button:focus {
color: hsl(var(--accent));
border-color: hsl(var(--accent));
}
.editorial-button:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Override: image (Source Serif italic caption with hairline above) --- */
.editorial-figure {
margin: 1.5rem 0;
}
.editorial-figure img {
display: block;
width: 100%;
height: auto;
}
.editorial-figure figcaption {
margin-top: 0.75rem;
padding-top: 0.5rem;
border-top: var(--editorial-rule);
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
font-style: italic;
font-size: 14px;
color: hsl(var(--muted-foreground));
}
/* --- Colophon --- */
.editorial-colophon {
border-top: var(--editorial-rule);
padding-top: 1.5rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 13px;
color: hsl(var(--muted-foreground));
}
.editorial-colophon-issn {
font-family: var(--font-mono, "JetBrains Mono", monospace);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* --- Page layout — narrow editorial column --- */
.editorial-column {
max-width: var(--editorial-measure);
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
.editorial-wide {
max-width: 96rem;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
.editorial-article-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
max-width: 80rem;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 1024px) {
.editorial-article-grid {
grid-template-columns: minmax(0, 64ch) 16rem;
}
}
`

View File

@ -1,248 +0,0 @@
package main
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData carries data parsed off the doc map for the four page templates.
// This is the same shape gotham uses; the renderers below differ only in
// slot composition and column widths.
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 parseEditorialPageData(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,
}
}
// Default editorial page — masthead header, narrow main column, footer.
templ Editorial(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow editorial-column w-full py-8 prose-editorial">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center">
<p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto pt-8 pb-6">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Section-front (landing) — stacked masthead, lead story, river, footer.
templ EditorialLanding(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["masthead"])
</header>
<section class="w-full editorial-wide py-6">
@templ.Raw(data.Slots["lead"])
</section>
<section class="w-full editorial-wide py-6">
@templ.Raw(data.Slots["river"])
</section>
<footer class="w-full mt-auto pt-8 pb-6">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Article — masthead, byline, narrow main column with drop cap, marginalia
// rail, footer. The grid collapses to a single column below 1024px.
templ EditorialArticle(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<div class="editorial-column w-full py-4">
@templ.Raw(data.Slots["byline"])
</div>
<div class="editorial-article-grid flex-grow py-8">
<main class="prose-editorial">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p>
}
</main>
<aside>
@templ.Raw(data.Slots["marginalia"])
</aside>
</div>
<footer class="w-full mt-auto pt-8 pb-6">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Full-width — edge-to-edge content for photo essays and data graphics.
templ EditorialFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="editorial-wide py-20 text-center">
<p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto pt-8 pb-6">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
func RenderEditorial(ctx context.Context, doc map[string]any) templ.Component {
return Editorial(parseEditorialPageData(doc))
}
func RenderEditorialLanding(ctx context.Context, doc map[string]any) templ.Component {
return EditorialLanding(parseEditorialPageData(doc))
}
func RenderEditorialArticle(ctx context.Context, doc map[string]any) templ.Component {
return EditorialArticle(parseEditorialPageData(doc))
}
func RenderEditorialFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return EditorialFullWidth(parseEditorialPageData(doc))
}

View File

@ -1,504 +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 carries data parsed off the doc map for the four page templates.
// This is the same shape gotham uses; the renderers below differ only in
// slot composition and column widths.
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 parseEditorialPageData(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,
}
}
// Default editorial page — masthead header, narrow main column, footer.
func Editorial(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\">")
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{},
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=\"antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</header><main class=\"flex-grow editorial-column w-full py-8 prose-editorial\">")
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\"><p style=\"color: hsl(var(--muted-foreground));\">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 pt-8 pb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</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
})
}
// Section-front (landing) — stacked masthead, lead story, river, footer.
func EditorialLanding(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\">")
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{},
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=\"antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["masthead"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</header><section class=\"w-full editorial-wide py-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["lead"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</section><section class=\"w-full editorial-wide py-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["river"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</section><footer class=\"w-full mt-auto pt-8 pb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</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, 16, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Article — masthead, byline, narrow main column with drop cap, marginalia
// rail, footer. The grid collapses to a single column below 1024px.
func EditorialArticle(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, 17, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
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, 18, "<body class=\"antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<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, 20, "</header><div class=\"editorial-column w-full py-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["byline"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><div class=\"editorial-article-grid flex-grow py-8\"><main class=\"prose-editorial\">")
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, 22, "<p style=\"color: hsl(var(--muted-foreground));\">No content blocks assigned to this page.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</main><aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["marginalia"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</aside></div><footer class=\"w-full mt-auto pt-8 pb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</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, 26, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Full-width — edge-to-edge content for photo essays and data graphics.
func EditorialFullWidth(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, 27, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
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, 28, "<body class=\"antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<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, 30, "</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, 31, "<div class=\"editorial-wide py-20 text-center\"><p style=\"color: hsl(var(--muted-foreground));\">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, 32, "</main><footer class=\"w-full mt-auto pt-8 pb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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
})
}
func RenderEditorial(ctx context.Context, doc map[string]any) templ.Component {
return Editorial(parseEditorialPageData(doc))
}
func RenderEditorialLanding(ctx context.Context, doc map[string]any) templ.Component {
return EditorialLanding(parseEditorialPageData(doc))
}
func RenderEditorialArticle(ctx context.Context, doc map[string]any) templ.Component {
return EditorialArticle(parseEditorialPageData(doc))
}
func RenderEditorialFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return EditorialFullWidth(parseEditorialPageData(doc))
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, serif);">
<div class="editorial-progress" aria-hidden="true"></div>
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
{% if title %}<div class="editorial-column w-full pt-10 md:pt-14"><h1 class="editorial-hero-title">{{ title }}</h1></div>{% endif %}
{% if slots.byline %}<div class="editorial-column w-full pt-4 pb-2">{{ slots.byline|safe }}</div>{% endif %}
<div class="editorial-article-grid flex-grow py-8">
<main class="editorial-dropcap prose-editorial">{% if slots.main %}{{ slots.main|safe }}{% else %}<p style="color: hsl(var(--muted-foreground));">This essay has no content yet.</p>{% endif %}</main>
<aside class="editorial-marginalia">{{ slots.marginalia|safe }}</aside>
</div>
{% 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,18 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, 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="{{ site_settings.Title }} home">
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt }}" class="mx-auto h-12 w-auto">{% else %}<span class="editorial-masthead-wordmark text-3xl">{{ site_settings.Title|default:"Editorial" }}</span>{% endif %}
</a>
<hr class="editorial-rule mx-auto mt-4 mb-8" style="max-width:10rem;">
<div>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="italic" 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="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, 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-10 md:py-14">
{% if title %}
<div class="text-center">
<h1 class="editorial-heading text-4xl md:text-5xl">{{ title }}</h1>
{% if page_meta.MetaDescription %}<p class="mt-3 italic" style="color:hsl(var(--muted-foreground));">{{ page_meta.MetaDescription }}</p>{% endif %}
<hr class="editorial-rule mx-auto mt-6" style="max-width:8rem;">
</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 class="italic" style="color:hsl(var(--muted-foreground));">No essays published yet.</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="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, 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-10 md:py-14">
{% 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-[hsl(var(--border))] md:pl-10">{{ slots.aside|safe }}</aside>
</div>
{% else %}
<div class="mx-auto max-w-2xl">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="italic" 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,13 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full py-10 md:py-14">
{% if slots.main %}<div class="editorial-column prose-editorial">{{ slots.main|safe }}</div>{% else %}<div class="editorial-column py-20 text-center"><p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}
</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="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-6xl mx-auto px-4 py-20 text-center"><p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}</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,14 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, serif);">
{{ admin_banner_html|safe }}
{% if slots.masthead %}<header class="w-full">{{ slots.masthead|safe }}</header>{% endif %}
<main class="flex-grow w-full">
{% if slots.lead %}<section class="max-w-6xl mx-auto px-4 pt-5 pb-8 md:pt-7 md:pb-10">{{ slots.lead|safe }}</section>{% endif %}
{% if slots.river %}<section class="pb-10 md:pb-16"><div class="max-w-3xl mx-auto px-4 space-y-8"><hr class="editorial-rule">{{ slots.river|safe }}</div></section>{% endif %}
</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,74 @@
<!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>
<style type="text/css">
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
@media only screen and (max-width: 620px) {
.email-container { width: 100% !important; max-width: 100% !important; }
.content-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style="background-color: {{ colors.background|default:"#f2ebda" }}; margin: 0; padding: 0; font-family: 'Source Serif 4', 'Source Serif Pro', Georgia, serif;">
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style="padding: 48px 10px; background-color: {{ colors.background|default:"#f2ebda" }};">
<table role="presentation" class="email-container" width="580" cellspacing="0" cellpadding="0" border="0" style="max-width: 580px; background-color: {{ colors.card|default:"#f7f2e6" }}; border: 1px solid {{ colors.border|default:"#d6cdbc" }};">
<!-- Masthead -->
<tr>
<td align="center" style="padding: 32px 40px 16px 40px;">
{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 48px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-family: 'Playfair Display', Georgia, serif; font-style: italic; font-weight: 900; font-size: 34px; color: {{ colors.foreground|default:"#211a14" }}; letter-spacing: -0.02em;">{{ site_name }}</h1>{% endif %}
</td>
</tr>
<!-- Hairline rule -->
<tr>
<td style="padding: 0 40px;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td style="border-top: 1px solid {{ colors.border|default:"#d6cdbc" }}; font-size: 0; line-height: 0; height: 1px;">&nbsp;</td></tr></table>
</td>
</tr>
<!-- Body -->
<tr>
<td class="content-padding" style="padding: 32px 48px 40px 48px; color: {{ colors.foreground|default:"#211a14" }}; font-size: 16px; line-height: 1.7; font-family: 'Source Serif 4', 'Source Serif Pro', Georgia, serif;">{{ body|safe }}</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 24px 48px 40px 48px; border-top: 1px solid {{ colors.border|default:"#d6cdbc" }};">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style="font-family: 'Source Serif 4', Georgia, serif; font-size: 12px; color: {{ colors.mutedForeground|default:"#6b6154" }}; text-transform: uppercase; letter-spacing: 0.12em;">
<p style="margin: 0 0 8px;">{{ site_name }}</p>
{% if site_url %}<p style="margin: 0 0 8px;"><a href="{{ site_url }}" style="color: {{ colors.accent|default:"#8a2e28" }}; text-decoration: none; border-bottom: 1px solid {{ colors.accent|default:"#8a2e28" }};">{{ site_url }}</a></p>{% endif %}
{% if unsubscribe_url %}<p style="margin: 12px 0 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#6b6154" }}; 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 — quiet notice on a hairline panel, small-caps message. Dismiss <script>, data-bn-annkey and localStorage keying preserved verbatim. Fields mirror builtin: variant, message, linkText, linkUrl, dismissable. #}
{% set anncls = "editorial-panel" %}
{% if variant == "promo" %}{% set anncls = "editorial-panel" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
<div data-block-override="editorial:announcement-bar" data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full border-b border-[hsl(var(--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="editorial-caps text-xs">{{ 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 hover:bg-black/10 transition-colors"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
</div>
</div>
{% if dismissable %}<script>
(function(){
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
bars.forEach(function(bar){
bar.setAttribute('data-bn-ann-wired','1');
var key='bnann:'+bar.getAttribute('data-bn-annkey');
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
var btn=bar.querySelector('[data-bn-ann-dismiss]');
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
});
})();
</script>{% endif %}

View File

@ -0,0 +1,16 @@
{# audio-embed override — editorial hairline player card. Fields + facade JS mirror builtin verbatim: url, media, artwork, title, artist, download. #}
<figure data-block-override="editorial:audio-embed" class="bn-audio editorial-panel my-8 overflow-hidden bg-card text-card-foreground{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden 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="editorial-caps truncate text-sm" style="font-family:var(--font-heading, '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="editorial-button inline-flex items-center gap-2 text-sm" 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="editorial-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 (editorial) — typographic login/register in an editorial-panel with small-caps labels, hairline tab rules and oxblood active tab. Wiring UNTOUCHABLE: single shared #auth-message, /api/auth/login (email,password) and /api/auth/register (username,email,password), hx-target #auth-message, captcha injected from _captcha_widget. #}
{% if context.isPublicLoggedIn %}
<div data-block-override="editorial:auth-form" class="auth-form-logged-in mx-auto max-w-md text-center py-8">
<p class="italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">You are signed in as <strong style="color:hsl(var(--foreground));">{{ context.publicUsername }}</strong></p>
<a href="/account" class="editorial-link mt-3 inline-block">My account</a>
</div>
{% else %}
<div data-block-override="editorial:auth-form" class="auth-form editorial-panel mx-auto max-w-md bg-card p-8">
<div id="auth-message"></div>
<div class="mb-6 flex border-b border-[hsl(var(--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="editorial-caps flex-1 py-2 text-center text-xs border-b-2 border-transparent data-[active]:border-[hsl(var(--accent))] data-[active]:text-[hsl(var(--accent))]">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="editorial-caps flex-1 py-2 text-center text-xs border-b-2 border-transparent data-[active]:border-[hsl(var(--accent))] data-[active]:text-[hsl(var(--accent))]">Register</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="editorial-caps text-muted-foreground block text-xs mb-1">Email</label><input type="email" name="email" required class="w-full editorial-input" /></div>
<div class="mb-4"><label class="editorial-caps text-muted-foreground block text-xs mb-1">Password</label><input type="password" name="password" required class="w-full editorial-input" /></div>
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
<button type="submit" class="editorial-button w-full" hx-disabled-elt="this">Sign in</button>
<p class="text-center mt-3 text-sm"><a href="/forgot-password" class="editorial-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="editorial-caps text-muted-foreground block text-xs mb-1">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="w-full editorial-input" /></div>
<div class="mb-3"><label class="editorial-caps text-muted-foreground block text-xs mb-1">Email</label><input type="email" name="email" required class="w-full editorial-input" /></div>
<div class="mb-4"><label class="editorial-caps text-muted-foreground block text-xs mb-1">Password</label><input type="password" name="password" required minlength="8" class="w-full editorial-input" /></div>
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
<button type="submit" class="editorial-button w-full" hx-disabled-elt="this">Create account</button>
</form>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,31 @@
{# auth-status (editorial) — header login state; small-caps links, hairline monogram and quiet editorial-panel dropdown. Driven by context.isPublicLoggedIn / context.publicDisplayName / context.publicUsername; logout posts /api/auth/logout. #}
{% if context.isPublicLoggedIn %}
<div data-block-override="editorial:auth-status" class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="editorial-caps flex items-center gap-2 text-xs 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="border:1px solid hsl(var(--border));color:hsl(var(--accent));">{{ context.publicDisplayName|default:context.publicUsername|first|upper }}</span>
<span>{{ context.publicDisplayName|default:context.publicUsername }}</span>
<svg 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 editorial-panel bg-card py-1 z-50">
<a href="/account" class="block px-4 py-2 text-sm text-foreground hover:bg-accent">My profile</a>
<a href="/account/reviews" class="block px-4 py-2 text-sm text-foreground hover:bg-accent">My reviews</a>
<hr class="my-1 editorial-rule"/>
<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-accent">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="editorial:auth-status" class="auth-status flex items-center gap-2">
<a href="/login" class="editorial-caps text-xs px-3 py-1.5 opacity-80 hover:opacity-100 hover:text-[hsl(var(--accent))] transition-colors">Sign in</a>
<a href="/login?tab=register" class="editorial-button text-xs">Reserve</a>
</div>
{% endif %}

View File

@ -0,0 +1,22 @@
{# author-bio-hero (editorial) — author-page header framed by hairline rules over generous whitespace. #}
{% if author %}
<section data-block-override="editorial:author-bio-hero" class="editorial-rule bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
{% if showAvatar and author.avatar_url %}<div class="mb-6 h-24 w-24 overflow-hidden rounded-full bg-muted ring-1 ring-[hsl(var(--border))]">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="mb-4 flex items-center justify-center gap-4"><span class="editorial-rule w-12"></span><span class="editorial-rule w-12"></span></div>
<h1 class="editorial-heading text-3xl md:text-4xl text-foreground" style="font-family:var(--font-heading, &quot;Playfair Display&quot;, Georgia, serif)">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="editorial-caps mt-2 text-sm text-muted-foreground">{{ 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, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, 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-[hsl(var(--accent))]" title="Website"><svg class="h-5 w-5" width="20" height="20" 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-[hsl(var(--accent))]" title="Twitter"><svg class="h-5 w-5" width="20" height="20" 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-[hsl(var(--accent))]" title="LinkedIn"><svg class="h-5 w-5" width="20" height="20" 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-[hsl(var(--accent))]" title="GitHub"><svg class="h-5 w-5" width="20" height="20" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,14 @@
{# author-bio (editorial) — byline card in an editorial-panel with hairline rule and small-caps label. #}
{% if post.author_name %}
<aside data-block-override="editorial:author-bio" class="editorial-panel bg-card p-6 text-card-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="editorial-caps mb-4 text-xs text-muted-foreground">{{ heading }}</p>{% endif %}
<div class="flex items-start gap-4">
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full bg-muted ring-1 ring-[hsl(var(--border))]">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="min-w-0">
<h3 class="editorial-caps text-base text-card-foreground" style="font-family:var(--font-heading, &quot;Playfair Display&quot;, Georgia, serif)">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-[hsl(var(--accent))]">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
<span class="editorial-rule mt-2 block w-10"></span>
{% if post.author_bio %}<p class="mt-2 text-sm text-muted-foreground" style="font-family:var(--font-body, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, serif)">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -0,0 +1,24 @@
{# blog-hero (editorial) — blog-index header + featured posts from the `posts` provider; typographic, hairline rules. #}
<section data-block-override="editorial:blog-hero" class="bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<div class="mx-auto max-w-2xl text-center">
{% if title %}<h1 class="editorial-caps editorial-underline text-4xl md:text-5xl text-foreground" style="font-family:var(--font-heading, &quot;Playfair Display&quot;, Georgia, serif)">{{ title }}</h1>{% endif %}
{% if subtitle %}<p class="mt-4 text-lg text-muted-foreground" style="font-family:var(--font-body, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, 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="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 %}">
<span class="editorial-rule mb-4 block w-12"></span>
<h2 class="editorial-caps {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} leading-snug text-foreground" style="font-family:var(--font-heading, &quot;Playfair Display&quot;, Georgia, serif)"><a href="{{ post.url }}" class="transition-colors hover:text-[hsl(var(--accent))]">{{ post.title }}</a></h2>
{% if showExcerpt and post.excerpt %}<p class="mt-3 text-sm text-muted-foreground" style="font-family:var(--font-body, &quot;Source Serif 4&quot;, &quot;Source Serif Pro&quot;, Georgia, 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="editorial-caps text-[hsl(var(--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>

View File

@ -0,0 +1,13 @@
{# breadcrumbs (editorial) — small-caps trail with hairline oxblood separators; current item plain. #}
{% set arrows = template == "arrows" %}
<nav aria-label="Breadcrumb" data-block-override="editorial:breadcrumbs" class="py-3{% if class %} {{ class }}{% endif %}">
<ol class="flex flex-wrap items-center gap-1 text-xs">
{% if showHome %}<li class="flex items-center"><a href="/" class="editorial-caps text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
{% for item in breadcrumbs %}
<li class="flex items-center">
{% if showHome or not forloop.First %}{% if arrows %}<svg class="mx-2 h-3.5 w-3.5 text-[hsl(var(--accent))]" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>{% else %}<span class="mx-2 text-[hsl(var(--accent))]">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
{% if item.is_current %}{% if showCurrent %}<span class="editorial-caps text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="editorial-caps text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]">{{ item.label }}</a>{% endif %}
</li>
{% endfor %}
</ol>
</nav>

View File

@ -0,0 +1,10 @@
{# button override — hairline uppercase button; ghost/link/secondary styles render as an inline oxblood underline link. Fields: link, style, label, size, icon, iconPosition, newTab, align. #}
{% with align=align|default:"left" %}
<div data-block-override="editorial:button" class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}">
{% if style == "ghost" or style == "link" or style == "secondary" %}
<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="editorial-link inline-flex items-center gap-2 text-sm">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>
{% else %}
<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="editorial-button inline-flex items-center gap-2">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>
{% endif %}
</div>
{% endwith %}

View File

@ -0,0 +1,4 @@
{# card override — editorial hairline panel; accent border on hover, oxblood link. Fields mirror builtin: layout, link, media, title, text, linkLabel. Media via {% img %}. #}
{% with layout=layout|default:"vertical" %}
<article data-block-override="editorial:card" class="bn-card editorial-panel overflow-hidden bg-card text-card-foreground transition-colors hover:border-[hsl(var(--accent))] {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">{% if link %}<a href="{{ link }}" class="contents">{% endif %}{% if media %}<div class="overflow-hidden bg-muted {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}<div class="flex flex-col gap-2 p-5 md:p-6 {% if layout == "horizontal" %}justify-center{% endif %}">{% if title %}<h3 class="editorial-heading text-lg" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</h3>{% endif %}{% if text %}<div class="text-sm leading-relaxed" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="mt-2 inline-flex items-center gap-1 text-sm editorial-link">{{ linkLabel }}::lucide:arrow-right:sm::</span>{% endif %}</div>{% if link %}</a>{% endif %}</article>
{% endwith %}

View File

@ -0,0 +1,18 @@
{# category-list (editorial) — from the `categories` provider. hairline list or bordered chips. #}
{% set pills = style == "pills" %}
<section data-block-override="editorial:category-list" class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="editorial-caps mb-5 text-xs text-muted-foreground">{{ heading }}</h2>{% endif %}
{% if pills %}
<div class="flex flex-wrap gap-2">
{% for cat in categories %}
<a href="{{ cat.url }}" class="editorial-caps inline-flex items-center gap-1.5 rounded-none border border-[hsl(var(--border))] bg-card px-4 py-1.5 text-xs text-card-foreground transition-colors hover:border-[hsl(var(--accent))] hover:text-[hsl(var(--accent))]">{{ cat.name }}{% if showCount %}<span class="text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a>
{% endfor %}
</div>
{% else %}
<ul class="space-y-1">
{% for cat in categories %}
<li><a href="{{ cat.url }}" class="editorial-rule flex items-center justify-between px-3 py-2.5 text-sm text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]"><span class="editorial-caps">{{ cat.name }}</span>{% if showCount %}<span class="rounded-none border border-[hsl(var(--border))] px-2 py-0.5 text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a></li>
{% endfor %}
</ul>
{% endif %}
</section>

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