Compare commits

..

No commits in common. "main" and "v0.1.1" have entirely different histories.
main ... v0.1.1

143 changed files with 4038 additions and 2686 deletions

1
.gitignore vendored
View File

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

View File

@ -1,55 +1,33 @@
# magazine-bold — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
# Magazine Bold — local build helpers
#
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
# classifies the repo as codeless (no Go at the root) and packs the artifact
# with manifest.pb (codeless: true) and NO plugin.wasm.
# The plugin compiles to a .so shared object loaded by the CMS at runtime.
#
# Usage:
# make build # pack magazine-bold-<version>.bnp (codeless — no wasm)
# make verify # validate the built .bnp (loader-identical checks)
# make archive-check # prove a clean `git archive HEAD` still builds codeless
# make publish # ninja plugin publish --bnp (to the active registry)
# make clean # remove build artefacts
# make # Build magazine-bold.so locally (default)
# make templ # Regenerate templ Go files
# make clean # Remove the built .so
.PHONY: build verify archive-check publish clean help
.PHONY: all templ clean help
PLUGIN_NAME := magazine-bold
NINJA := ninja
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
TEMPL := $(HOME)/go/bin/templ
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
build:
$(NINJA) plugin build --dir .
# Default target: build the .so locally for development.
all: $(PLUGIN_NAME).so
# Validate the built .bnp exactly as the registry/loader will.
verify:
$(NINJA) plugin verify $(BNP)
# Local plugin build (no container). Useful for CI / quick checks.
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
archive-check:
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
trap 'rm -rf "$$T"' EXIT && \
git archive HEAD | tar -x -C "$$T" && \
cd "$$T" && $(NINJA) plugin build --dir . && \
echo "archive-check OK"
# Regenerate templ Go files locally (for development).
templ:
$(TEMPL) generate
# 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)-*.bnp plugin.wasm
rm -f $(PLUGIN_NAME).so
help:
@echo "Targets:"
@echo " build Pack the codeless .bnp (no wasm)"
@echo " verify Validate the built .bnp (loader-identical checks)"
@echo " archive-check Prove a clean git archive HEAD packs codeless"
@echo " publish Publish the .bnp to the active registry"
@echo " clean Remove build artefacts"
@echo " all Build $(PLUGIN_NAME).so locally (default)"
@echo " templ Regenerate templ Go files locally"
@echo " clean Remove the built .so"

View File

@ -1,24 +0,0 @@
# Magazine Bold
A streetwear label's cover-story chrome. Massive condensed uppercase headlines run over duotone imagery, with grids that overlap and break on purpose. Fluoro accent slabs cut through the layout. It reads like newsprint plugged into the mains by day, and turns ink-black with fluoro highlights at night.
## Good for
We built Magazine Bold for fashion and culture publishing. Treat these as starting points, not limits. If the mood fits your brand, use it:
- Streetwear labels and product drops
- Lookbooks and seasonal campaigns
- Culture and music magazines
- Photographers and stylists showing a portfolio
## What you get
- Five color presets, each with a tuned light mode and a tuned dark mode: Newsprint & Voltage, Ink & Electric Blue, Chalk & Lime, Flux Green, and Signal Orange.
- Archivo Black for headlines, Inter for body, IBM Plex Mono for captions and credits. All three are bundled. Fonts stay admin-controlled, so you can swap them from the typography settings.
- Four persona blocks: a full-bleed cover story, a scrolling credits ticker, a drop calendar with dates, and a lookbook gallery.
- A demo site that installs when you activate the theme: a home page, an about page, a short blog, and a contact page whose form emails your admins.
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
## Install
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully built starting point.

View File

@ -1,44 +1,56 @@
# Magazine Bold — fonts
# Magazine Bold — Recommended fonts
This theme **bundles** a Latin subset of three open-licence (SIL OFL 1.1)
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.
Per `themes/docs/FONTS.md` (wave-1 policy), this theme ships `fonts.json = []`
and provides CSS fallback stacks via `--font-heading`, `--font-body`,
`--font-mono`. The site admin assigns real fonts via the typography settings
panel; the recommendations below match the spec §5 intent.
| Slot | Bundled family | Weights / styles | Role |
|---------|----------------|------------------------|---------------------------------------------------------------|
| Heading | Archivo Black | 400 normal | Massive condensed uppercase cover headlines and the wordmark |
| Body | Inter | 400 / 500 / 700 normal | Body copy, decks and UI text |
| Mono | IBM Plex Mono | 400 / 500 normal | Kickers, datelines, folio numbers, captions and button labels |
## Display (heading slot)
The bundled woff2 files and their licence record live in `assets/fonts/`
(`LICENSES.md`). 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.
Spec calls for **PP Editorial New** (Regular Italic + Ultralight) at 180280pt
for cover-story hits, with **Migra** as a secondary serif drop-in. Both are
commercial; the open-source picks below render the same italic-display energy.
## Assigning the bundled faces
Open-source picks (assign one to **Heading**):
1. In the BlockNinja admin, open **Settings → Theme → Typography**.
2. In the **Bundled** tab, assign **Archivo Black** to the *Heading* slot.
3. Assign **Inter** to the *Body* slot.
4. Assign **IBM Plex Mono** to the *Mono* slot.
- `google:Playfair Display` — the closest open-source approximation of
PP Editorial New's contrast and italic axis. Open the typography panel,
pick "Playfair Display" from the Google Fonts tab, assign to Heading.
- `google:Fraunces` — a more contemporary alternative with optical sizing;
works well as the secondary serif (`font-serif-sub`).
- `google:Cormorant Garamond` — slightly higher-contrast classic serif if
you want a more traditional broadsheet feel.
## Optional swaps (Google Fonts tab)
Commercial picks (require admin licensing + upload):
The identity is condensed-heavy display + neutral sans body + mono details.
Any of these drop in via the CSS variables with no template change:
- `upload:PP Editorial New` — purchase from pangrampangram.com, upload via the
font picker's "Upload" tab.
- `upload:Migra` — purchase from pangrampangram.com (Migra is a sibling
family), upload via the picker.
- **Anton** (`google:Anton`) — a taller, more condensed display face for the
*Heading* slot; the loudest cover-headline option and the WO's alternate.
- **Archivo** (`google:Archivo`) — for the *Body* slot if you want the body to
echo the display family.
- **Space Grotesk** (`google:Space Grotesk`) — an alternative body / deck face.
- **Space Mono** (`google:Space Mono`) — a chunkier mono for the *Mono* slot.
## Body
## Bundling a commercial display face (out of scope for Wave A)
Spec calls for **Inter** 1618px with tight leading and generous tracking on
small-caps kickers. Inter is already in the curated Google Fonts list:
If the brand locks in a licensed display face in place of Archivo Black, 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.
- `google:Inter` — open the typography panel, pick "Inter" from the Google
Fonts tab, assign to Body. Aim for weights 400 + 600.
## Mono (captions, folios, metadata only)
Spec calls for **JetBrains Mono** at caption sizes for kickers, folios and
the colophon strip. Curated:
- `google:JetBrains Mono` — open the typography panel, pick "JetBrains Mono"
from the Google Fonts tab, assign to Mono. Weight 400 is enough.
Alternates: `google:Space Mono`, `google:IBM Plex Mono`.
## How the theme degrades when nothing is picked
The compiled CSS shipped with this `.so` uses the variables with sensible
fallbacks so a fresh install still reads like an editorial magazine:
- `--font-heading` falls back to `"Playfair Display", "Cormorant Garamond", Georgia, "Times New Roman", serif`
- `--font-body` falls back to `"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`
- `--font-mono` falls back to `"JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`

View File

@ -1,30 +0,0 @@
# Recommended Icons — Magazine Bold
Magazine Bold is a typographic, rule-heavy theme. It leans on condensed
headlines, mono labels and heavy rules, not iconography, so it references only
two bundled packs. `plugin.mod` declares them in `required_icon_packs`; until
the loader auto-installs declared packs, an admin should confirm they are
enabled before activation.
## Required packs
- **lucide** — ships bundled with the CMS (v0.460). Enabled by default. Used
for chrome and UI glyphs (arrows, chevrons, close, check, play, map-pin,
navigation, mail, rss, star, flag). If icons render as empty boxes, open
`/admin/icons` and confirm **Lucide** is installed and enabled.
- **simple-icons** — ships bundled with the CMS (v14.0). Used only for brand
glyphs in `author-bio-hero`, `author-bio` and `social-links` (x, linkedin,
github). Confirm it is enabled at `/admin/icons` if brand marks are blank.
## Icons used by the theme chrome and overrides
All via the sprite pipeline (`<svg><use href="/icons/<pack>.svg#<name>"/></svg>`
or the `::<pack>:<name>::` shorthand):
`arrow-right`, `chevron-down`, `chevron-left`, `chevron-right`, `x`, `check`,
`play`, `download`, `map-pin`, `navigation`, `mail`, `rss`, `star`, `flag`,
`diamond`, `globe` (lucide); `x`, `linkedin`, `github` (simple-icons).
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.

View File

@ -1,17 +0,0 @@
# Bundled font licenses
All bundled woff2 files in this directory are latin-subset builds redistributed
under the SIL Open Font License 1.1 (OFL). No modifications were made beyond
subsetting to the latin range.
| Family | Files | License | Source |
|--------|-------|---------|--------|
| Archivo Black | archivo-black-400.woff2 | SIL OFL 1.1 | Omnibus-Type / Google Fonts |
| Inter | inter-400.woff2, inter-500.woff2, inter-700.woff2 | SIL OFL 1.1 | Rasmus Andersson / Google Fonts |
| IBM Plex Mono | ibm-plex-mono-400.woff2, ibm-plex-mono-500.woff2 | SIL OFL 1.1 | IBM / Google Fonts |
Full OFL text: https://openfontlicense.org/. Font-family is still admin-controlled;
these bundled families appear as `source=template` rows in the fonts picker and
are consumed only via `var(--font-heading|body|mono, <fallback>)`. Archivo Black
drives the condensed display headlines, Inter the body, IBM Plex Mono the
kickers, datelines and captions.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,34 +0,0 @@
# Magazine Bold theme block definitions (codeless .bnp, WO-TF-021).
#
# These are the theme's TRUE persona blocks — streetwear / fashion-drop
# furniture with no builtin equivalent. Everything the theme used to ship that
# duplicated a builtin (masthead, photo essay, pull quote, issue archive,
# colophon) is now a re-skin OVERRIDE of a builtin (navbar, gallery, quote,
# post-index, footer) under templates/overrides/magazine-bold/. Keys are fully
# qualified (magazine-bold:<key>) because the codeless loader registers
# definitions verbatim.
blocks:
- key: magazine-bold:cover_story
title: Cover Story
description: Asymmetric magazine cover — mono folio kicker, massive condensed headline, deck and a duotone cover image with a shop/read CTA.
category: theme
schema: cover_story.schema.json
template: cover_story.ninjatpl
- key: magazine-bold:lookbook
title: Lookbook
description: Grid of duotone look frames with mono numbering, captions and optional shop links — the drop's lookbook spread.
category: theme
schema: lookbook.schema.json
template: lookbook.ninjatpl
- key: magazine-bold:drop_calendar
title: Drop Calendar
description: Dated list of upcoming and past drops — mono datelines, condensed names and a live/soon/sold status tag.
category: theme
schema: drop_calendar.schema.json
template: drop_calendar.ninjatpl
- key: magazine-bold:credits_ticker
title: Credits Ticker
description: Marquee strip of credits (photographer, stylist, cast) — MID-motion scroll, paused on hover, static under prefers-reduced-motion.
category: theme
schema: credits_ticker.schema.json
template: credits_ticker.ninjatpl

View File

@ -1,32 +0,0 @@
{# cover_story (magazine-bold) — asymmetric cover: mono folio kicker (top, always in frame), massive condensed headline CLAMPED to its reserved 8-col track so it never crosses the deck or bleeds off-viewport, deck in a reserved right column, duotone cover image, shop/read CTA. Fields: kicker, headline (richtext), deck (richtext), image (media), link, linkLabel. Image via {% img %} (media filter not registered in block render path). MID motion: mb-reveal + duotone hover, reduced-motion safe. #}
<section data-block="magazine-bold:cover_story" class="w-full overflow-hidden py-10 md:py-14">
<div class="mx-auto max-w-6xl px-4 sm:px-6">
{% if kicker %}<div class="mb-caps mb-4 md:mb-6 text-[hsl(var(--accent))]">{{ kicker }}</div>{% endif %}
<div class="grid grid-cols-1 gap-y-6 gap-x-8 lg:grid-cols-12 lg:items-end">
<div class="min-w-0 lg:col-span-8"><h1 class="mb-cover-folio text-foreground">{% if headline %}{{ headline|safe }}{% else %}Cover story{% endif %}</h1></div>
<div class="min-w-0 lg:col-span-4 lg:pb-2">
{% if deck %}<div class="text-muted-foreground" style="font-family:var(--font-body, 'Inter', system-ui, sans-serif);font-size:clamp(1rem,1.25vw,1.35rem);line-height:1.35;">{{ deck|safe }}</div>{% endif %}
{% if link %}<div class="mt-6"><a href="{{ link }}" class="mb-button">{{ linkLabel|default:"Read the story" }}</a></div>{% endif %}
</div>
</div>
{% if image %}<div class="mb-reveal mb-duotone mt-8 md:mt-10 aspect-[16/9] w-full overflow-hidden">{% img image alt="" class="h-full w-full object-cover" layout="hero" %}</div>{% endif %}
</div>
</section>
<script>
(function(){
if(window.__mbReveal)return;window.__mbReveal=1;
function run(){
var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var els=document.querySelectorAll('.mb-reveal');
if(!els.length)return;
if(reduce||!('IntersectionObserver' in window)){els.forEach(function(el){el.classList.add('mb-in');});return;}
els.forEach(function(el){el.classList.add('mb-armed');});
var io=new IntersectionObserver(function(entries){entries.forEach(function(e){if(e.isIntersecting){e.target.classList.add('mb-in');io.unobserve(e.target);}});},{threshold:0.12,rootMargin:'0px 0px -8% 0px'});
els.forEach(function(el){io.observe(el);});
var revealAll=function(){els.forEach(function(el){el.classList.add('mb-in');});};
setTimeout(revealAll,900);
window.addEventListener('load',revealAll,{once:true});
}
if(document.readyState!='loading')run();else document.addEventListener('DOMContentLoaded',run);
})();
</script>

View File

@ -1,10 +0,0 @@
{# credits_ticker (magazine-bold) — marquee strip of credits. Fields: items[].label. MID motion: CSS marquee, paused on hover, static under prefers-reduced-motion. The track renders the item list TWICE so the -50% translate loops seamlessly (engine can't repeat N times). #}
{% if items %}
<section data-block="magazine-bold:credits_ticker" class="colorblock-accent w-full border-y-2 border-[hsl(var(--foreground))] py-3">
<div class="mb-marquee">
<div class="mb-marquee-track">
{% for item in items %}<span class="mb-caps px-6 text-[hsl(var(--accent-foreground))]">{{ item.label }}</span><span aria-hidden="true" class="text-[hsl(var(--accent-foreground))]">/</span>{% endfor %}{% for item in items %}<span class="mb-caps px-6 text-[hsl(var(--accent-foreground))]">{{ item.label }}</span><span aria-hidden="true" class="text-[hsl(var(--accent-foreground))]">/</span>{% endfor %}
</div>
</div>
</section>
{% endif %}

View File

@ -1,21 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Credits Ticker",
"description": "Marquee strip of credits scrolling across a fluoro accent band.",
"type": "object",
"properties": {
"items": {
"type": "array",
"title": "Credits",
"description": "The credit lines shown in the ticker.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"label": { "type": "string", "title": "Credit", "description": "One credit (e.g. \"Photography — Kit Sato\").", "x-editor": "text" }
}
}
}
}
}

View File

@ -1,23 +0,0 @@
{# drop_calendar (magazine-bold) — dated list of drops: mono datelines, condensed names, a status tag. Fields: heading, intro, drops[].date/.name/.detail/.status(live|soon|sold)/.link/.linkLabel. Static, no motion. #}
<section data-block="magazine-bold:drop_calendar" class="w-full overflow-hidden py-10 md:py-12">
<div class="mx-auto max-w-4xl px-4 sm:px-6">
{% if heading %}<h2 class="mb-heading text-3xl md:text-5xl text-foreground">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mt-4 max-w-2xl text-deck text-muted-foreground">{{ intro }}</p>{% endif %}
{% if drops %}
<ul class="mt-10 border-t-2 border-[hsl(var(--foreground))]">
{% for drop in drops %}
<li class="flex flex-col gap-3 border-b-2 border-[hsl(var(--foreground))] py-5 sm:flex-row sm:items-center sm:gap-8">
<div class="mb-caps shrink-0 text-[hsl(var(--accent))] sm:w-40">{{ drop.date }}</div>
<div class="min-w-0 flex-1">
<h3 class="mb-heading text-xl md:text-2xl text-foreground">{% if drop.link %}<a href="{{ drop.link }}" class="transition-colors hover:text-[hsl(var(--accent))]">{{ drop.name }}</a>{% else %}{{ drop.name }}{% endif %}</h3>
{% if drop.detail %}<p class="mt-1 text-sm text-muted-foreground">{{ drop.detail }}</p>{% endif %}
</div>
{% if drop.status == "live" %}<span class="mb-caps shrink-0 bg-[hsl(var(--accent))] px-3 py-1 text-[hsl(var(--accent-foreground))]">Live</span>
{% elif drop.status == "sold" %}<span class="mb-caps shrink-0 border-2 border-[hsl(var(--border))] px-3 py-1 text-muted-foreground">Sold out</span>
{% elif drop.status == "soon" %}<span class="mb-caps shrink-0 border-2 border-[hsl(var(--foreground))] px-3 py-1 text-foreground">Coming soon</span>{% endif %}
</li>
{% endfor %}
</ul>
{% else %}<div class="mb-caption py-12">No drops scheduled.</div>{% endif %}
</div>
</section>

View File

@ -1,45 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Drop Calendar",
"description": "Dated list of upcoming and past drops with a status tag.",
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Heading",
"description": "Section headline.",
"x-editor": "text"
},
"intro": {
"type": "string",
"title": "Intro",
"description": "Optional standfirst line.",
"x-editor": "textarea"
},
"drops": {
"type": "array",
"title": "Drops",
"description": "The scheduled drops.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"date": { "type": "string", "title": "Date", "description": "Dateline (e.g. \"14 MAR\").", "x-editor": "text" },
"name": { "type": "string", "title": "Name", "description": "Drop name.", "x-editor": "text" },
"detail": { "type": "string", "title": "Detail", "description": "Optional one-line detail.", "x-editor": "text" },
"status": {
"type": "string",
"title": "Status",
"description": "Availability tag.",
"enum": ["live", "soon", "sold"],
"default": "soon",
"x-editor": "select"
},
"link": { "type": "string", "title": "Link", "description": "Optional link to the drop.", "x-editor": "link" },
"linkLabel": { "type": "string", "title": "Link Label", "description": "Link label.", "x-editor": "text", "default": "View drop" }
}
}
}
}
}

View File

@ -1,42 +0,0 @@
{# lookbook (magazine-bold) — grid of duotone look frames with mono numbering, captions and optional shop links. Fields: heading, intro, looks[].image/.caption/.credit/.link/.linkLabel. Image via {% img %} (media filter not registered in block render path). MID motion: duotone hover + mb-reveal, reduced-motion safe. #}
<section data-block="magazine-bold:lookbook" class="w-full overflow-hidden py-10 md:py-12">
<div class="mx-auto max-w-6xl px-4 sm:px-6">
{% if heading %}<h2 class="mb-heading text-3xl md:text-5xl text-foreground">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mt-4 max-w-2xl text-deck text-muted-foreground">{{ intro }}</p>{% endif %}
{% if looks %}
<div class="mt-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{% for look in looks %}
<figure class="mb-reveal flex flex-col">
{% if look.image %}<div class="mb-duotone aspect-[3/4] w-full overflow-hidden">{% img look.image alt=look.caption|default:"" class="h-full w-full object-cover" %}</div>{% endif %}
<figcaption class="mt-3 flex items-baseline gap-3">
<span class="mb-caps text-[hsl(var(--accent))]">{{ forloop.Counter|stringformat:"%02d" }}</span>
<span class="min-w-0">
{% if look.caption %}<span class="block text-sm font-medium text-foreground">{{ look.caption }}</span>{% endif %}
{% if look.credit %}<span class="mb-caps block text-muted-foreground">{{ look.credit }}</span>{% endif %}
</span>
</figcaption>
{% if look.link %}<a href="{{ look.link }}" class="mb-link mt-2 inline-block text-sm">{{ look.linkLabel|default:"Shop the look" }}</a>{% endif %}
</figure>
{% endfor %}
</div>
{% else %}<div class="mb-caption py-12">No looks yet.</div>{% endif %}
</div>
</section>
<script>
(function(){
if(window.__mbReveal)return;window.__mbReveal=1;
function run(){
var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var els=document.querySelectorAll('.mb-reveal');
if(!els.length)return;
if(reduce||!('IntersectionObserver' in window)){els.forEach(function(el){el.classList.add('mb-in');});return;}
els.forEach(function(el){el.classList.add('mb-armed');});
var io=new IntersectionObserver(function(entries){entries.forEach(function(e){if(e.isIntersecting){e.target.classList.add('mb-in');io.unobserve(e.target);}});},{threshold:0.12,rootMargin:'0px 0px -8% 0px'});
els.forEach(function(el){io.observe(el);});
var revealAll=function(){els.forEach(function(el){el.classList.add('mb-in');});};
setTimeout(revealAll,900);
window.addEventListener('load',revealAll,{once:true});
}
if(document.readyState!='loading')run();else document.addEventListener('DOMContentLoaded',run);
})();
</script>

View File

@ -1,37 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Lookbook",
"description": "Grid of duotone look frames for a drop — numbered, captioned, optionally shoppable.",
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Heading",
"description": "Section headline.",
"x-editor": "text"
},
"intro": {
"type": "string",
"title": "Intro",
"description": "Optional standfirst line.",
"x-editor": "textarea"
},
"looks": {
"type": "array",
"title": "Looks",
"description": "The lookbook frames.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"image": { "type": "string", "title": "Image", "description": "Look photo.", "x-editor": "media" },
"caption": { "type": "string", "title": "Caption", "description": "Look name / description.", "x-editor": "text" },
"credit": { "type": "string", "title": "Credit", "description": "Optional credit line (model, photographer).", "x-editor": "text" },
"link": { "type": "string", "title": "Shop Link", "description": "Optional link to the product.", "x-editor": "link" },
"linkLabel": { "type": "string", "title": "Link Label", "description": "Shop link label.", "x-editor": "text", "default": "Shop the look" }
}
}
}
}
}

33
button_override.go Normal file
View File

@ -0,0 +1,33 @@
package main
import (
"bytes"
"context"
)
// ButtonOverrideBlock renders the built-in button block with the Magazine Bold
// squared-ink treatment and accent hover.
//
// Built-in button content shape: {"label": "...", "url": "...", "variant": "primary|secondary|destructive"}
func ButtonOverrideBlock(ctx context.Context, content map[string]any) string {
label := getString(content, "label")
if label == "" {
// fall back to "text" — some older button blocks used that key.
label = getString(content, "text")
}
url := getString(content, "url")
if url == "" {
url = getString(content, "href")
}
variant := getString(content, "variant")
switch variant {
case "secondary", "destructive", "primary":
// ok
default:
variant = "primary"
}
var buf bytes.Buffer
_ = mbButtonComponent(label, url, variant).Render(ctx, &buf)
return buf.String()
}

13
button_override.templ Normal file
View File

@ -0,0 +1,13 @@
package main
// mbButtonComponent renders a squared-ink button with accent hover.
// All variants get the visible 2px ring on :focus-visible from .mb-button.
templ mbButtonComponent(label, url, variant string) {
if label != "" {
<a
href={ templ.SafeURL(url) }
class={ "mb-button", "mb-button-" + variant }
data-variant={ variant }
>{ label }</a>
}
}

101
button_override_templ.go Normal file
View File

@ -0,0 +1,101 @@
// 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"
// mbButtonComponent renders a squared-ink button with accent hover.
// All variants get the visible 2px ring on :focus-visible from .mb-button.
func mbButtonComponent(label, url, variant string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if label != "" {
var templ_7745c5c3_Var2 = []any{"mb-button", "mb-button-" + variant}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(url))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 8, Col: 28}
}
_, 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, 2, "\" 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_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_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, 3, "\" data-variant=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(variant)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 10, Col: 25}
}
_, 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, 4, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 11, Col: 10}
}
_, 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, 5, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

35
colophon.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// ColophonBlockMeta is the mono caption strip with credits and optional social row.
var ColophonBlockMeta = blocks.BlockMeta{
Key: "colophon",
Title: "Colophon",
Description: "Mono caption strip with credits and an optional social row.",
Source: "magazine-bold",
Category: blocks.CategoryLayout,
}
// ColophonData is what the templ component consumes.
type ColophonData struct {
Credits string
ShowSocial bool
}
// ColophonBlock renders the colophon strip.
// content keys: credits (richtext), showSocial (select "true"/"false").
func ColophonBlock(ctx context.Context, content map[string]any) string {
data := ColophonData{
Credits: getString(content, "credits"),
ShowSocial: getBool(content, "showSocial", false),
}
var buf bytes.Buffer
_ = colophonComponent(data).Render(ctx, &buf)
return buf.String()
}

19
colophon.templ Normal file
View File

@ -0,0 +1,19 @@
package main
// colophonComponent renders the mono colophon strip.
templ colophonComponent(data ColophonData) {
<div data-block="magazine-bold:colophon" class="py-8 font-mono mb-caption">
if data.Credits != "" {
<div class="mb-4">
@templ.Raw(data.Credits)
</div>
}
if data.ShowSocial {
<ul class="flex items-center gap-6 mb-hairline-top pt-4 mt-4">
<li><a href="#" class="hover:text-foreground">Instagram</a></li>
<li><a href="#" class="hover:text-foreground">Twitter</a></li>
<li><a href="#" class="hover:text-foreground">RSS</a></li>
</ul>
}
</div>
}

65
colophon_templ.go Normal file
View File

@ -0,0 +1,65 @@
// 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 mono colophon strip.
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 data-block=\"magazine-bold:colophon\" class=\"py-8 font-mono mb-caption\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Credits != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Credits).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ShowSocial {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<ul class=\"flex items-center gap-6 mb-hairline-top pt-4 mt-4\"><li><a href=\"#\" class=\"hover:text-foreground\">Instagram</a></li><li><a href=\"#\" class=\"hover:text-foreground\">Twitter</a></li><li><a href=\"#\" class=\"hover:text-foreground\">RSS</a></li></ul>")
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
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

41
cover_story.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// CoverStoryBlockMeta is the 12-column asymmetric hero anchoring the issue landing.
var CoverStoryBlockMeta = blocks.BlockMeta{
Key: "cover_story",
Title: "Cover Story",
Description: "12-column asymmetric hero with kicker, oversized display headline, deck and cover image.",
Source: "magazine-bold",
Category: blocks.CategoryContent,
}
// CoverStoryData is what the templ component consumes.
type CoverStoryData struct {
Kicker string
Headline string
Deck string
Image string
Link string
}
// CoverStoryBlock renders the cover-story hero.
// content keys: kicker, headline (richtext), deck (richtext), image (media), link.
func CoverStoryBlock(ctx context.Context, content map[string]any) string {
data := CoverStoryData{
Kicker: getString(content, "kicker"),
Headline: getString(content, "headline"),
Deck: getString(content, "deck"),
Image: blocks.ResolveMediaPath(getString(content, "image")),
Link: getString(content, "link"),
}
var buf bytes.Buffer
_ = coverStoryComponent(data).Render(ctx, &buf)
return buf.String()
}

46
cover_story.templ Normal file
View File

@ -0,0 +1,46 @@
package main
// coverStoryComponent renders the 12-column asymmetric cover hero.
// Note: the spec calls for an oversized display headline; this consumes
// `--font-heading` via .font-display and clamps via .text-folio.
templ coverStoryComponent(data CoverStoryData) {
<section data-block="magazine-bold:cover_story" class="w-full py-12">
<div class="max-w-6xl mx-auto px-6">
<div class="mb-grid-asym-cover">
<div class="mb-col-folio">
if data.Kicker != "" {
<div class="font-mono text-kicker text-muted-foreground mb-4">{ data.Kicker }</div>
}
</div>
<div class="mb-col-headline">
<h1 class="font-display text-folio text-foreground">
if data.Headline != "" {
@templ.Raw(data.Headline)
} else {
Cover story
}
</h1>
</div>
<div class="mb-col-deck">
if data.Deck != "" {
<div class="font-sans text-deck text-muted-foreground">
@templ.Raw(data.Deck)
</div>
}
</div>
</div>
if data.Image != "" {
<div class="mt-10">
<img src={ data.Image } alt="" class="w-full h-auto block" loading="lazy"/>
</div>
}
if data.Link != "" {
<div class="mt-6">
<a href={ templ.SafeURL(data.Link) } class="mb-button">
Read the story →
</a>
</div>
}
</div>
</section>
}

141
cover_story_templ.go Normal file
View File

@ -0,0 +1,141 @@
// 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"
// coverStoryComponent renders the 12-column asymmetric cover hero.
// Note: the spec calls for an oversized display headline; this consumes
// `--font-heading` via .font-display and clamps via .text-folio.
func coverStoryComponent(data CoverStoryData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"magazine-bold:cover_story\" class=\"w-full py-12\"><div class=\"max-w-6xl mx-auto px-6\"><div class=\"mb-grid-asym-cover\"><div class=\"mb-col-folio\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Kicker != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"font-mono text-kicker text-muted-foreground mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Kicker)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `cover_story.templ`, Line: 12, Col: 81}
}
_, 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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div class=\"mb-col-headline\"><h1 class=\"font-display text-folio text-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Headline != "" {
templ_7745c5c3_Err = templ.Raw(data.Headline).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Cover story")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h1></div><div class=\"mb-col-deck\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Deck != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"font-sans text-deck text-muted-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Deck).Render(ctx, templ_7745c5c3_Buffer)
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
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Image != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"mt-10\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Image)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `cover_story.templ`, Line: 34, Col: 26}
}
_, 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, 11, "\" alt=\"\" class=\"w-full h-auto block\" loading=\"lazy\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Link != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"mt-6\"><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(data.Link))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `cover_story.templ`, Line: 39, Col: 39}
}
_, 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, 13, "\" class=\"mb-button\">Read the story →</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

191
css.go Normal file
View File

@ -0,0 +1,191 @@
package main
// magazineBoldUtilityCSS is appended into the host Tailwind input. It carries
// the editorial display-type utilities, font-slot classes, the hairline rule
// motif, the asymmetric grid utilities, and the per-accent pull-quote variants.
//
// All font-family references go through CSS variables per docs/FONTS.md.
// All color references go through `hsl(var(--token))` consumers of the 19
// theme tokens — no hardcoded hex, rgb(), or named colors. Per-accent
// pull-quote variants intentionally key off the `--accent` token (which the
// active preset rotates between hot pink, electric blue and lime).
//
// The `data-mb-accent` attribute selectors let a pull-quote pin a specific
// accent independently of the preset — they map back into the theme token
// space using the HSL triples documented in the spec §4 accent rows.
const magazineBoldUtilityCSS = `
/* ===========================================================
* Magazine Bold theme utilities
* Appended to the Tailwind input via CSSManifest.InputCSSAppend.
* =========================================================== */
/* --- Font-slot classes (consume CMS-managed font variables) --- */
.font-display {
font-family: var(--font-heading, "Playfair Display", "Cormorant Garamond", Georgia, "Times New Roman", serif);
font-feature-settings: "ss01", "ss02", "kern", "liga";
font-optical-sizing: auto;
}
.font-serif-sub {
font-family: var(--font-heading, "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif);
}
.font-sans {
font-family: var(--font-body, "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
}
.font-mono {
font-family: var(--font-mono, "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
}
/* --- Display-size utilities ------------------------------- */
/* text-folio: 220pt-equivalent oversized folio number / cover hit.
The clamp resolves to ~220px (~165pt) at 1440px, scaling down to 80px on mobile. */
.text-folio {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-size: clamp(80px, 18vw, 240px);
line-height: 0.85;
letter-spacing: -0.04em;
font-weight: 200;
font-style: italic;
}
.text-deck {
font-family: var(--font-body, "Inter", system-ui, sans-serif);
font-size: clamp(28px, 3.2vw, 48px);
line-height: 1.15;
letter-spacing: -0.01em;
font-weight: 400;
}
.text-kicker {
font-family: var(--font-mono, "JetBrains Mono", monospace);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.18em;
font-weight: 600;
}
/* --- Hairline rule motif ---------------------------------- */
.mb-hairline { border-color: hsl(var(--border)); border-width: 1px; }
.mb-hairline-top { border-top: 1px solid hsl(var(--border)); }
.mb-hairline-bottom { border-bottom: 1px solid hsl(var(--border)); }
/* --- Asymmetric editorial grid ---------------------------- */
.mb-grid-12 {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 1.5rem;
}
.mb-grid-asym-cover {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 2rem;
align-items: end;
}
.mb-col-folio { grid-column: 1 / span 2; }
.mb-col-headline { grid-column: 3 / span 7; }
.mb-col-deck { grid-column: 10 / span 3; }
.mb-col-image-wide { grid-column: 1 / -1; }
@media (max-width: 1024px) {
.mb-grid-asym-cover { grid-template-columns: 1fr; gap: 1.25rem; }
.mb-col-folio, .mb-col-headline, .mb-col-deck { grid-column: 1 / -1; }
}
/* --- Photo essay frame spans ------------------------------ */
.mb-photo-grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 1.25rem;
}
.mb-frame-half { grid-column: span 6 / span 6; }
.mb-frame-full { grid-column: 1 / -1; }
.mb-frame-tall { grid-column: span 5 / span 5; grid-row: span 2; }
@media (max-width: 768px) {
.mb-photo-grid { grid-template-columns: 1fr; }
.mb-frame-half, .mb-frame-full, .mb-frame-tall { grid-column: 1 / -1; grid-row: auto; }
}
/* --- Color-block accent panels ---------------------------- */
.colorblock-accent {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
/* --- Per-accent pull-quote variants ----------------------- *
* These let a pull_quote override the preset accent on a per-instance basis.
* They use literal HSL triples that match the spec §4 accent values so a
* "pink" pull-quote always reads pink even on the ink-blue preset.
*/
[data-mb-accent="pink"] { --mb-accent-h: 330; --mb-accent-s: 95%; --mb-accent-l: 58%; }
[data-mb-accent="blue"] { --mb-accent-h: 220; --mb-accent-s: 100%; --mb-accent-l: 56%; }
[data-mb-accent="lime"] { --mb-accent-h: 78; --mb-accent-s: 90%; --mb-accent-l: 50%; }
[data-mb-accent] .mb-quote-bar {
background-color: hsl(var(--mb-accent-h) var(--mb-accent-s) var(--mb-accent-l));
}
[data-mb-accent] .mb-quote-text { color: hsl(var(--foreground)); }
/* --- Drop-cap (heading override) -------------------------- */
.mb-dropcap::first-letter {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-style: italic;
font-weight: 200;
float: left;
font-size: 5.5em;
line-height: 0.85;
padding-right: 0.08em;
margin-top: 0.04em;
color: hsl(var(--foreground));
}
/* --- Body text override (Inter + margin folios) ----------- */
.mb-body {
font-family: var(--font-body, "Inter", system-ui, sans-serif);
font-size: clamp(16px, 1.05vw, 18px);
line-height: 1.55;
letter-spacing: 0.005em;
color: hsl(var(--foreground));
}
/* --- Squared ink button ----------------------------------- */
.mb-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
font-family: var(--font-mono, "JetBrains Mono", monospace);
font-size: 12px;
letter-spacing: 0.14em;
text-transform: uppercase;
padding: 0.875rem 1.5rem;
border: 1px solid hsl(var(--primary));
border-radius: 0;
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}
.mb-button:hover {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
border-color: hsl(var(--accent));
}
.mb-button:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Caption (mono, used by image override + photo essay) -- */
.mb-caption {
font-family: var(--font-mono, "JetBrains Mono", monospace);
font-size: 12px;
line-height: 1.45;
letter-spacing: 0.01em;
color: hsl(var(--muted-foreground));
}
/* --- Full-bleed image with numbered folio ----------------- */
.mb-fullbleed { width: 100vw; max-width: 100vw; margin-left: 50%; transform: translateX(-50%); }
.mb-image-folio {
font-family: var(--font-mono, "JetBrains Mono", monospace);
font-size: 11px;
letter-spacing: 0.16em;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
}
`

100
email_wrapper.go Normal file
View File

@ -0,0 +1,100 @@
package main
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// MagazineBoldEmailWrapper wraps body content in the Magazine Bold email template.
// The template is a 600px paper-toned table with a PP Editorial New wordmark,
// an ink hairline, body in Inter 16/24, and a mono colophon strip.
func MagazineBoldEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
_ = magazineBoldEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// Hex defaults map onto the paper-pink light preset so wallets / clients that
// don't ship custom colours still resolve to the brand's paper tone.
// Spec §4 paper-pink light: background `38 30% 96%` → ~#F7F2EB
// foreground `0 0% 8%` → ~#141414
// border `0 0% 88%` → ~#E0E0E0
// muted `38 20% 92%`→ ~#EEE9E0
// mutedFg `0 0% 40%` → ~#666666
// accent `330 95% 58%` → ~#F0298A
func mbEmailBg(ctx templates.EmailContext) string {
if ctx.Colors.Background != "" {
return ctx.Colors.Background
}
return "#F7F2EB"
}
func mbEmailFg(ctx templates.EmailContext) string {
if ctx.Colors.Foreground != "" {
return ctx.Colors.Foreground
}
return "#141414"
}
func mbEmailMuted(ctx templates.EmailContext) string {
if ctx.Colors.Muted != "" {
return ctx.Colors.Muted
}
return "#EEE9E0"
}
func mbEmailMutedFg(ctx templates.EmailContext) string {
if ctx.Colors.MutedForeground != "" {
return ctx.Colors.MutedForeground
}
return "#666666"
}
func mbEmailBorder(ctx templates.EmailContext) string {
if ctx.Colors.Border != "" {
return ctx.Colors.Border
}
return "#E0E0E0"
}
func mbEmailCard(ctx templates.EmailContext) string {
if ctx.Colors.Card != "" {
return ctx.Colors.Card
}
return "#FFFFFF"
}
// mbEmailDisplayStack is the serif fallback the wordmark falls back to in
// clients that strip @font-face (Outlook 365 web). System serifs only.
const mbEmailDisplayStack = `'PP Editorial New', Georgia, 'Times New Roman', serif`
// mbEmailBodyStack is the sans fallback used for body copy in clients that
// strip @font-face.
const mbEmailBodyStack = `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`
// mbEmailMonoStack is the mono fallback used for the colophon strip.
const mbEmailMonoStack = `'JetBrains Mono', 'Courier New', Courier, monospace`
// mbEmailStyleBody styles the outer body tag.
func mbEmailStyleBody(ctx templates.EmailContext) string {
return fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: %s;", mbEmailBg(ctx), mbEmailBodyStack)
}
func mbEmailStyleHeader(ctx templates.EmailContext) string {
return fmt.Sprintf("padding: 32px 40px; background-color: %s; border-bottom: 1px solid %s;", mbEmailCard(ctx), mbEmailBorder(ctx))
}
func mbEmailStyleWordmark(ctx templates.EmailContext) string {
return fmt.Sprintf("margin: 0; font-family: %s; font-style: italic; font-weight: 200; font-size: 42px; line-height: 1; letter-spacing: -0.02em; color: %s;", mbEmailDisplayStack, mbEmailFg(ctx))
}
func mbEmailStyleBodyContent(ctx templates.EmailContext) string {
return fmt.Sprintf("padding: 40px 48px; background-color: %s; color: %s; font-family: %s; font-size: 16px; line-height: 24px;", mbEmailCard(ctx), mbEmailFg(ctx), mbEmailBodyStack)
}
func mbEmailStyleColophon(ctx templates.EmailContext) string {
return fmt.Sprintf("padding: 24px 48px; background-color: %s; border-top: 1px solid %s; font-family: %s; font-size: 12px; line-height: 18px; color: %s; letter-spacing: 0.08em; text-transform: uppercase;", mbEmailMuted(ctx), mbEmailBorder(ctx), mbEmailMonoStack, mbEmailMutedFg(ctx))
}

83
email_wrapper.templ Normal file
View File

@ -0,0 +1,83 @@
package main
import (
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// magazineBoldEmailTemplate is the 600px paper-toned editorial email wrapper.
// It uses inlined hex defaults that map onto the paper-pink light preset so the
// layout reads correctly in clients that strip server CSS.
templ magazineBoldEmailTemplate(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; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
@media only screen and (max-width: 620px) {
.mb-email-container { width: 100% !important; max-width: 100% !important; }
.mb-email-pad { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style={ mbEmailStyleBody(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;", mbEmailBg(emailCtx)) }>
<table role="presentation" class="mb-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s;", mbEmailCard(emailCtx), mbEmailBorder(emailCtx)) }>
<!-- Wordmark / masthead -->
<tr>
<td align="left" class="mb-email-pad" style={ mbEmailStyleHeader(emailCtx) }>
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={ mbEmailStyleWordmark(emailCtx) }>{ emailCtx.SiteSettings.SiteName }</h1>
}
</td>
</tr>
<!-- Ink hairline -->
<tr>
<td style={ fmt.Sprintf("height: 1px; line-height: 1px; background-color: %s;", mbEmailFg(emailCtx)) }>&nbsp;</td>
</tr>
<!-- Body -->
<tr>
<td class="mb-email-pad" style={ mbEmailStyleBodyContent(emailCtx) }>
@templ.Raw(body)
</td>
</tr>
<!-- Mono colophon -->
<tr>
<td class="mb-email-pad" style={ mbEmailStyleColophon(emailCtx) }>
<div>{ emailCtx.SiteSettings.SiteName }</div>
if emailCtx.SiteSettings.SiteURL != "" {
<div style="margin-top:8px;">
<a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", mbEmailMutedFg(emailCtx)) }>{ emailCtx.SiteSettings.SiteURL }</a>
</div>
}
if emailCtx.UnsubscribeURL != "" {
<div style="margin-top:8px;">
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", mbEmailMutedFg(emailCtx)) }>Unsubscribe</a>
</div>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}

344
email_wrapper_templ.go Normal file
View File

@ -0,0 +1,344 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// magazineBoldEmailTemplate is the 600px paper-toned editorial email wrapper.
// It uses inlined hex defaults that map onto the paper-pink light preset so the
// layout reads correctly in clients that strip server CSS.
func magazineBoldEmailTemplate(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: 19, 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 { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }\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; line-height: 100%; outline: none; text-decoration: none; }\n\t\t\t\tbody { margin: 0 !important; padding: 0 !important; width: 100% !important; }\n\t\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t\t.mb-email-container { width: 100% !important; max-width: 100% !important; }\n\t\t\t\t\t.mb-email-pad { 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(mbEmailStyleBody(emailCtx))
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_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: 34, 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;", mbEmailBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 39, Col: 109}
}
_, 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=\"mb-email-container\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s;", mbEmailCard(emailCtx), mbEmailBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 40, Col: 247}
}
_, 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, "\"><!-- Wordmark / masthead --><tr><td align=\"left\" class=\"mb-email-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(mbEmailStyleHeader(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 43, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.LogoURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.LogoURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 45, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 45, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" style=\"max-height: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, 13, "<h1 style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(mbEmailStyleWordmark(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 47, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 47, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</td></tr><!-- Ink hairline --><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("height: 1px; line-height: 1px; background-color: %s;", mbEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 53, Col: 108}
}
_, 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><!-- Body --><tr><td class=\"mb-email-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(mbEmailStyleBodyContent(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 57, Col: 74}
}
_, 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><!-- Mono colophon --><tr><td class=\"mb-email-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(mbEmailStyleColophon(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 63, Col: 71}
}
_, 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, "\"><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 64, Col: 46}
}
_, 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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.SiteURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div style=\"margin-top:8px;\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 templ.SafeURL
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.SiteSettings.SiteURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 67, Col: 65}
}
_, 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, 23, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", mbEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 67, Col: 155}
}
_, 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, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 67, Col: 189}
}
_, 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, "</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div style=\"margin-top:8px;\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 templ.SafeURL
templ_7745c5c3_Var19, 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: 72, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", mbEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 72, Col: 149}
}
_, 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, 28, "\">Unsubscribe</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

49
embed.go Normal file
View File

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

View File

@ -1,26 +1 @@
[
{
"name": "Archivo Black",
"family": "Archivo Black",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/archivo-black-400.woff2" }
]
},
{
"name": "Inter",
"family": "Inter",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/inter-400.woff2" },
{ "weight": "500", "style": "normal", "file": "fonts/inter-500.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/inter-700.woff2" }
]
},
{
"name": "IBM Plex Mono",
"family": "IBM Plex Mono",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/ibm-plex-mono-400.woff2" },
{ "weight": "500", "style": "normal", "file": "fonts/ibm-plex-mono-500.woff2" }
]
}
]
[]

20
go.mod Normal file
View File

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

44
go.sum Normal file
View File

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

63
heading_override.go Normal file
View File

@ -0,0 +1,63 @@
package main
import (
"bytes"
"context"
"strconv"
)
// HeadingOverrideBlock renders headings with the Magazine Bold display-serif scale
// and an optional drop-cap on h1.
//
// Built-in heading content shape: {"text": "...", "level": 1-6, "textClass": "..."}
func HeadingOverrideBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
textClass := getString(content, "textClass")
level := parseHeadingLevel(content)
var buf bytes.Buffer
_ = mbHeadingComponent(level, text, textClass).Render(ctx, &buf)
return buf.String()
}
// parseHeadingLevel parses the level from content, defaulting to 2.
func parseHeadingLevel(content map[string]any) int {
if level, ok := content["level"].(float64); ok {
l := int(level)
if l >= 1 && l <= 6 {
return l
}
}
if level, ok := content["level"].(int); ok {
if level >= 1 && level <= 6 {
return level
}
}
if level, ok := content["level"].(string); ok {
if l, err := strconv.Atoi(level); err == nil && l >= 1 && l <= 6 {
return l
}
}
return 2
}
// mbHeadingBaseClass returns the Magazine Bold class for each heading level.
// h1 picks up the .text-folio clamp + drop-cap behaviour.
func mbHeadingBaseClass(level int) string {
switch level {
case 1:
return "font-display text-folio text-foreground mb-dropcap"
case 2:
return "font-display text-deck text-foreground"
case 3:
return "font-display text-3xl text-foreground"
case 4:
return "font-serif-sub text-2xl text-foreground"
case 5:
return "font-serif-sub text-xl text-foreground"
case 6:
return "font-mono text-kicker text-muted-foreground"
default:
return "font-display text-deck text-foreground"
}
}

21
heading_override.templ Normal file
View File

@ -0,0 +1,21 @@
package main
// mbHeadingComponent renders a heading at the requested level with Magazine Bold scale.
templ mbHeadingComponent(level int, text, textClass string) {
switch level {
case 1:
<h1 class={ mbHeadingBaseClass(1), textClass }>{ text }</h1>
case 2:
<h2 class={ mbHeadingBaseClass(2), textClass }>{ text }</h2>
case 3:
<h3 class={ mbHeadingBaseClass(3), textClass }>{ text }</h3>
case 4:
<h4 class={ mbHeadingBaseClass(4), textClass }>{ text }</h4>
case 5:
<h5 class={ mbHeadingBaseClass(5), textClass }>{ text }</h5>
case 6:
<h6 class={ mbHeadingBaseClass(6), textClass }>{ text }</h6>
default:
<h2 class={ mbHeadingBaseClass(2), textClass }>{ text }</h2>
}
}

291
heading_override_templ.go Normal file
View File

@ -0,0 +1,291 @@
// 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"
// mbHeadingComponent renders a heading at the requested level with Magazine Bold scale.
func mbHeadingComponent(level int, text, textClass string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch level {
case 1:
var templ_7745c5c3_Var2 = []any{mbHeadingBaseClass(1), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 7, 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, 3, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 2:
var templ_7745c5c3_Var5 = []any{mbHeadingBaseClass(2), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var5).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 9, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 3:
var templ_7745c5c3_Var8 = []any{mbHeadingBaseClass(3), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 11, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 4:
var templ_7745c5c3_Var11 = []any{mbHeadingBaseClass(4), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h4 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var11).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 13, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 5:
var templ_7745c5c3_Var14 = []any{mbHeadingBaseClass(5), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h5 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 15, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</h5>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 6:
var templ_7745c5c3_Var17 = []any{mbHeadingBaseClass(6), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h6 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var17).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 17, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h6>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
var templ_7745c5c3_Var20 = []any{mbHeadingBaseClass(2), textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var20).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 19, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

71
helpers.go Normal file
View File

@ -0,0 +1,71 @@
package main
// getString extracts a string value from content map.
func getString(content map[string]any, key string) string {
if v, ok := content[key].(string); ok {
return v
}
return ""
}
// getInt extracts an int value from content map (handles float64 from JSON).
func getInt(content map[string]any, key string, defaultVal int) int {
if v, ok := content[key].(float64); ok {
return int(v)
}
if v, ok := content[key].(int); ok {
return v
}
return defaultVal
}
// getBool extracts a bool from content (handles "true"/"false" strings).
func getBool(content map[string]any, key string, defaultVal bool) bool {
if v, ok := content[key].(bool); ok {
return v
}
if v, ok := content[key].(string); ok {
switch v {
case "true", "TRUE", "True", "1", "yes", "on":
return true
case "false", "FALSE", "False", "0", "no", "off", "":
return false
}
}
return defaultVal
}
// getSlice extracts a slice of maps from content.
func getSlice(content map[string]any, key string) []map[string]any {
if v, ok := content[key].([]any); ok {
result := make([]map[string]any, 0, len(v))
for _, item := range v {
if m, ok := item.(map[string]any); ok {
result = append(result, m)
}
}
return result
}
return nil
}
// normalizeAccent maps the schema accent enum into a CSS class suffix.
// Unknown values fall back to "pink" (the default-preset accent slot).
func normalizeAccent(raw string) string {
switch raw {
case "pink", "blue", "lime":
return raw
default:
return "pink"
}
}
// normalizeSpan maps the photo-essay frame span enum.
func normalizeSpan(raw string) string {
switch raw {
case "half", "full", "tall":
return raw
default:
return "half"
}
}

23
image_override.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// ImageOverrideBlock renders the built-in image block with the Magazine Bold
// treatment: optional full-bleed mode + a mono caption folio.
//
// Built-in image content shape: {"src": "...", "alt": "...", "caption": "...", "fullBleed": true/false}
func ImageOverrideBlock(ctx context.Context, content map[string]any) string {
src := blocks.ResolveMediaPath(getString(content, "src"))
alt := getString(content, "alt")
caption := getString(content, "caption")
fullBleed := getBool(content, "fullBleed", false)
var buf bytes.Buffer
_ = mbImageComponent(src, alt, caption, fullBleed).Render(ctx, &buf)
return buf.String()
}

14
image_override.templ Normal file
View File

@ -0,0 +1,14 @@
package main
// mbImageComponent renders an image with optional full-bleed mode and a numbered caption.
// Skips rendering if no src — guarantees zero broken <img>.
templ mbImageComponent(src, alt, caption string, fullBleed bool) {
if src != "" {
<figure class={ "my-8", templ.KV("mb-fullbleed", fullBleed) }>
<img src={ src } alt={ alt } class="w-full h-auto block" loading="lazy"/>
if caption != "" {
<figcaption class="mb-caption font-mono mt-2 px-6">{ caption }</figcaption>
}
</figure>
}
}

111
image_override_templ.go Normal file
View File

@ -0,0 +1,111 @@
// 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"
// mbImageComponent renders an image with optional full-bleed mode and a numbered caption.
// Skips rendering if no src — guarantees zero broken <img>.
func mbImageComponent(src, alt, caption string, fullBleed bool) 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 src != "" {
var templ_7745c5c3_Var2 = []any{"my-8", templ.KV("mb-fullbleed", fullBleed)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<figure class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(src)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 8, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(alt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 8, Col: 29}
}
_, 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, 4, "\" class=\"w-full h-auto block\" loading=\"lazy\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<figcaption class=\"mb-caption font-mono mt-2 px-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(caption)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 10, Col: 64}
}
_, 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, 6, "</figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

43
issue_archive.go Normal file
View File

@ -0,0 +1,43 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// IssueArchiveBlockMeta is the numbered list of back-issues.
var IssueArchiveBlockMeta = blocks.BlockMeta{
Key: "issue_archive",
Title: "Issue Archive",
Description: "Numbered list of back-issues driven by a bucket query.",
Source: "magazine-bold",
Category: blocks.CategoryNavigation,
}
// IssueArchiveData is what the templ component consumes.
type IssueArchiveData struct {
BucketSlug string
Limit int
}
// IssueArchiveBlock renders the issue archive list.
// content keys: bucketSlug (bucket-picker), limit (number).
//
// NOTE: bucket-row resolution is handled by the host at render-time via
// `bucket_data` injection — this block renders the scaffold and an empty
// state if the host hasn't supplied items yet.
func IssueArchiveBlock(ctx context.Context, content map[string]any) string {
data := IssueArchiveData{
BucketSlug: getString(content, "bucketSlug"),
Limit: getInt(content, "limit", 12),
}
items := getSlice(content, "items")
if items == nil {
items = getSlice(content, "bucket_data")
}
var buf bytes.Buffer
_ = issueArchiveComponent(data, items).Render(ctx, &buf)
return buf.String()
}

37
issue_archive.templ Normal file
View File

@ -0,0 +1,37 @@
package main
import "fmt"
// issueArchiveComponent renders a numbered archive list.
// Unknown / empty bucket → renders an empty-state message, no broken markup.
templ issueArchiveComponent(data IssueArchiveData, items []map[string]any) {
<nav data-block="magazine-bold:issue_archive" class="py-6">
<div class="font-mono text-kicker text-muted-foreground mb-6">Issue Archive</div>
if len(items) == 0 {
<p class="font-mono mb-caption">No issues yet.</p>
} else {
<ol class="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-4">
for i, item := range items {
if i >= max(1, data.Limit) {
{{ break }}
}
<li class="flex items-baseline gap-4 mb-hairline-bottom py-3">
<span class="font-mono text-kicker text-muted-foreground w-10">{ fmt.Sprintf("%02d", i+1) }</span>
<span class="font-display flex-1 text-foreground">
if title, ok := item["title"].(string); ok {
{ title }
} else if name, ok := item["name"].(string); ok {
{ name }
} else {
Untitled issue
}
</span>
if date, ok := item["date"].(string); ok && date != "" {
<span class="font-mono mb-caption">{ date }</span>
}
</li>
}
</ol>
}
</nav>
}

138
issue_archive_templ.go Normal file
View File

@ -0,0 +1,138 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
// issueArchiveComponent renders a numbered archive list.
// Unknown / empty bucket → renders an empty-state message, no broken markup.
func issueArchiveComponent(data IssueArchiveData, items []map[string]any) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<nav data-block=\"magazine-bold:issue_archive\" class=\"py-6\"><div class=\"font-mono text-kicker text-muted-foreground mb-6\">Issue Archive</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(items) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"font-mono mb-caption\">No issues yet.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<ol class=\"grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for i, item := range items {
if i >= max(1, data.Limit) {
break
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " <li class=\"flex items-baseline gap-4 mb-hairline-bottom py-3\"><span class=\"font-mono text-kicker text-muted-foreground w-10\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d", i+1))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `issue_archive.templ`, Line: 19, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span> <span class=\"font-display flex-1 text-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if title, ok := item["title"].(string); ok {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `issue_archive.templ`, Line: 22, Col: 15}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if name, ok := item["name"].(string); ok {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `issue_archive.templ`, Line: 24, Col: 14}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Untitled issue")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if date, ok := item["date"].(string); ok && date != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"font-mono mb-caption\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(date)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `issue_archive.templ`, Line: 30, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</ol>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,503 +0,0 @@
# Magazine Bold theme — codeless .bnp manifest (Theme Fleet Wave A, WO-TF-021).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
#
# Persona: a streetwear label / fashion-drop publication. Print-cover energy
# translated to drops and lookbooks — massive condensed uppercase headlines
# (Archivo Black), duotone imagery, grid-breaking overlap, mono datelines and
# fluoro accent slabs. Newsprint-plus-voltage by day, ink-black-plus-fluoro by
# night. Colour ALWAYS via hsl(var(--token)); font families ALWAYS via
# var(--font-heading|body|mono, <fallback>). No literal colours, no hardcoded
# families. Motion tier MID: JS effects (marquee, scroll reveal, duotone hover),
# no canvas, prefers-reduced-motion honored with static fallbacks.
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
# Icon packs referenced by theme templates (static: lucide + simple-icons;
# dynamic defaults resolve to lucide, e.g. the divider's `lucide:diamond`).
# Declared so /icons/<pack>.svg is materialized on fresh installs — without it
# the sprite can 404 and every sprite icon renders invisible.
required_icon_packs: [lucide, simple-icons]
system_templates:
- key: magazine-bold
title: Magazine Bold
description: "A streetwear label's cover-story chrome: massive condensed uppercase headlines, duotone imagery, grid-breaking overlap and fluoro accent slabs. Built for fashion drops, lookbooks and culture publications."
page_templates:
- system: magazine-bold
key: default
title: Default
description: "Standard page: masthead, single reading column, colophon footer."
slots: [header, main, footer]
- system: magazine-bold
key: full-width
title: Full Bleed
description: "Edge-to-edge for lookbooks, cover images and photo essays."
slots: [header, main, footer]
- system: magazine-bold
key: landing
title: Cover / Drop
description: "Cover-story stack: masthead, lead drop, river of stories, colophon."
slots: [masthead, lead, river, footer]
- system: magazine-bold
key: article
title: Feature
description: "Long-form feature: byline, drop-cap opener, reading column with a marginalia rail."
slots: [header, byline, main, marginalia, footer]
- system: magazine-bold
key: blog-index
title: Archive
description: "Drop archive: masthead, featured drop, and a river of stories."
slots: [header, featured, main, footer]
- system: magazine-bold
key: contact
title: Studio
description: "Contact layout with an optional aside for stockists, hours and location."
slots: [header, main, aside, footer]
- system: magazine-bold
key: auth
title: Members
description: "Centred members shell with wordmark and a fluoro rule."
slots: [main, footer]
template_overrides:
# Chrome (4)
- { template: magazine-bold, block: navbar }
- { template: magazine-bold, block: footer }
- { template: magazine-bold, block: utility-bar }
- { template: magazine-bold, block: announcement-bar }
# Marketing (12)
- { template: magazine-bold, block: hero }
- { template: magazine-bold, block: feature-grid }
- { template: magazine-bold, block: rich-section }
- { template: magazine-bold, block: stats }
- { template: magazine-bold, block: testimonial }
- { template: magazine-bold, block: logo-strip }
- { template: magazine-bold, block: pricing }
- { template: magazine-bold, block: team }
- { template: magazine-bold, block: faq }
- { template: magazine-bold, block: timeline }
- { template: magazine-bold, block: cta }
- { template: magazine-bold, block: countdown }
# Primitives (11)
- { template: magazine-bold, block: heading }
- { template: magazine-bold, block: text }
- { template: magazine-bold, block: rich-text }
- { template: magazine-bold, block: image }
- { template: magazine-bold, block: quote }
- { template: magazine-bold, block: divider }
- { template: magazine-bold, block: button }
- { template: magazine-bold, block: card }
- { template: magazine-bold, block: gallery }
- { template: magazine-bold, block: video-embed }
- { template: magazine-bold, block: audio-embed }
# Commerce / local (6)
- { template: magazine-bold, block: menu-list }
- { template: magazine-bold, block: event-list }
- { template: magazine-bold, block: contact-form }
- { template: magazine-bold, block: contact-card }
- { template: magazine-bold, block: hours-location }
- { template: magazine-bold, block: social-links }
# Blog family (12)
- { template: magazine-bold, block: blog-hero }
- { template: magazine-bold, block: featured-posts }
- { template: magazine-bold, block: related-posts }
- { template: magazine-bold, block: popular-posts }
- { template: magazine-bold, block: category-list }
- { template: magazine-bold, block: post-hero }
- { template: magazine-bold, block: post-metadata }
- { template: magazine-bold, block: author-bio }
- { template: magazine-bold, block: author-bio-hero }
- { template: magazine-bold, block: newsletter-signup }
- { template: magazine-bold, block: breadcrumbs }
- { template: magazine-bold, block: sidebar-nav }
# Auth surface (3)
- { template: magazine-bold, block: auth-form }
- { template: magazine-bold, block: password-reset-form }
- { template: magazine-bold, block: auth-status }
# System (1)
- { template: magazine-bold, block: page-suggestions }
email_wrappers:
- magazine-bold
css:
input_css_append: |
/* === Magazine Bold theme — utility layer ===
*
* Streetwear / fashion-drop identity: massive condensed uppercase headlines,
* heavy 2px rules, hard square edges, mono datelines and fluoro accent slabs.
* 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 display/body/mono 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/magazine-bold/fonts/...). Presets set typography.fontHeading/
* body/mono to template:magazine-bold:<Family> so --font-heading|body|mono
* resolve to these families. Families/weights mirror fonts.json exactly. */
@font-face { font-family: 'Archivo Black'; src: url('/templates/magazine-bold/fonts/archivo-black-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Inter'; src: url('/templates/magazine-bold/fonts/inter-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Inter'; src: url('/templates/magazine-bold/fonts/inter-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: 'Inter'; src: url('/templates/magazine-bold/fonts/inter-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('/templates/magazine-bold/fonts/ibm-plex-mono-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('/templates/magazine-bold/fonts/ibm-plex-mono-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
@layer utilities {
:root {
--mb-measure: 68ch;
--mb-rule: 2px solid hsl(var(--foreground));
}
/* --- Font slots (consume CMS-managed font variables) --- */
.font-display { font-family: var(--font-heading, "Archivo Black", "Arial Black", sans-serif); }
.font-sans { font-family: var(--font-body, "Inter", system-ui, sans-serif); }
.font-mono { font-family: var(--font-mono, "IBM Plex Mono", ui-monospace, monospace); }
/* --- Page widths / reading measures --- */
.mb-column {
max-width: var(--mb-measure);
margin-left: auto; margin-right: auto;
padding-left: 1rem; padding-right: 1rem;
}
.mb-wide {
max-width: 80rem;
margin-left: auto; margin-right: auto;
padding-left: 1rem; padding-right: 1rem;
}
.mb-article-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
max-width: 84rem;
margin-left: auto; margin-right: auto;
padding-left: 1rem; padding-right: 1rem;
}
@media (min-width: 1024px) {
.mb-article-grid { grid-template-columns: minmax(0, 68ch) 18rem; }
}
/* --- Prose reading column --- */
.mb-prose {
max-width: var(--mb-measure);
margin-left: auto; margin-right: auto;
font-family: var(--font-body, "Inter", system-ui, sans-serif);
color: hsl(var(--foreground));
line-height: 1.6;
}
.mb-prose p { margin: 0 0 1.15em 0; }
.mb-prose h1, .mb-prose h2, .mb-prose h3, .mb-prose h4 {
font-family: var(--font-heading, "Archivo Black", "Arial Black", sans-serif);
text-transform: uppercase;
letter-spacing: -0.01em;
line-height: 1.0;
color: hsl(var(--foreground));
}
.mb-prose a {
color: hsl(var(--accent));
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
.mb-prose blockquote {
font-weight: 500;
border-left: 4px solid hsl(var(--accent));
padding-left: 1rem; margin-left: 0;
color: hsl(var(--foreground));
}
/* --- Mono / tracked labels (kickers, eyebrows, meta) --- */
.mb-caps {
font-family: var(--font-mono, "IBM Plex Mono", ui-monospace, monospace);
text-transform: uppercase;
letter-spacing: 0.14em;
font-weight: 500;
font-size: 0.72rem;
line-height: 1.35;
}
/* --- Display heading (Archivo Black, massive condensed caps) --- */
.mb-heading {
font-family: var(--font-heading, "Archivo Black", "Arial Black", sans-serif);
text-transform: uppercase;
letter-spacing: -0.02em;
line-height: 0.95;
color: hsl(var(--foreground));
}
.mb-underline { position: relative; padding-bottom: 0.28em; }
.mb-underline::after {
content: "";
position: absolute; left: 0; right: 0; bottom: 0;
height: 4px;
background: hsl(var(--accent));
}
/* --- Heavy rules (bars, not hairlines) --- */
hr.mb-rule, .mb-rule, .mb-hairline {
border: 0;
border-top: var(--mb-rule);
margin: 0;
}
.mb-hairline-top { border-top: var(--mb-rule); }
.mb-hairline-bottom { border-bottom: var(--mb-rule); }
/* --- Body text override --- */
.mb-body {
font-family: var(--font-body, "Inter", system-ui, sans-serif);
color: hsl(var(--foreground));
line-height: 1.6;
}
/* --- Inline accent link --- */
.mb-link {
color: hsl(var(--accent));
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 3px;
transition: color 120ms ease, text-decoration-color 120ms ease;
}
.mb-link:hover, .mb-link:focus-visible { color: hsl(var(--foreground)); }
/* --- Button — fluoro / ink slab, square, uppercase mono --- */
.mb-button, .mb-btn {
display: inline-block;
padding: 0.7rem 1.4rem;
font-family: var(--font-mono, "IBM Plex Mono", ui-monospace, monospace);
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.14em;
background-color: hsl(var(--foreground));
color: hsl(var(--background));
border: 2px solid hsl(var(--foreground));
text-decoration: none;
cursor: pointer;
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}
.mb-button:hover, .mb-button:focus, .mb-btn:hover, .mb-btn:focus {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
border-color: hsl(var(--accent));
}
.mb-button:focus-visible, .mb-btn:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Panel / card — heavy 2px frame, hard edges, no shadow --- */
.mb-panel, .mb-card {
border: 2px solid hsl(var(--foreground));
background-color: hsl(var(--card));
transition: border-color 120ms ease, transform 120ms ease;
}
.mb-panel:hover, .mb-card:hover { border-color: hsl(var(--accent)); }
/* --- Form field — heavy border, square, accent focus --- */
.mb-input {
width: 100%;
border: 2px solid hsl(var(--input));
background-color: hsl(var(--background));
color: hsl(var(--foreground));
padding: 0.6rem 0.8rem;
font-family: var(--font-body, "Inter", system-ui, sans-serif);
font-size: 0.95rem;
border-radius: 0;
}
.mb-input::placeholder { color: hsl(var(--muted-foreground)); }
.mb-input:focus {
outline: none;
border-color: hsl(var(--accent));
box-shadow: 0 0 0 2px hsl(var(--accent));
}
/* --- Figure / caption --- */
.mb-figure { margin: 1.5rem 0; }
.mb-figure img { display: block; width: 100%; height: auto; }
.mb-figure figcaption {
margin-top: 0.6rem;
font-family: var(--font-mono, "IBM Plex Mono", ui-monospace, monospace);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: hsl(var(--muted-foreground));
}
.mb-caption {
font-family: var(--font-mono, "IBM Plex Mono", ui-monospace, monospace);
font-size: 0.72rem;
line-height: 1.4;
letter-spacing: 0.04em;
color: hsl(var(--muted-foreground));
}
/* --- Drop cap — oversized Archivo Black accent first letter --- */
.mb-dropcap p:first-of-type::first-letter {
font-family: var(--font-heading, "Archivo Black", "Arial Black", sans-serif);
color: hsl(var(--accent));
float: left;
font-size: 4.4em;
line-height: 0.72;
/* Horizontal clearance so wrapped lines (esp. the 3rd) never crowd the
* cap, plus a little breathing room below it. */
padding-right: 0.16em;
padding-top: 0.02em;
margin-bottom: 0.04em;
}
/* --- Marginalia rail (mono side notes, MID motion reveal) --- */
.mb-marginalia {
font-family: var(--font-mono, "IBM Plex Mono", ui-monospace, monospace);
font-size: 0.75rem;
line-height: 1.5;
color: hsl(var(--muted-foreground));
}
.mb-marginalia aside {
margin: 0 0 1.5rem 0;
padding-left: 0.75rem;
border-left: var(--mb-rule);
opacity: 0;
animation: mb-fade-in 500ms ease forwards;
}
@keyframes mb-fade-in { to { opacity: 1; } }
@media (max-width: 768px) {
.mb-marginalia {
margin-top: 1.5rem;
border-top: var(--mb-rule);
padding-top: 1rem;
}
}
/* --- Masthead wordmark --- */
.mb-masthead-wordmark {
font-family: var(--font-heading, "Archivo Black", "Arial Black", sans-serif);
text-transform: uppercase;
letter-spacing: -0.03em;
color: hsl(var(--foreground));
}
/* --- Display size utilities (folio / deck / kicker) --- */
.text-folio {
font-family: var(--font-heading, "Archivo Black", "Arial Black", sans-serif);
font-size: clamp(54px, 15vw, 220px);
line-height: 0.82;
letter-spacing: -0.04em;
text-transform: uppercase;
overflow-wrap: break-word;
word-break: break-word;
}
/* Cover-story headline — CLAMPED so the longest single word fits its
* reserved 8-of-12 column (~740px at 1440) and never crosses the deck
* column or bleeds off-viewport. Deliberate grid-breaking scale kept, but
* bounded: the folio is big, not unbounded. word-break is a last-resort
* safety net; the clamp keeps whole words within the track. */
.mb-cover-folio {
font-family: var(--font-heading, "Archivo Black", "Arial Black", sans-serif);
font-size: clamp(2.75rem, 8vw, 8rem);
line-height: 0.86;
letter-spacing: -0.035em;
text-transform: uppercase;
overflow-wrap: break-word;
hyphens: none;
}
.text-deck {
font-family: var(--font-body, "Inter", system-ui, sans-serif);
font-size: clamp(24px, 3vw, 44px);
line-height: 1.12;
letter-spacing: -0.01em;
font-weight: 500;
}
.text-kicker {
font-family: var(--font-mono, "IBM Plex Mono", ui-monospace, monospace);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.18em;
font-weight: 600;
}
/* --- Asymmetric editorial grids (persona blocks) --- */
.mb-grid-12 { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 1.5rem; }
.mb-photo-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 1.25rem; }
.mb-frame-half { grid-column: span 6 / span 6; }
.mb-frame-full { grid-column: 1 / -1; }
.mb-frame-tall { grid-column: span 5 / span 5; grid-row: span 2; }
@media (max-width: 768px) {
.mb-photo-grid { grid-template-columns: 1fr; }
.mb-frame-half, .mb-frame-full, .mb-frame-tall { grid-column: 1 / -1; grid-row: auto; }
}
/* --- River container (centred max-width column for the story river slot) --- */
.mb-river {
max-width: 64rem;
margin-left: auto; margin-right: auto;
padding-left: 1rem; padding-right: 1rem;
}
/* --- Color-block accent panel --- */
.colorblock-accent { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
/* --- Full-bleed image --- */
.mb-fullbleed { width: 100vw; max-width: 100vw; margin-left: 50%; transform: translateX(-50%); }
/* --- Duotone photo treatment (signature — persona blocks) --- */
.mb-duotone { position: relative; }
.mb-duotone img { filter: grayscale(1) contrast(1.15); display: block; width: 100%; height: 100%; object-fit: cover; }
.mb-duotone::after {
content: ""; position: absolute; inset: 0;
background: hsl(var(--accent));
mix-blend-mode: multiply;
opacity: 0.55;
transition: opacity 200ms ease;
pointer-events: none;
}
.mb-duotone:hover::after { opacity: 0.2; }
@media (prefers-reduced-motion: reduce) {
.mb-duotone:hover::after { opacity: 0.55; }
}
/* --- Scroll reveal (MID motion) — ROBUST: visible by default (no-JS / no-observer
* / reduced-motion / un-scrolled full-page capture all show content). JS opts
* IN to the hidden-then-reveal animation by adding .mb-armed, and a safety net
* force-reveals everything. Never leaves content permanently hidden. --- */
.mb-reveal { opacity: 1; transform: none; }
.mb-reveal.mb-armed { opacity: 0; transform: translateY(16px); transition: opacity 500ms ease, transform 500ms ease; }
.mb-reveal.mb-armed.mb-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
.mb-reveal, .mb-reveal.mb-armed { opacity: 1; transform: none; transition: none; }
}
/* --- Marquee ticker (MID motion; paused on hover; static fallback) --- */
.mb-marquee { overflow: hidden; white-space: nowrap; }
.mb-marquee-track {
display: inline-block;
will-change: transform;
animation: mb-marquee 22s linear infinite;
}
.mb-marquee:hover .mb-marquee-track { animation-play-state: paused; }
@keyframes mb-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
.mb-marquee-track { animation: none; }
}
/* --- Reading-progress bar (fluoro; scroll-timeline; reduced-motion guard) --- */
.mb-progress {
position: sticky; top: 0;
height: 4px;
background: hsl(var(--accent));
transform-origin: 0 50%;
animation: mb-progress linear both;
animation-timeline: scroll(root block);
z-index: 50;
}
@keyframes mb-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) {
.mb-progress { animation: none; transform: scaleX(0); }
}
/* --- Per-instance pull-quote accent override (persona quote) --- */
[data-mb-accent="pink"] { --mb-accent-h: 330; --mb-accent-s: 95%; --mb-accent-l: 58%; }
[data-mb-accent="blue"] { --mb-accent-h: 220; --mb-accent-s: 100%; --mb-accent-l: 56%; }
[data-mb-accent="lime"] { --mb-accent-h: 78; --mb-accent-s: 90%; --mb-accent-l: 50%; }
[data-mb-accent] .mb-quote-bar { background-color: hsl(var(--mb-accent-h) var(--mb-accent-s) var(--mb-accent-l)); }
}

View File

@ -1,37 +0,0 @@
[
{
"key": "magazine-bold:default-master",
"title": "Magazine Bold Default Master",
"page_templates": ["default", "full-width", "blog-index", "contact"],
"blocks": [
{ "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "OFFCUT" } }, "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": "columns", "companyName": "OFFCUT", "tagline": "A streetwear label and its cover stories.", "showNewsletter": true, "newsletterTitle": "Drop alerts", "newsletterDescription": "Get the next drop before it sells.", "copyright": "OFFCUT" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "magazine-bold:landing-master",
"title": "Magazine Bold Cover Master",
"page_templates": ["landing"],
"blocks": [
{ "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "OFFCUT" } }, "slot": "masthead", "sort_order": 0 },
{ "block_key": "slot", "title": "Lead", "content": { "slotName": "lead", "placeholder": "Cover story" }, "slot": "lead", "sort_order": 0 },
{ "block_key": "slot", "title": "River", "content": { "slotName": "river", "placeholder": "Recent stories" }, "slot": "river", "sort_order": 0 },
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "columns", "companyName": "OFFCUT", "tagline": "A streetwear label and its cover stories.", "showNewsletter": true, "newsletterTitle": "Drop alerts", "newsletterDescription": "Get the next drop before it sells.", "copyright": "OFFCUT" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "magazine-bold:article-master",
"title": "Magazine Bold Feature Master",
"page_templates": ["article"],
"blocks": [
{ "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "OFFCUT" } }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Byline", "content": { "slotName": "byline" }, "slot": "byline", "sort_order": 0 },
{ "block_key": "slot", "title": "Feature Body", "content": { "slotName": "main", "placeholder": "Feature 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": "columns", "companyName": "OFFCUT", "tagline": "A streetwear label and its cover stories.", "copyright": "OFFCUT" }, "slot": "footer", "sort_order": 0 }
]
}
]

37
masthead.go Normal file
View File

@ -0,0 +1,37 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// MastheadBlockMeta is the issue-masthead (wordmark + folio bar) at the top of the page.
var MastheadBlockMeta = blocks.BlockMeta{
Key: "masthead",
Title: "Issue Masthead",
Description: "Wordmark, issue number and tagline strip — sits at the top of every page.",
Source: "magazine-bold",
Category: blocks.CategoryLayout,
}
// MastheadData is what the templ component consumes.
type MastheadData struct {
IssueNo string
Tagline string
Date string
}
// MastheadBlock renders the issue masthead.
// content keys: issueNo, tagline, date — all optional, no panic on empty.
func MastheadBlock(ctx context.Context, content map[string]any) string {
data := MastheadData{
IssueNo: getString(content, "issueNo"),
Tagline: getString(content, "tagline"),
Date: getString(content, "date"),
}
var buf bytes.Buffer
_ = mastheadComponent(data).Render(ctx, &buf)
return buf.String()
}

18
masthead.templ Normal file
View File

@ -0,0 +1,18 @@
package main
// mastheadComponent renders the wordmark + folio strip.
templ mastheadComponent(data MastheadData) {
<div data-block="magazine-bold:masthead" class="py-6 mb-hairline-bottom flex items-end justify-between gap-6">
<div class="flex items-baseline gap-4">
if data.IssueNo != "" {
<span class="font-mono text-kicker">No. { data.IssueNo }</span>
}
if data.Tagline != "" {
<span class="font-mono text-kicker text-muted-foreground">{ data.Tagline }</span>
}
</div>
if data.Date != "" {
<span class="font-mono text-kicker text-muted-foreground">{ data.Date }</span>
}
</div>
}

106
masthead_templ.go Normal file
View File

@ -0,0 +1,106 @@
// 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 wordmark + folio strip.
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 data-block=\"magazine-bold:masthead\" class=\"py-6 mb-hairline-bottom flex items-end justify-between gap-6\"><div class=\"flex items-baseline gap-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.IssueNo != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<span class=\"font-mono text-kicker\">No. ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.IssueNo)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 8, Col: 58}
}
_, 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
}
}
if data.Tagline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"font-mono text-kicker text-muted-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Tagline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 11, Col: 76}
}
_, 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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Date != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"font-mono text-kicker text-muted-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Date)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 15, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span>")
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
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

70
photo_essay.go Normal file
View File

@ -0,0 +1,70 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// PhotoEssayBlockMeta is the numbered photo-essay scaffolding.
var PhotoEssayBlockMeta = blocks.BlockMeta{
Key: "photo_essay",
Title: "Photo Essay",
Description: "Numbered photo-essay with asymmetric frame spans (half / full / tall) and mono captions.",
Source: "magazine-bold",
Category: blocks.CategoryContent,
}
// PhotoFrame is a single image in the photo essay.
type PhotoFrame struct {
Number int
Image string
Caption string
Span string
}
// PhotoEssayData is what the templ component consumes.
type PhotoEssayData struct {
Title string
Frames []PhotoFrame
}
// PhotoEssayBlock renders the photo essay scaffolding.
// content keys: title (text), frames (collection of {image, caption, span}).
func PhotoEssayBlock(ctx context.Context, content map[string]any) string {
rawFrames := getSlice(content, "frames")
frames := make([]PhotoFrame, 0, len(rawFrames))
for i, item := range rawFrames {
img := blocks.ResolveMediaPath(getString(item, "image"))
// Skip frames that have no image (malformed-content safety).
if img == "" {
continue
}
frames = append(frames, PhotoFrame{
Number: i + 1,
Image: img,
Caption: getString(item, "caption"),
Span: normalizeSpan(getString(item, "span")),
})
}
data := PhotoEssayData{
Title: getString(content, "title"),
Frames: frames,
}
var buf bytes.Buffer
_ = photoEssayComponent(data).Render(ctx, &buf)
return buf.String()
}
// photoFrameSpanClass maps a span string to its CSS utility class.
func photoFrameSpanClass(span string) string {
switch span {
case "full":
return "mb-frame-full"
case "tall":
return "mb-frame-tall"
default:
return "mb-frame-half"
}
}

31
photo_essay.templ Normal file
View File

@ -0,0 +1,31 @@
package main
import "fmt"
// photoEssayComponent renders a numbered photo-essay grid.
templ photoEssayComponent(data PhotoEssayData) {
<section data-block="magazine-bold:photo_essay" class="w-full py-12">
<div class="max-w-6xl mx-auto px-6">
if data.Title != "" {
<h2 class="font-display text-deck text-foreground mb-8">{ data.Title }</h2>
}
if len(data.Frames) == 0 {
<div class="font-mono mb-caption py-12">No frames yet.</div>
} else {
<div class="mb-photo-grid">
for _, frame := range data.Frames {
<figure class={ photoFrameSpanClass(frame.Span) }>
<img src={ frame.Image } alt={ frame.Caption } class="w-full h-auto block" loading="lazy"/>
<figcaption class="mb-caption font-mono mt-2 flex items-baseline gap-3">
<span class="mb-image-folio">{ fmt.Sprintf("%02d", frame.Number) }</span>
if frame.Caption != "" {
<span>{ frame.Caption }</span>
}
</figcaption>
</figure>
}
</div>
}
</div>
</section>
}

167
photo_essay_templ.go Normal file
View File

@ -0,0 +1,167 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
// photoEssayComponent renders a numbered photo-essay grid.
func photoEssayComponent(data PhotoEssayData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"magazine-bold:photo_essay\" class=\"w-full py-12\"><div class=\"max-w-6xl mx-auto px-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Title != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"font-display text-deck text-foreground mb-8\">")
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: `photo_essay.templ`, Line: 10, Col: 72}
}
_, 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, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Frames) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"font-mono mb-caption py-12\">No frames yet.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"mb-photo-grid\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, frame := range data.Frames {
var templ_7745c5c3_Var3 = []any{photoFrameSpanClass(frame.Span)}
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, 6, "<figure 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: `photo_essay.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, 7, "\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(frame.Image)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `photo_essay.templ`, Line: 18, Col: 29}
}
_, 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, 8, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(frame.Caption)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `photo_essay.templ`, Line: 18, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" class=\"w-full h-auto block\" loading=\"lazy\"><figcaption class=\"mb-caption font-mono mt-2 flex items-baseline gap-3\"><span class=\"mb-image-folio\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d", frame.Number))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `photo_essay.templ`, Line: 20, Col: 72}
}
_, 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, 10, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if frame.Caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(frame.Caption)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `photo_essay.templ`, Line: 22, Col: 30}
}
_, 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, 12, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</figcaption></figure>")
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></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -2,8 +2,11 @@
name = "magazine-bold"
display_name = "Magazine Bold"
scope = "@themes"
version = "0.5.0"
description = "A streetwear label's cover-story chrome: massive condensed uppercase headlines, duotone imagery, grid-breaking overlap and fluoro accent slabs. Newsprint plus voltage by day, ink-black plus fluoro by night. For fashion drops, lookbooks and culture publications."
version = "0.1.1"
description = "Editorial-print theme with oversized display type, asymmetric grids and rotating color-block accents for fashion, streetwear and culture publications."
kind = "theme"
categories = ["templates"]
tags = ["fashion", "lifestyle", "streetwear", "magazine", "bold", "display", "editorial", "drops", "lookbook"]
tags = ["fashion", "lifestyle", "streetwear", "magazine", "bold", "display", "music", "editorial", "art"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"

View File

@ -1,15 +1,10 @@
[
{
"id": "newsprint-voltage",
"name": "Newsprint & Voltage",
"description": "The default. Off-white newsprint ground and near-black ink by day, ink-black with a fluoro hot-magenta accent by night.",
"id": "paper-pink",
"name": "Paper & Hot Pink",
"description": "Warm paper-tone background with a hot-pink editorial accent (default).",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:magazine-bold:Archivo Black",
"fontBody": "template:magazine-bold:Inter",
"fontMono": "template:magazine-bold:IBM Plex Mono"
},
"lightColors": {
"background": "38 30% 96%",
"foreground": "0 0% 8%",
@ -23,48 +18,43 @@
"secondaryForeground": "0 0% 8%",
"muted": "38 20% 92%",
"mutedForeground": "0 0% 40%",
"accent": "330 95% 55%",
"accent": "330 95% 58%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 86%",
"input": "0 0% 86%",
"ring": "330 95% 55%"
"border": "0 0% 88%",
"input": "0 0% 88%",
"ring": "330 95% 58%"
},
"darkColors": {
"background": "0 0% 7%",
"background": "0 0% 8%",
"foreground": "38 30% 96%",
"card": "0 0% 11%",
"cardForeground": "38 30% 96%",
"popover": "0 0% 11%",
"popoverForeground": "38 30% 96%",
"primary": "38 30% 96%",
"primaryForeground": "0 0% 7%",
"primaryForeground": "0 0% 8%",
"secondary": "0 0% 14%",
"secondaryForeground": "38 30% 96%",
"muted": "0 0% 14%",
"mutedForeground": "38 10% 62%",
"accent": "330 100% 64%",
"accentForeground": "0 0% 7%",
"mutedForeground": "38 10% 60%",
"accent": "330 95% 62%",
"accentForeground": "0 0% 8%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 20%",
"input": "0 0% 20%",
"ring": "330 100% 64%"
"border": "0 0% 18%",
"input": "0 0% 18%",
"ring": "330 95% 62%"
}
}
},
{
"id": "ink-blue",
"name": "Ink & Electric Blue",
"description": "The same newsprint/ink neutrals with an electric-blue accent — cooler, sportier drop energy.",
"description": "Same paper/ink neutrals with an electric-blue accent.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:magazine-bold:Archivo Black",
"fontBody": "template:magazine-bold:Inter",
"fontMono": "template:magazine-bold:IBM Plex Mono"
},
"lightColors": {
"background": "38 30% 96%",
"foreground": "0 0% 8%",
@ -78,48 +68,43 @@
"secondaryForeground": "0 0% 8%",
"muted": "38 20% 92%",
"mutedForeground": "0 0% 40%",
"accent": "220 100% 54%",
"accent": "220 100% 56%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 86%",
"input": "0 0% 86%",
"ring": "220 100% 54%"
"border": "0 0% 88%",
"input": "0 0% 88%",
"ring": "220 100% 56%"
},
"darkColors": {
"background": "0 0% 7%",
"background": "0 0% 8%",
"foreground": "38 30% 96%",
"card": "0 0% 11%",
"cardForeground": "38 30% 96%",
"popover": "0 0% 11%",
"popoverForeground": "38 30% 96%",
"primary": "38 30% 96%",
"primaryForeground": "0 0% 7%",
"primaryForeground": "0 0% 8%",
"secondary": "0 0% 14%",
"secondaryForeground": "38 30% 96%",
"muted": "0 0% 14%",
"mutedForeground": "38 10% 62%",
"accent": "216 100% 66%",
"accentForeground": "0 0% 7%",
"mutedForeground": "38 10% 60%",
"accent": "220 100% 62%",
"accentForeground": "0 0% 8%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 20%",
"input": "0 0% 20%",
"ring": "216 100% 66%"
"border": "0 0% 18%",
"input": "0 0% 18%",
"ring": "220 100% 62%"
}
}
},
{
"id": "chalk-lime",
"name": "Chalk & Lime",
"description": "Cooler chalk ground and an acid-lime accent — a fresh, contemporary edge. Deeper ink-black night desk.",
"description": "Cooler chalk background with a lime accent — fresh, contemporary edge.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:magazine-bold:Archivo Black",
"fontBody": "template:magazine-bold:Inter",
"fontMono": "template:magazine-bold:IBM Plex Mono"
},
"lightColors": {
"background": "60 15% 97%",
"foreground": "0 0% 8%",
@ -133,13 +118,13 @@
"secondaryForeground": "0 0% 8%",
"muted": "60 12% 93%",
"mutedForeground": "0 0% 40%",
"accent": "78 90% 45%",
"accent": "78 90% 50%",
"accentForeground": "0 0% 8%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 86%",
"input": "0 0% 86%",
"ring": "78 90% 45%"
"border": "0 0% 88%",
"input": "0 0% 88%",
"ring": "78 90% 50%"
},
"darkColors": {
"background": "0 0% 6%",
@ -153,124 +138,14 @@
"secondary": "0 0% 13%",
"secondaryForeground": "60 15% 95%",
"muted": "0 0% 13%",
"mutedForeground": "60 8% 62%",
"accent": "76 92% 56%",
"mutedForeground": "60 8% 60%",
"accent": "78 90% 55%",
"accentForeground": "0 0% 8%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 18%",
"input": "0 0% 18%",
"ring": "76 92% 56%"
}
}
},
{
"id": "flux-green",
"name": "Flux Green",
"description": "Newsprint ground with a fluorescent green accent — rave-flyer energy. Ink-black night with a glowing lime-green pop.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:magazine-bold:Archivo Black",
"fontBody": "template:magazine-bold:Inter",
"fontMono": "template:magazine-bold:IBM Plex Mono"
},
"lightColors": {
"background": "38 28% 95%",
"foreground": "0 0% 8%",
"card": "0 0% 100%",
"cardForeground": "0 0% 8%",
"popover": "0 0% 100%",
"popoverForeground": "0 0% 8%",
"primary": "0 0% 8%",
"primaryForeground": "38 28% 95%",
"secondary": "38 18% 90%",
"secondaryForeground": "0 0% 8%",
"muted": "38 18% 92%",
"mutedForeground": "0 0% 40%",
"accent": "150 82% 38%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 86%",
"input": "0 0% 86%",
"ring": "150 82% 38%"
},
"darkColors": {
"background": "0 0% 7%",
"foreground": "38 28% 95%",
"card": "0 0% 11%",
"cardForeground": "38 28% 95%",
"popover": "0 0% 11%",
"popoverForeground": "38 28% 95%",
"primary": "38 28% 95%",
"primaryForeground": "0 0% 7%",
"secondary": "0 0% 14%",
"secondaryForeground": "38 28% 95%",
"muted": "0 0% 14%",
"mutedForeground": "38 10% 62%",
"accent": "146 90% 52%",
"accentForeground": "0 0% 7%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 20%",
"input": "0 0% 20%",
"ring": "146 90% 52%"
}
}
},
{
"id": "signal-orange",
"name": "Signal Orange",
"description": "Warm newsprint and a hazard-orange accent — hi-vis workwear energy. Ink-black night with a glowing tangerine pop.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:magazine-bold:Archivo Black",
"fontBody": "template:magazine-bold:Inter",
"fontMono": "template:magazine-bold:IBM Plex Mono"
},
"lightColors": {
"background": "38 32% 95%",
"foreground": "0 0% 8%",
"card": "0 0% 100%",
"cardForeground": "0 0% 8%",
"popover": "0 0% 100%",
"popoverForeground": "0 0% 8%",
"primary": "0 0% 8%",
"primaryForeground": "38 32% 95%",
"secondary": "38 20% 90%",
"secondaryForeground": "0 0% 8%",
"muted": "38 20% 92%",
"mutedForeground": "0 0% 40%",
"accent": "18 100% 50%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 86%",
"input": "0 0% 86%",
"ring": "18 100% 50%"
},
"darkColors": {
"background": "0 0% 7%",
"foreground": "38 32% 95%",
"card": "0 0% 11%",
"cardForeground": "38 32% 95%",
"popover": "0 0% 11%",
"popoverForeground": "38 32% 95%",
"primary": "38 32% 95%",
"primaryForeground": "0 0% 7%",
"secondary": "0 0% 14%",
"secondaryForeground": "38 32% 95%",
"muted": "0 0% 14%",
"mutedForeground": "38 10% 62%",
"accent": "22 100% 58%",
"accentForeground": "0 0% 7%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 20%",
"input": "0 0% 20%",
"ring": "22 100% 58%"
"border": "0 0% 17%",
"input": "0 0% 17%",
"ring": "78 90% 55%"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 KiB

After

Width:  |  Height:  |  Size: 90 KiB

37
pull_quote.go Normal file
View File

@ -0,0 +1,37 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// PullQuoteBlockMeta is the color-block pull quote.
var PullQuoteBlockMeta = blocks.BlockMeta{
Key: "pull_quote",
Title: "Pull Quote",
Description: "Color-block pull quote with rotating accent (pink / blue / lime).",
Source: "magazine-bold",
Category: blocks.CategoryContent,
}
// PullQuoteData is what the templ component consumes.
type PullQuoteData struct {
Quote string
Attribution string
Accent string
}
// PullQuoteBlock renders the color-block pull quote.
// content keys: quote (richtext), attribution (text), accent (select).
func PullQuoteBlock(ctx context.Context, content map[string]any) string {
data := PullQuoteData{
Quote: getString(content, "quote"),
Attribution: getString(content, "attribution"),
Accent: normalizeAccent(getString(content, "accent")),
}
var buf bytes.Buffer
_ = pullQuoteComponent(data).Render(ctx, &buf)
return buf.String()
}

20
pull_quote.templ Normal file
View File

@ -0,0 +1,20 @@
package main
// pullQuoteComponent renders a pull quote anchored by an accent color bar.
// The accent value drives the data-mb-accent attribute, which the appended
// CSS uses to colour the .mb-quote-bar regardless of the active preset.
templ pullQuoteComponent(data PullQuoteData) {
<aside data-block="magazine-bold:pull_quote" data-mb-accent={ data.Accent } class="my-8 grid grid-cols-12 gap-4 items-start">
<div class="mb-quote-bar col-span-1 h-full min-h-[3rem]"></div>
<blockquote class="col-span-11 mb-quote-text">
<p class="font-display text-deck">
if data.Quote != "" {
@templ.Raw(data.Quote)
}
</p>
if data.Attribution != "" {
<cite class="block mt-4 font-mono text-kicker text-muted-foreground not-italic">— { data.Attribution }</cite>
}
</blockquote>
</aside>
}

89
pull_quote_templ.go Normal file
View File

@ -0,0 +1,89 @@
// 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 a pull quote anchored by an accent color bar.
// The accent value drives the data-mb-accent attribute, which the appended
// CSS uses to colour the .mb-quote-bar regardless of the active preset.
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 data-block=\"magazine-bold:pull_quote\" data-mb-accent=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Accent)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pull_quote.templ`, Line: 7, Col: 74}
}
_, 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, "\" class=\"my-8 grid grid-cols-12 gap-4 items-start\"><div class=\"mb-quote-bar col-span-1 h-full min-h-[3rem]\"></div><blockquote class=\"col-span-11 mb-quote-text\"><p class=\"font-display text-deck\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Quote != "" {
templ_7745c5c3_Err = templ.Raw(data.Quote).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Attribution != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<cite class=\"block mt-4 font-mono text-kicker text-muted-foreground not-italic\">— ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Attribution)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pull_quote.templ`, Line: 16, Col: 106}
}
_, 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, "</cite>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</blockquote></aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

172
register.go Normal file
View File

@ -0,0 +1,172 @@
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.
func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc {
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
return f(ctx, doc)
}
}
// Register is the plugin entry point that registers the Magazine Bold system
// template, page templates, blocks, overrides and email wrapper.
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
Key: "magazine-bold",
Title: "Magazine Bold",
Description: "Editorial-print theme with oversized display type, asymmetric grids and rotating color-block accents for fashion, streetwear and culture publications.",
})
// Page templates — slots per spec §6 / UAT §3.
if err := tr.RegisterPageTemplate("magazine-bold", templates.PageTemplateMeta{
Key: "default",
Title: "Default",
Description: "Standard editorial page with masthead, asymmetric main, footer colophon.",
Slots: []string{"masthead", "main", "colophon"},
}, wrap(RenderMagazineBold)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("magazine-bold", templates.PageTemplateMeta{
Key: "landing",
Title: "Issue Landing",
Description: "Cover-story hero plus issue navigation strip.",
Slots: []string{"cover", "secondary", "main", "colophon"},
}, wrap(RenderMagazineBoldLanding)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("magazine-bold", templates.PageTemplateMeta{
Key: "article",
Title: "Article / Feature",
Description: "Long-form feature with deck, byline rail and pull-quote gutter.",
Slots: []string{"masthead", "deck", "main", "colophon"},
}, wrap(RenderMagazineBoldArticle)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("magazine-bold", templates.PageTemplateMeta{
Key: "full-width",
Title: "Full Bleed",
Description: "Edge-to-edge photo-essay layout.",
Slots: []string{"masthead", "main", "colophon"},
}, wrap(RenderMagazineBoldFullWidth)); err != nil {
return err
}
// Schemas BEFORE blocks (per UAT §3).
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
return err
}
// Theme blocks.
br.Register(MastheadBlockMeta, MastheadBlock)
br.Register(CoverStoryBlockMeta, CoverStoryBlock)
br.Register(PhotoEssayBlockMeta, PhotoEssayBlock)
br.Register(PullQuoteBlockMeta, PullQuoteBlock)
br.Register(IssueArchiveBlockMeta, IssueArchiveBlock)
br.Register(ColophonBlockMeta, ColophonBlock)
// Built-in overrides — only active when this theme is selected.
br.RegisterTemplateOverride("magazine-bold", "heading", HeadingOverrideBlock)
br.RegisterTemplateOverride("magazine-bold", "text", TextOverrideBlock)
br.RegisterTemplateOverride("magazine-bold", "image", ImageOverrideBlock)
br.RegisterTemplateOverride("magazine-bold", "button", ButtonOverrideBlock)
// Branded email wrapper.
tr.RegisterEmailWrapper("magazine-bold", MagazineBoldEmailWrapper)
return nil
}
// DefaultMasterPages returns the default master pages Magazine Bold seeds.
func DefaultMasterPages() []plugin.MasterPageDefinition {
return []plugin.MasterPageDefinition{
{
Key: "magazine-bold:default-master",
Title: "Magazine Bold — Default",
PageTemplates: []string{"default", "article"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "navbar",
Title: "Masthead Nav",
Content: map[string]any{"menuName": "main"},
Slot: "masthead",
SortOrder: 0,
},
{
BlockKey: "magazine-bold:masthead",
Title: "Issue Masthead",
Content: map[string]any{"issueNo": "01", "tagline": "Vol. 1 / Spring"},
Slot: "masthead",
SortOrder: 1,
},
{
BlockKey: "slot",
Title: "Main Slot",
Content: map[string]any{"slotName": "main"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "magazine-bold:colophon",
Title: "Colophon",
Content: map[string]any{"showSocial": "true"},
Slot: "colophon",
SortOrder: 0,
},
},
},
{
Key: "magazine-bold:landing-master",
Title: "Magazine Bold — Issue Landing",
PageTemplates: []string{"landing", "full-width"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "navbar",
Title: "Masthead Nav",
Content: map[string]any{"menuName": "main"},
Slot: "masthead",
SortOrder: 0,
},
{
BlockKey: "magazine-bold:cover_story",
Title: "Cover Story",
Content: map[string]any{"issueNo": "01"},
Slot: "cover",
SortOrder: 0,
},
{
BlockKey: "magazine-bold:issue_archive",
Title: "Issue Archive",
Content: map[string]any{"bucketSlug": "issues"},
Slot: "secondary",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Main Slot",
Content: map[string]any{"slotName": "main"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "magazine-bold:colophon",
Title: "Colophon",
Content: map[string]any{"showSocial": "true"},
Slot: "colophon",
SortOrder: 0,
},
},
},
}
}

39
registration.go Normal file
View File

@ -0,0 +1,39 @@
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 Magazine Bold template.
var Registration = plugin.PluginRegistration{
Name: "magazine-bold",
Version: plugin.ParseModVersion(pluginModBytes),
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
return Register(tr, br)
},
Assets: func() http.Handler { return AssetsHandler() },
Schemas: func() fs.FS { return Schemas() },
ThemePresets: func() []byte { return ThemePresets() },
BundledFonts: func() []byte { return BundledFonts() },
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
}
// ThemeCSSManifest returns the CSS manifest for Magazine Bold. The
// InputCSSAppend payload is appended into the host Tailwind input so the
// theme's display-type utilities (`.text-folio`, `.text-deck`,
// `.font-display`, etc.) and hairline rules ship with the compiled CSS.
//
// NOTE: per docs/FONTS.md, this pass does NOT inject @font-face declarations.
// The CMS emits @font-face automatically for fonts assigned via the picker.
// We only inject custom utility CSS here.
func ThemeCSSManifest() *plugin.CSSManifest {
return &plugin.CSSManifest{
InputCSSAppend: magazineBoldUtilityCSS,
}
}

View File

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Colophon",
"description": "Mono caption strip with credits and optional social row.",
"type": "object",
"properties": {
"credits": {
"type": "string",
"title": "Credits",
"description": "Mono-set credits block (rich text).",
"x-editor": "richtext"
},
"showSocial": {
"type": "string",
"title": "Show Social Row",
"description": "Toggle the social row beneath the credits.",
"enum": ["true", "false"],
"default": "false",
"x-editor": "select"
}
}
}

View File

@ -1,19 +1,19 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Cover Story",
"description": "Asymmetric magazine cover anchoring the drop / landing page.",
"description": "12-column asymmetric hero anchoring the issue landing page.",
"type": "object",
"properties": {
"kicker": {
"type": "string",
"title": "Kicker",
"description": "Mono label above the headline (e.g. \"Drop 04 / Spring\").",
"description": "Small-caps label above the headline.",
"x-editor": "text"
},
"headline": {
"type": "string",
"title": "Headline",
"description": "Massive display headline (rich text — allows italics).",
"description": "Display headline (rich text — allows italics).",
"x-editor": "richtext"
},
"deck": {
@ -25,21 +25,14 @@
"image": {
"type": "string",
"title": "Cover Image",
"description": "Duotone-treated cover image.",
"description": "Hero image for the cover story.",
"x-editor": "media"
},
"link": {
"type": "string",
"title": "CTA Link",
"description": "URL to the full feature or the shop.",
"title": "Read Story Link",
"description": "URL to the full feature.",
"x-editor": "link"
},
"linkLabel": {
"type": "string",
"title": "CTA Label",
"description": "Button label.",
"x-editor": "text",
"default": "Read the story"
}
}
}

View File

@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Issue Archive",
"description": "Numbered list of back-issues driven by a bucket query.",
"type": "object",
"properties": {
"bucketSlug": {
"type": "string",
"title": "Issues Bucket",
"description": "Bucket that holds the issue rows.",
"x-editor": "bucket-picker"
},
"limit": {
"type": "integer",
"title": "Limit",
"description": "How many issues to render.",
"x-editor": "number",
"minimum": 1,
"maximum": 50,
"default": 12
}
}
}

View File

@ -0,0 +1,26 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Issue Masthead",
"description": "Wordmark + folio bar across the top of the page.",
"type": "object",
"properties": {
"issueNo": {
"type": "string",
"title": "Issue Number",
"description": "Folio number (e.g. \"01\").",
"x-editor": "text"
},
"tagline": {
"type": "string",
"title": "Tagline",
"description": "Issue volume / tagline line (e.g. \"Vol. 1 / Spring\").",
"x-editor": "text"
},
"date": {
"type": "string",
"title": "Date",
"description": "Issue date or month.",
"x-editor": "text"
}
}
}

View File

@ -0,0 +1,47 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Photo Essay",
"description": "Numbered photo-essay scaffolding with asymmetric frame spans.",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Essay Title",
"description": "Essay heading shown above the frames.",
"x-editor": "text"
},
"frames": {
"type": "array",
"title": "Frames",
"description": "Ordered list of photo frames in the essay.",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"image": {
"type": "string",
"title": "Image",
"description": "Frame image.",
"x-editor": "media"
},
"caption": {
"type": "string",
"title": "Caption",
"description": "Mono-set caption.",
"x-editor": "text"
},
"span": {
"type": "string",
"title": "Span",
"description": "Frame layout span.",
"enum": ["half", "full", "tall"],
"default": "half",
"x-editor": "select"
}
},
"required": ["image"]
}
}
}
}

View File

@ -0,0 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Pull Quote",
"description": "Color-block pull quote that sits in the gutter or main column.",
"type": "object",
"properties": {
"quote": {
"type": "string",
"title": "Quote",
"description": "The quote body (rich text).",
"x-editor": "richtext"
},
"attribution": {
"type": "string",
"title": "Attribution",
"description": "Who said it.",
"x-editor": "text"
},
"accent": {
"type": "string",
"title": "Accent",
"description": "Which rotating accent to apply.",
"enum": ["pink", "blue", "lime"],
"default": "pink",
"x-editor": "select"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 805 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 791 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

View File

@ -1,236 +0,0 @@
{
"demo": true,
"settings": {
"merge": {
"companyName": "OFFCUT",
"tagline": "A streetwear label and its cover stories."
}
},
"media": [
{ "id": "14eaa32d-782a-43f2-8a5d-be8f9ff67667", "file": "media/sneakers.jpg", "alt": "Three pairs of boots in a row, lit against black — the Drop 04 cover", "folder": "Demo" },
{ "id": "ad24ad3b-6e03-405a-9b1c-1e185521ee19", "file": "media/editorial-figure.jpg", "alt": "Figure walking the block in the Drop 04 shell jacket", "folder": "Demo" },
{ "id": "54cc7a27-5265-4b8c-9b53-bcd876ffaa8b", "file": "media/streetwear-look.jpg", "alt": "Styled look, Drop 04 cargo set", "folder": "Demo" },
{ "id": "ea1eb2cf-b92f-4dbc-8779-e3968c6564cd", "file": "media/sneakers-street.jpg", "alt": "Detail shot on the street — Drop 04 footwear", "folder": "Demo" }
],
"data_tables": [
{
"key": "contact_submissions",
"name": "Studio Inbox",
"description": "Seeded contact-form submissions for the OFFCUT 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": "OFFCUT",
"template_key": "landing",
"blocks": [
{
"block_key": "magazine-bold:cover_story",
"title": "Cover",
"slot": "lead",
"sort_order": 1,
"content": {
"kicker": "Drop 04 / Spring",
"headline": "Cut from the offcuts",
"deck": "One drop. Made from last season's leftover cloth. Nothing reordered.",
"image": "14eaa32d-782a-43f2-8a5d-be8f9ff67667",
"link": "/drops",
"linkLabel": "See the drop"
}
},
{
"block_key": "magazine-bold:credits_ticker",
"title": "Credits",
"slot": "lead",
"sort_order": 2,
"content": { "items": [ { "label": "Photography Kit Sato" }, { "label": "Styling Remi Vaughn" }, { "label": "Cast Ari, Dune, Lux" }, { "label": "Set The Old Print Works" } ] }
},
{
"block_key": "heading",
"title": "River heading",
"slot": "river",
"sort_order": 1,
"content": { "text": "From the studio", "level": 2 }
},
{
"block_key": "card",
"title": "Story: Offcut method",
"slot": "river",
"sort_order": 2,
"content": { "layout": "vertical", "title": "The offcut method", "text": "<p>How we build a full drop from cloth other people threw away.</p>", "link": "/blog/the-offcut-method", "linkLabel": "Read the story" }
},
{
"block_key": "card",
"title": "Story: Casting the block",
"slot": "river",
"sort_order": 3,
"content": { "layout": "vertical", "title": "Casting off the block", "text": "<p>We cast the drop on the street it was cut on. Here is who showed up.</p>", "link": "/blog/casting-off-the-block", "linkLabel": "Read the story" }
},
{
"block_key": "card",
"title": "Story: One colour",
"slot": "river",
"sort_order": 4,
"content": { "layout": "vertical", "title": "One colour, all season", "text": "<p>Why the whole drop runs a single fluoro accent, and nothing else.</p>", "link": "/blog/one-colour-all-season", "linkLabel": "Read the story" }
},
{
"block_key": "magazine-bold:drop_calendar",
"title": "Drops",
"slot": "river",
"sort_order": 5,
"content": {
"heading": "Drop calendar",
"intro": "Every drop is one run. When it is gone it is gone.",
"drops": [
{ "date": "14 MAR", "name": "Offcut 04", "detail": "Spring drop. Reworked shell jackets and cargo sets.", "status": "live" },
{ "date": "02 MAY", "name": "Offcut 05", "detail": "Workwear capsule. Signed up members first.", "status": "soon" },
{ "date": "09 FEB", "name": "Offcut 03", "detail": "Winter drop. Fleece and heavy cotton.", "status": "sold" }
]
}
},
{
"block_key": "newsletter-signup",
"title": "Drop alerts",
"slot": "river",
"sort_order": 6,
"content": { "title": "Drop alerts", "description": "One email per drop. We tell you when, and nothing else.", "listSlug": "main", "source": "home", "submitText": "Get alerts", "successMessage": "You are on the list. Watch your inbox before the next drop.", "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 OFFCUT", "level": 1 } },
{ "block_key": "rich-text", "title": "Bio", "slot": "main", "sort_order": 2, "content": { "html": "<p>OFFCUT is a streetwear label built on leftover cloth. We buy end-of-run rolls that mills cannot sell and cut them into short, numbered drops.</p><p>Every drop is one run. We do not reorder. When a size is gone it stays gone. That keeps the work honest and keeps the waste down.</p><p>The studio is in an old print works. The photography, the lookbook and the label all come out of the same room.</p>" } },
{
"block_key": "magazine-bold:lookbook",
"title": "Lookbook",
"slot": "main",
"sort_order": 3,
"content": {
"heading": "Drop 04 lookbook",
"intro": "Shot on the block, styled in one colour.",
"looks": [
{ "image": "ad24ad3b-6e03-405a-9b1c-1e185521ee19", "caption": "Shell jacket, offcut nylon", "credit": "On Ari" },
{ "image": "54cc7a27-5265-4b8c-9b53-bcd876ffaa8b", "caption": "Cargo set, reworked cotton", "credit": "On Dune" },
{ "image": "ea1eb2cf-b92f-4dbc-8779-e3968c6564cd", "caption": "Fleece half-zip", "credit": "On Lux" }
]
}
},
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 4, "content": { "style": "block", "align": "center", "quote": "We only make what we can cut from what is already there.", "attribution": "OFFCUT studio" } }
]
},
{
"slug": "/blog",
"title": "Stories",
"template_key": "blog-index",
"blocks": [
{ "block_key": "card", "title": "Featured: Offcut method", "slot": "featured", "sort_order": 1, "content": { "layout": "horizontal", "title": "The offcut method", "text": "<p>How we build a full drop from cloth other people threw away. The lead story this season.</p>", "link": "/blog/the-offcut-method", "linkLabel": "Read the story" } },
{ "block_key": "card", "title": "Post: Casting the block", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "Casting off the block", "text": "<p>We cast the drop on the street it was cut on.</p>", "link": "/blog/casting-off-the-block", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: One colour", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "One colour, all season", "text": "<p>Why the whole drop runs a single fluoro accent.</p>", "link": "/blog/one-colour-all-season", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: The offcut method again", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Numbering the run", "text": "<p>Every piece is numbered by hand. Here is why that matters.</p>", "link": "/blog/the-offcut-method", "linkLabel": "Read on" } }
]
},
{
"slug": "/blog/the-offcut-method",
"title": "The offcut method",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "<p>By the OFFCUT studio. Drop 04.</p>" } },
{ "block_key": "text", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "text": "<p>Most labels start with a colour card and a factory minimum. We start with a phone call about a roll of cloth that nobody wants. That is the whole method, and it shapes everything after it.</p>" } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Mills end every season with rolls they cannot sell. Wrong shade, short length, last year's spec. To them it is dead stock, money tied up in a warehouse. We buy those rolls cheap and we buy them whole. Then we cut a drop around what we actually have.</p><p>That means the run is short. A roll makes forty jackets, not four hundred. When it is cut, the pattern retires. The next drop uses a different cloth and looks like a different label, because it is a different cloth.</p><p>We do not sketch a collection and then go hunting for fabric to match it. We work the other way round. The cloth lands first, we lay it out on the cutting table, and the drop is whatever that cloth wants to be. A stiff shell nylon becomes jackets. A soft brushed cotton becomes the fleece. The material writes the brief.</p><p>Short runs change how the studio works. There is no sampling round after round, no wholesale forecast to hit. We cut, we shoot, we post the drop, and we move on. The whole cycle is weeks, not seasons.</p><p>It changes the numbering too. Every piece carries its number out of a known total — 12 of 40, not one of an endless reprint. When the last one goes, that is the end of it. There is no restock email because there is nothing to restock.</p><p>People ask us to reorder. We cannot, and we would not. The limit is the point. A garment you can buy any day for the next three years is not the same object as one cut from a roll that no longer exists.</p><p>There is a waste argument here, and it is real — offcut cloth that would have been landfill becomes a jacket someone wears for years. But we do not lead with it. We lead with the clothes. The sustainability is a consequence of the method, not the pitch.</p><p>The method has limits we have made peace with. We cannot scale a hit. We cannot promise a colour will come back. We cannot build the kind of catalogue that a normal label leans on. What we can do is make every drop feel like the only one that will ever look like it, because it is.</p><p>That is the offcut method. Buy what nobody wants, cut it while it lasts, number it, and let it go. Then do it again with something else.</p>" } },
{ "block_key": "quote", "title": "Gutter quote", "slot": "marginalia", "sort_order": 1, "content": { "style": "pull", "align": "left", "quote": "A roll makes forty jackets, not four hundred.", "attribution": "Drop 04" } }
]
},
{
"slug": "/blog/casting-off-the-block",
"title": "Casting off the block",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "<p>By the OFFCUT studio. Drop 04.</p>" } },
{ "block_key": "text", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "text": "<p>We do not book models through an agency. We cast the drop on the street it was cut on, and we shoot the people who show up. Here is how the casting went.</p>" } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>We put up one poster outside the print works. It gave a date, a time, and nothing else. On the day, thirty people came by. Some knew the label. Most did not.</p><p>We shot everyone in the same set, in the same light, in one colour. No hair, no retouching past the basics. The clothes had to carry the frame, so we let them.</p><p>The cast in the lookbook are the people who came. That is the only rule we kept.</p>" } },
{ "block_key": "quote", "title": "Gutter quote", "slot": "marginalia", "sort_order": 1, "content": { "style": "pull", "align": "left", "quote": "We shoot the people who show up.", "attribution": "Casting" } }
]
},
{
"slug": "/blog/one-colour-all-season",
"title": "One colour, all season",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "<p>By the OFFCUT studio. Drop 04.</p>" } },
{ "block_key": "text", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "text": "<p>The whole drop runs one accent colour. It is on the labels, the site, the poster and the tape on the boxes. There is nothing else, and that is on purpose.</p>" } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>A single colour makes a small label look sure of itself. It also makes the work easy to spot in a feed full of noise. One fluoro hit, black ink, newsprint paper, and you know it is us before you read a word.</p><p>Next drop the colour changes. The system stays. One accent, held all the way through, then swapped clean for the next run.</p>" } },
{ "block_key": "quote", "title": "Gutter quote", "slot": "marginalia", "sort_order": 1, "content": { "style": "pull", "align": "left", "quote": "One fluoro hit, black ink, newsprint paper.", "attribution": "House style" } }
]
},
{
"slug": "/contact",
"title": "Studio",
"template_key": "contact",
"blocks": [
{
"block_key": "contact-form",
"title": "Write to the studio",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "Write to the studio",
"description": "Stockist requests, press, or a question about a drop. We read everything.",
"submitLabel": "Send",
"successMessage": "Thank you. Your note reached the studio. We will write back.",
"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 us",
"slot": "aside",
"sort_order": 1,
"content": {
"heading": "Elsewhere",
"description": "The studio inbox is best. These work too.",
"columns": 1,
"channels": [
{ "icon": "lucide:mail", "label": "Email", "value": "studio@offcut.example", "link": "mailto:studio@offcut.example" },
{ "icon": "lucide:map-pin", "label": "Studio", "value": "The Old Print Works", "link": "" }
]
}
}
]
},
{
"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 next drop before it goes public.</p>" } }
]
}
],
"menu_items": [
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
{ "menu": "main", "label": "Stories", "page_slug": "/blog", "sort_order": 2 },
{ "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 3 },
{ "menu": "main", "label": "Studio", "page_slug": "/contact", "sort_order": 4 }
]
}

View File

@ -1,19 +0,0 @@
{
"workflows": [
{
"key": "contact_notify_admins",
"name": "Studio note 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" }
}
]
}
]
}

246
template.templ Normal file
View File

@ -0,0 +1,246 @@
package main
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData carries the data the Magazine Bold page templates render.
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 parseMagazineBoldPageData(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,
}
}
// mbHead reuses the core head component with the magazine-bold plugin style sheet.
templ mbHead(data PageData) {
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/magazine-bold/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
}
// MagazineBold — default page template.
// Slots: masthead, main, colophon.
templ MagazineBold(data PageData) {
<!DOCTYPE html>
<html lang="en">
@mbHead(data)
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col font-sans">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full mb-hairline-bottom">
<div class="max-w-6xl mx-auto px-6">
@templ.Raw(data.Slots["masthead"])
</div>
</header>
<main class="flex-grow w-full">
<div class="max-w-6xl mx-auto px-6 py-12">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center font-mono mb-caption">No content blocks assigned to this page.</div>
}
</div>
</main>
<footer class="w-full mt-auto mb-hairline-top">
<div class="max-w-6xl mx-auto px-6">
@templ.Raw(data.Slots["colophon"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// MagazineBoldLanding — issue landing page template.
// Slots: cover, secondary, main, colophon.
templ MagazineBoldLanding(data PageData) {
<!DOCTYPE html>
<html lang="en">
@mbHead(data)
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col font-sans">
@bn.AdminBypassBanner(data.SiteSettings)
<section class="w-full">
@templ.Raw(data.Slots["cover"])
</section>
<section class="w-full border-t border-b mb-hairline">
<div class="max-w-6xl mx-auto px-6 py-10">
@templ.Raw(data.Slots["secondary"])
</div>
</section>
<main class="flex-grow w-full">
<div class="max-w-6xl mx-auto px-6 py-12">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center font-mono mb-caption">No content blocks assigned to this page.</div>
}
</div>
</main>
<footer class="w-full mt-auto mb-hairline-top">
<div class="max-w-6xl mx-auto px-6">
@templ.Raw(data.Slots["colophon"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// MagazineBoldArticle — long-form feature template with deck and pull-quote gutter.
// Slots: masthead, deck, main, colophon.
templ MagazineBoldArticle(data PageData) {
<!DOCTYPE html>
<html lang="en">
@mbHead(data)
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col font-sans">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full mb-hairline-bottom">
<div class="max-w-6xl mx-auto px-6">
@templ.Raw(data.Slots["masthead"])
</div>
</header>
<section class="w-full">
<div class="max-w-6xl mx-auto px-6 py-10">
@templ.Raw(data.Slots["deck"])
</div>
</section>
<main class="flex-grow w-full">
<div class="max-w-4xl mx-auto px-6 py-12">
if main, ok := data.Slots["main"]; ok && main != "" {
<article class="mb-body">
@templ.Raw(main)
</article>
} else {
<div class="py-20 text-center font-mono mb-caption">No content blocks assigned to this page.</div>
}
</div>
</main>
<footer class="w-full mt-auto mb-hairline-top">
<div class="max-w-6xl mx-auto px-6">
@templ.Raw(data.Slots["colophon"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// MagazineBoldFullWidth — edge-to-edge photo-essay template.
// Slots: masthead, main, colophon.
templ MagazineBoldFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en">
@mbHead(data)
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col font-sans">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full mb-hairline-bottom">
<div class="max-w-6xl mx-auto px-6">
@templ.Raw(data.Slots["masthead"])
</div>
</header>
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="max-w-4xl mx-auto py-20 px-6 text-center font-mono mb-caption">No content blocks assigned to this page.</div>
}
</main>
<footer class="w-full mt-auto mb-hairline-top">
<div class="max-w-6xl mx-auto px-6">
@templ.Raw(data.Slots["colophon"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// RenderMagazineBold renders the default page.
func RenderMagazineBold(ctx context.Context, doc map[string]any) templ.Component {
return MagazineBold(parseMagazineBoldPageData(doc))
}
// RenderMagazineBoldLanding renders the issue-landing page.
func RenderMagazineBoldLanding(ctx context.Context, doc map[string]any) templ.Component {
return MagazineBoldLanding(parseMagazineBoldPageData(doc))
}
// RenderMagazineBoldArticle renders the long-form article page.
func RenderMagazineBoldArticle(ctx context.Context, doc map[string]any) templ.Component {
return MagazineBoldArticle(parseMagazineBoldPageData(doc))
}
// RenderMagazineBoldFullWidth renders the edge-to-edge photo-essay page.
func RenderMagazineBoldFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return MagazineBoldFullWidth(parseMagazineBoldPageData(doc))
}

513
template_templ.go Normal file
View File

@ -0,0 +1,513 @@
// 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 the data the Magazine Bold page templates render.
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 parseMagazineBoldPageData(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,
}
}
// mbHead reuses the core head component with the magazine-bold plugin style sheet.
func mbHead(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 = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/magazine-bold/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// MagazineBold — default page template.
// Slots: masthead, main, colophon.
func MagazineBold(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, 1, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = mbHead(data).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col font-sans\">")
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 mb-hairline-bottom\"><div class=\"max-w-6xl mx-auto px-6\">")
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, 4, "</div></header><main class=\"flex-grow w-full\"><div class=\"max-w-6xl mx-auto px-6 py-12\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"py-20 text-center font-mono mb-caption\">No content blocks assigned to this page.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></main><footer class=\"w-full mt-auto mb-hairline-top\"><div class=\"max-w-6xl mx-auto px-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["colophon"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></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
})
}
// MagazineBoldLanding — issue landing page template.
// Slots: cover, secondary, main, colophon.
func MagazineBoldLanding(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, 9, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = mbHead(data).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col font-sans\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<section class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["cover"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</section><section class=\"w-full border-t border-b mb-hairline\"><div class=\"max-w-6xl mx-auto px-6 py-10\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["secondary"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></section><main class=\"flex-grow w-full\"><div class=\"max-w-6xl mx-auto px-6 py-12\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"py-20 text-center font-mono mb-caption\">No content blocks assigned to this page.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></main><footer class=\"w-full mt-auto mb-hairline-top\"><div class=\"max-w-6xl mx-auto px-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["colophon"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></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, 17, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// MagazineBoldArticle — long-form feature template with deck and pull-quote gutter.
// Slots: masthead, deck, main, colophon.
func MagazineBoldArticle(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, 18, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = mbHead(data).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col font-sans\">")
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, 20, "<header class=\"w-full mb-hairline-bottom\"><div class=\"max-w-6xl mx-auto px-6\">")
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, 21, "</div></header><section class=\"w-full\"><div class=\"max-w-6xl mx-auto px-6 py-10\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["deck"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></section><main class=\"flex-grow w-full\"><div class=\"max-w-4xl mx-auto px-6 py-12\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<article class=\"mb-body\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"py-20 text-center font-mono mb-caption\">No content blocks assigned to this page.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></main><footer class=\"w-full mt-auto mb-hairline-top\"><div class=\"max-w-6xl mx-auto px-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["colophon"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// MagazineBoldFullWidth — edge-to-edge photo-essay template.
// Slots: masthead, main, colophon.
func MagazineBoldFullWidth(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_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = mbHead(data).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<body class=\"bg-background text-foreground antialiased min-h-screen flex flex-col font-sans\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<header class=\"w-full mb-hairline-bottom\"><div class=\"max-w-6xl mx-auto px-6\">")
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, 32, "</div></header><main class=\"flex-grow w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div class=\"max-w-4xl mx-auto py-20 px-6 text-center font-mono mb-caption\">No content blocks assigned to this page.</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</main><footer class=\"w-full mt-auto mb-hairline-top\"><div class=\"max-w-6xl mx-auto px-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["colophon"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// RenderMagazineBold renders the default page.
func RenderMagazineBold(ctx context.Context, doc map[string]any) templ.Component {
return MagazineBold(parseMagazineBoldPageData(doc))
}
// RenderMagazineBoldLanding renders the issue-landing page.
func RenderMagazineBoldLanding(ctx context.Context, doc map[string]any) templ.Component {
return MagazineBoldLanding(parseMagazineBoldPageData(doc))
}
// RenderMagazineBoldArticle renders the long-form article page.
func RenderMagazineBoldArticle(ctx context.Context, doc map[string]any) templ.Component {
return MagazineBoldArticle(parseMagazineBoldPageData(doc))
}
// RenderMagazineBoldFullWidth renders the edge-to-edge photo-essay page.
func RenderMagazineBoldFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return MagazineBoldFullWidth(parseMagazineBoldPageData(doc))
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,68 +0,0 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--[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:"#f4f1ea" }}; margin: 0; padding: 0; font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;">
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style="padding: 40px 10px; background-color: {{ colors.background|default:"#f4f1ea" }};">
<table role="presentation" class="email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#ffffff" }}; border: 2px solid {{ colors.foreground|default:"#111111" }};">
<!-- Masthead wordmark -->
<tr>
<td align="left" style="padding: 28px 40px 20px 40px; border-bottom: 2px solid {{ colors.foreground|default:"#111111" }};">
{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 40px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-family: 'Archivo Black', 'Arial Black', sans-serif; text-transform: uppercase; font-size: 30px; line-height: 1; color: {{ colors.foreground|default:"#111111" }}; letter-spacing: -0.02em;">{{ site_name }}</h1>{% endif %}
</td>
</tr>
<!-- Body -->
<tr>
<td class="content-padding" style="padding: 32px 44px 36px 44px; color: {{ colors.foreground|default:"#111111" }}; font-size: 16px; line-height: 1.6; font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;">{{ body|safe }}</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding: 20px 44px 32px 44px; border-top: 2px solid {{ colors.foreground|default:"#111111" }};">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="left" style="font-family: 'IBM Plex Mono', 'Courier New', monospace; font-size: 12px; color: {{ colors.mutedForeground|default:"#6b6b6b" }}; text-transform: uppercase; letter-spacing: 0.1em;">
<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:"#ec0f6e" }}; text-decoration: none; border-bottom: 2px solid {{ colors.accent|default:"#ec0f6e" }};">{{ 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:"#6b6b6b" }}; text-decoration: underline;">Unsubscribe</a></p>{% endif %}
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -1,20 +0,0 @@
<!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;Inter&quot;, system-ui, sans-serif);">
<div class="mb-progress" aria-hidden="true"></div>
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
{# Article template owns the page <h1> (seed carries no level-1 heading block); aligned to the article grid's left column edge. #}
<div class="mx-auto w-full max-w-[84rem] px-4 pt-10 md:pt-14">
{% if title %}<h1 class="mb-heading text-4xl sm:text-5xl md:text-7xl" style="max-width:18ch;">{{ title }}</h1>{% endif %}
{% if slots.byline %}<div class="mt-5 mb-caps text-muted-foreground">{{ slots.byline|safe }}</div>{% endif %}
</div>
<div class="mb-article-grid flex-grow pt-6 pb-10 md:pt-8 md:pb-14">
<main class="mb-dropcap mb-prose">{% 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="mb-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

@ -1,18 +0,0 @@
<!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;Inter&quot;, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
<main class="flex-grow w-full flex flex-col items-center justify-center px-4 py-12">
<div class="w-full max-w-md">
<a href="/" class="block text-center" aria-label="{{ 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="mb-masthead-wordmark text-3xl">{{ site_settings.Title|default:"Magazine Bold" }}</span>{% endif %}
</a>
<hr class="mb-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

@ -1,23 +0,0 @@
<!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;Inter&quot;, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">
<div class="mb-wide py-10 md:py-14">
{% if title %}
<div class="text-center">
<h1 class="mb-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="mb-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

@ -1,22 +0,0 @@
<!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;Inter&quot;, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">
<div class="mb-wide 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

@ -1,13 +0,0 @@
<!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;Inter&quot;, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full py-10 md:py-14">
{% if slots.main %}<div class="mb-column mb-prose">{{ slots.main|safe }}</div>{% else %}<div class="mb-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

@ -1,11 +0,0 @@
<!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;Inter&quot;, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="mb-wide 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

@ -1,14 +0,0 @@
<!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;Inter&quot;, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
{% if slots.masthead %}<header class="w-full">{{ slots.masthead|safe }}</header>{% endif %}
<main class="flex-grow w-full overflow-x-hidden">
{% if slots.lead %}<section class="mb-wide py-8 md:py-12">{{ slots.lead|safe }}</section>{% endif %}
{% if slots.river %}<section class="mb-river pb-10 md:pb-14"><hr class="mb-rule mb-8">{{ slots.river|safe }}</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

@ -1,21 +0,0 @@
{# 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 = "mb-panel" %}
{% if variant == "promo" %}{% set anncls = "mb-panel" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
<div data-block-override="magazine-bold: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="mb-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 width="16" height="16" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
</div>
</div>
{% if dismissable %}<script>
(function(){
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
bars.forEach(function(bar){
bar.setAttribute('data-bn-ann-wired','1');
var key='bnann:'+bar.getAttribute('data-bn-annkey');
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
var btn=bar.querySelector('[data-bn-ann-dismiss]');
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
});
})();
</script>{% endif %}

View File

@ -1,16 +0,0 @@
{# audio-embed override — magazine-bold hairline player card. Fields + facade JS mirror builtin verbatim: url, media, artwork, title, artist, download. #}
<figure data-block-override="magazine-bold:audio-embed" class="bn-audio mb-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="mb-caps truncate text-sm" style="font-family:var(--font-heading, 'Archivo Black', 'Arial Black', sans-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="mb-button inline-flex items-center gap-2 text-sm" aria-label="Play{% if title %}: {{ title }}{% endif %}"><svg width="16" height="16" class="inline-block h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg> Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="mb-link inline-flex items-center gap-1 text-sm"><svg width="16" height="16" class="inline-block h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#download"></use></svg> Download</a>{% endif %}</div></div></div></figure><script>
(function(){
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
e.preventDefault();
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
if(!src)return;
var slot=btn.closest('[data-bn-audio-slot]');
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
btn.remove();slot.insertBefore(a,slot.firstChild);
});
})();
</script>

View File

@ -1,33 +0,0 @@
{# auth-form (magazine-bold) — typographic login/register in an mb-panel with small-caps labels, hairline tab rules and fluoro 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="magazine-bold: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, 'Inter', system-ui, sans-serif);">You are signed in as <strong style="color:hsl(var(--foreground));">{{ context.publicUsername }}</strong></p>
<a href="/account" class="mb-link mt-3 inline-block">My account</a>
</div>
{% else %}
<div data-block-override="magazine-bold:auth-form" class="auth-form mb-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="mb-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="mb-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="mb-caps text-muted-foreground block text-xs mb-1">Email</label><input type="email" name="email" required class="w-full mb-input" /></div>
<div class="mb-4"><label class="mb-caps text-muted-foreground block text-xs mb-1">Password</label><input type="password" name="password" required class="w-full mb-input" /></div>
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
<button type="submit" class="mb-button w-full" hx-disabled-elt="this">Sign in</button>
<p class="text-center mt-3 text-sm"><a href="/forgot-password" class="mb-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="mb-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 mb-input" /></div>
<div class="mb-3"><label class="mb-caps text-muted-foreground block text-xs mb-1">Email</label><input type="email" name="email" required class="w-full mb-input" /></div>
<div class="mb-4"><label class="mb-caps text-muted-foreground block text-xs mb-1">Password</label><input type="password" name="password" required minlength="8" class="w-full mb-input" /></div>
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
<button type="submit" class="mb-button w-full" hx-disabled-elt="this">Create account</button>
</form>
</div>
</div>
{% endif %}

View File

@ -1,31 +0,0 @@
{# auth-status (magazine-bold) — header login state; small-caps links, hairline monogram and quiet mb-panel dropdown. Driven by context.isPublicLoggedIn / context.publicDisplayName / context.publicUsername; logout posts /api/auth/logout. #}
{% if context.isPublicLoggedIn %}
<div data-block-override="magazine-bold:auth-status" class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="mb-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 width="16" height="16" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div id="auth-dropdown-menu" class="hidden absolute right-0 top-full mt-1 w-48 mb-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 mb-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="magazine-bold:auth-status" class="auth-status flex items-center gap-2">
<a href="/login" class="mb-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="mb-button text-xs">Reserve</a>
</div>
{% endif %}

View File

@ -1,22 +0,0 @@
{# author-bio-hero (magazine-bold) — author-page header framed by hairline rules over generous whitespace. #}
{% if author %}
<section data-block-override="magazine-bold:author-bio-hero" class="mb-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="mb-rule w-12"></span><span class="mb-rule w-12"></span></div>
<h1 class="mb-caps text-3xl md:text-4xl text-foreground" style="font-family:var(--font-heading, &quot;Archivo Black&quot;, 'Arial Black', sans-serif)">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="mb-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;Inter&quot;, system-ui, sans-serif)">{{ author.bio }}</p>{% endif %}
<div class="mt-6 flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground">
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
</div>
{% if showSocial %}<div class="mt-6 flex items-center justify-center gap-4">
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]" title="Website"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]" title="Twitter"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]" title="LinkedIn"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]" title="GitHub"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -1,14 +0,0 @@
{# author-bio (magazine-bold) — byline card in an mb-panel with hairline rule and small-caps label. #}
{% if post.author_name %}
<aside data-block-override="magazine-bold:author-bio" class="mb-panel bg-card p-6 text-card-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="mb-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="mb-caps text-base text-card-foreground" style="font-family:var(--font-heading, &quot;Archivo Black&quot;, 'Arial Black', sans-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="mb-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;Inter&quot;, system-ui, sans-serif)">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -1,24 +0,0 @@
{# blog-hero (magazine-bold) — blog-index header + featured posts from the `posts` provider; typographic, hairline rules. #}
<section data-block-override="magazine-bold: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="mb-caps mb-underline text-4xl md:text-5xl text-foreground" style="font-family:var(--font-heading, &quot;Archivo Black&quot;, 'Arial Black', sans-serif)">{{ title }}</h1>{% endif %}
{% if subtitle %}<p class="mt-4 text-lg text-muted-foreground" style="font-family:var(--font-body, &quot;Inter&quot;, system-ui, sans-serif)">{{ subtitle }}</p>{% endif %}
</div>
{% if posts %}
<div class="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
{% for post in posts %}
<article class="group flex flex-col{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-8{% endif %}">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="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="mb-rule mb-4 block w-12"></span>
<h2 class="mb-caps {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} leading-snug text-foreground" style="font-family:var(--font-heading, &quot;Archivo Black&quot;, 'Arial Black', sans-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;Inter&quot;, system-ui, sans-serif)">{{ post.excerpt }}</p>{% endif %}
<div class="mt-4 flex flex-wrap items-center gap-x-3 text-xs text-muted-foreground">{% if post.author_name %}<span class="mb-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

@ -1,13 +0,0 @@
{# breadcrumbs (magazine-bold) — small-caps trail with hairline fluoro separators; current item plain. #}
{% set arrows = template == "arrows" %}
<nav aria-label="Breadcrumb" data-block-override="magazine-bold: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="mb-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 width="14" height="14" 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="mb-caps text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="mb-caps text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]">{{ item.label }}</a>{% endif %}
</li>
{% endfor %}
</ol>
</nav>

View File

@ -1,10 +0,0 @@
{# button override — hairline uppercase button; ghost/link/secondary styles render as an inline fluoro underline link. Fields: link, style, label, size, icon, iconPosition, newTab, align. #}
{% with align=align|default:"left" %}
<div data-block-override="magazine-bold: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="mb-link inline-flex items-center gap-2 text-sm">{% if icon and iconPosition != "right" %}<svg width="16" height="16" class="inline-block h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}<svg width="16" height="16" class="inline-block h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</a>
{% else %}
<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="mb-button inline-flex items-center gap-2">{% if icon and iconPosition != "right" %}<svg width="16" height="16" class="inline-block h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}<svg width="16" height="16" class="inline-block h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</a>
{% endif %}
</div>
{% endwith %}

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