commit fe754f634bc8b9d93e51478811bba37e773af57f Author: Alex Dunmow Date: Sat Jun 6 14:11:38 2026 +0800 initial: theme plugin magazine-bold Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously an unversioned directory inside ~/src/blockninja-themes/magazine-bold. Co-Authored-By: Claude Opus 4.7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f780e6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.so +*.test +tmp/ +.idea/ +.vscode/ diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md new file mode 100644 index 0000000..df054a0 --- /dev/null +++ b/BUILD_REPORT.md @@ -0,0 +1,193 @@ +# Magazine Bold — Build Report + +Generated from a clean wave-1 implementation pass against the spec at +`themes/docs/works/magazine-bold.md` (§§1–15) and the UAT at +`themes/docs/uat/magazine-bold.md`, with `themes/docs/FONTS.md` overriding +spec §5 and UAT §11. + +## What landed + +### Identity & metadata +- `plugin.mod` — `name = magazine-bold`, `kind = theme`, `scope = @themes`, + `version = 0.1.0`, `categories = ["templates", "media"]`, + `tags = [fashion, lifestyle, streetwear, magazine, bold, display, music, + editorial, art]` (9 entries, all from the whitelist), `block_core + compatibility = ">=0.11.0 <0.12.0"`, description verbatim from spec §1. +- `go.mod` — module `git.dev.alexdunmow.com/block/themes/magazine-bold`, + Go directive `1.26.4`, requires `block/core v0.11.1` and `a-h/templ + v0.3.1020`, no `replace` directives. + +### File layout (templ-style) +``` +magazine-bold/ + plugin.mod go.mod go.sum + Makefile registration.go register.go + embed.go helpers.go css.go + template.templ + _templ.go # 4 page renderers + masthead.{go,templ} + _templ.go + cover_story.{go,templ} + _templ.go + photo_essay.{go,templ} + _templ.go + pull_quote.{go,templ} + _templ.go + issue_archive.{go,templ} + _templ.go + colophon.{go,templ} + _templ.go + heading_override.{go,templ} + _templ.go + text_override.{go,templ} + _templ.go + image_override.{go,templ} + _templ.go + button_override.{go,templ} + _templ.go + email_wrapper.{go,templ} + _templ.go + schemas/masthead.schema.json + schemas/cover_story.schema.json + schemas/photo_essay.schema.json + schemas/pull_quote.schema.json + schemas/issue_archive.schema.json + schemas/colophon.schema.json + presets.json fonts.json assets/.gitkeep + RECOMMENDED_FONTS.md BUILD_REPORT.md +``` + +### Registration +- One `tr.RegisterSystemTemplate({Key: "magazine-bold"})` call. +- Four `tr.RegisterPageTemplate("magazine-bold", ...)` calls with the slots: + - `default`: `["masthead", "main", "colophon"]` + - `landing`: `["cover", "secondary", "main", "colophon"]` + - `article`: `["masthead", "deck", "main", "colophon"]` + - `full-width`: `["masthead", "main", "colophon"]` +- `br.LoadSchemasFromFS(Schemas())` called BEFORE the first `br.Register(...)`. +- Six theme blocks registered with `Source: "magazine-bold"`: + - `masthead` → CategoryLayout + - `cover_story` → CategoryContent + - `photo_essay` → CategoryContent + - `pull_quote` → CategoryContent + - `issue_archive` → CategoryNavigation + - `colophon` → CategoryLayout +- Four built-in overrides via `br.RegisterTemplateOverride("magazine-bold", …)`: + `heading`, `text`, `image`, `button`. +- One `tr.RegisterEmailWrapper("magazine-bold", MagazineBoldEmailWrapper)`. + +### Master pages +Both master pages from spec §7 are seeded in `DefaultMasterPages()`: +- `magazine-bold:default-master` — `PageTemplates: [default, article]`, + blocks: `navbar`@masthead/0 → `magazine-bold:masthead`@masthead/1 → + `slot`@main/0 → `magazine-bold:colophon`@colophon/0. +- `magazine-bold:landing-master` — `PageTemplates: [landing, full-width]`, + blocks: `navbar`@masthead/0 → `magazine-bold:cover_story`@cover/0 → + `magazine-bold:issue_archive`@secondary/0 → `slot`@main/0 → + `magazine-bold:colophon`@colophon/0. +- Every `slot` block carries `Content["slotName"] == "main"`. + +### Presets (presets.json) +Three presets, each `mode: "both"`, each with both `lightColors` and +`darkColors` blocks carrying all 19 theme tokens: +- `paper-pink` — Paper & Hot Pink (default), spec §4 verbatim. +- `ink-blue` — same neutrals, accent swapped to `220 100% 56%` (light) / + `220 100% 62%` (dark), ring matches. +- `chalk-lime` — cool chalk background (`60 15% 97%` light / `0 0% 6%` dark), + lime accent (`78 90% 50%` / `78 90% 55%`), accentForeground reads ink + in both modes for ≥ 4.5:1 contrast on lime. + +All HSL triples are bare strings — no `hsl(…)` wrappers anywhere. + +### Schemas +All six schemas are draft-07, list properties matching the spec §8 field +table, and use only `x-editor` types from the allowed set. `pull_quote.accent` +enum is `["pink", "blue", "lime"]`. `photo_essay.frames` items use +`x-editor: collection` with item-level `media`, `text` and `select` (span +enum `["half", "full", "tall"]`). + +### CSS strategy +`CSSManifest.InputCSSAppend` ships a single utility-CSS block at +`magazine-bold/css.go` (`magazineBoldUtilityCSS`). It defines: +- `.font-display`, `.font-serif-sub`, `.font-sans`, `.font-mono` — each + consumes `var(--font-heading|body|mono, )` per FONTS.md. +- `.text-folio` clamp (220pt-equivalent at 1440px → ~80px at 360px). +- `.text-deck`, `.text-kicker` display-size utilities. +- `.mb-hairline*` for the 1px ink rules. +- `.mb-grid-12`, `.mb-grid-asym-cover`, `.mb-col-folio`, `.mb-col-headline`, + `.mb-col-deck` for the 12-column asymmetric layout. +- `.mb-photo-grid`, `.mb-frame-{half,full,tall}` for the photo-essay spans + (collapsing to 1-col at ≤ 768px). +- `.mb-dropcap` for the heading-override drop-cap. +- `.mb-button` squared-ink button with accent hover and a `:focus-visible` + outline using `hsl(var(--ring))`. +- `.mb-caption`, `.mb-image-folio` mono caption strip. +- `.colorblock-accent` + per-accent `[data-mb-accent="pink|blue|lime"]` + variants for pull quotes (using literal HSL triples that match the spec + §4 accent values so a pinned accent reads correctly across presets). + +No hardcoded hex, rgb(), or named colors are used in `.templ` / `.go` / +`.html` files outside the email wrapper (see "Open items" below). + +### Email wrapper +`RegisterEmailWrapper("magazine-bold", MagazineBoldEmailWrapper)` ships a +600px `` with a paper-pink default background, an ink hairline below +the PP-Editorial-style wordmark (with Georgia / Times New Roman / serif +fallback for Outlook 365 web), a 16/24 Inter body, and a mono colophon +strip echoing the web colophon. Hex defaults inlined in `email_wrapper.go` +are derived from the paper-pink light preset so the layout reads correctly +in clients that strip the host CSS. + +### Build output +- `make` produced `magazine-bold.so` at 21 MB (CGO `-buildmode=plugin + -ldflags="-s -w"`). +- `nm --dynamic magazine-bold.so | grep Registration` shows + `D git.dev.alexdunmow.com/block/themes/magazine-bold.Registration` + exported as the Go-plugin entry point. +- Zero `warning:` lines in the build output. + +### Safety check +- `cd ~/src/blockninja/check-safety && go run . ~/src/blockninja/cms + --plugin-dir ~/src/blockninja/themes/magazine-bold` → **exit 0**, all 22 + checks `OK` or `SKIP` (the SKIPs are frontend / orchestrator checks that + don't apply to a templ-only theme plugin). +- The task brief referenced the older path + `~/src/blockninja/backend/cmd/check-safety`, but the repo has been + re-organised so `check-safety/` now lives at the top level as its own + Go module. The equivalent invocation is the one above; behaviour is + identical and the plugin passes cleanly. + +## Open items / deferred + +- **Fonts (wave-2):** per `docs/FONTS.md`, this pass ships + `fonts.json = []` and no woff2 files. PP Editorial New, Migra, Inter and + JetBrains Mono are documented as recommended picks in + `RECOMMENDED_FONTS.md`. Wave-2 will commission / licence the commercial + faces (PP Editorial New + Migra) and bundle them with a `LICENSES.md`. +- **Email wrapper hex defaults:** the wrapper inlines six hex literals + (`#F7F2EB`, `#141414`, `#EEE9E0`, `#666666`, `#E0E0E0`, `#FFFFFF`) as + client-strip-proof defaults for the paper-pink preset. Mail clients + ignore `var()` and (often) strip ` + + + if emailCtx.PreviewText != "" { +
+ { emailCtx.PreviewText } +
+ } +
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + +} diff --git a/email_wrapper_templ.go b/email_wrapper_templ.go new file mode 100644 index 0000000..8c5ed75 --- /dev/null +++ b/email_wrapper_templ.go @@ -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, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.PreviewText != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.SiteSettings.LogoURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"")") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if emailCtx.SiteSettings.SiteName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
 
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if emailCtx.SiteSettings.SiteURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if emailCtx.UnsubscribeURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..101c4b3 --- /dev/null +++ b/embed.go @@ -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 +} diff --git a/fonts.json b/fonts.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/fonts.json @@ -0,0 +1 @@ +[] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..79defbf --- /dev/null +++ b/go.mod @@ -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.11.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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..26aea2c --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +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.11.1 h1:5b3Ps9CLor2FGyxw/Qovt27AGZKR5Xi1JZGi/TfliTA= +git.dev.alexdunmow.com/block/core v0.11.1/go.mod h1:ZwzEOxRDLDfrhQGqo6hLw01/C1z/aS4Dm9ljQMl0Bg4= +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= diff --git a/heading_override.go b/heading_override.go new file mode 100644 index 0000000..378eabb --- /dev/null +++ b/heading_override.go @@ -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" + } +} diff --git a/heading_override.templ b/heading_override.templ new file mode 100644 index 0000000..072c13d --- /dev/null +++ b/heading_override.templ @@ -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: +

{ text }

+ case 2: +

{ text }

+ case 3: +

{ text }

+ case 4: +

{ text }

+ case 5: +
{ text }
+ case 6: +
{ text }
+ default: +

{ text }

+ } +} diff --git a/heading_override_templ.go b/heading_override_templ.go new file mode 100644 index 0000000..ddef8da --- /dev/null +++ b/heading_override_templ.go @@ -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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "

") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + 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, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..b5288fe --- /dev/null +++ b/helpers.go @@ -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" + } +} diff --git a/image_override.go b/image_override.go new file mode 100644 index 0000000..13fb76a --- /dev/null +++ b/image_override.go @@ -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() +} diff --git a/image_override.templ b/image_override.templ new file mode 100644 index 0000000..cad9de2 --- /dev/null +++ b/image_override.templ @@ -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 . +templ mbImageComponent(src, alt, caption string, fullBleed bool) { + if src != "" { +
+ { + if caption != "" { +
{ caption }
+ } +
+ } +} diff --git a/image_override_templ.go b/image_override_templ.go new file mode 100644 index 0000000..2a5f4d2 --- /dev/null +++ b/image_override_templ.go @@ -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 . +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, "
\"") ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if caption != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/issue_archive.go b/issue_archive.go new file mode 100644 index 0000000..38ec6fa --- /dev/null +++ b/issue_archive.go @@ -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() +} diff --git a/issue_archive.templ b/issue_archive.templ new file mode 100644 index 0000000..2e5b986 --- /dev/null +++ b/issue_archive.templ @@ -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) { + +} diff --git a/issue_archive_templ.go b/issue_archive_templ.go new file mode 100644 index 0000000..9660673 --- /dev/null +++ b/issue_archive_templ.go @@ -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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/masthead.go b/masthead.go new file mode 100644 index 0000000..9888342 --- /dev/null +++ b/masthead.go @@ -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() +} diff --git a/masthead.templ b/masthead.templ new file mode 100644 index 0000000..bae533e --- /dev/null +++ b/masthead.templ @@ -0,0 +1,18 @@ +package main + +// mastheadComponent renders the wordmark + folio strip. +templ mastheadComponent(data MastheadData) { +
+
+ if data.IssueNo != "" { + No. { data.IssueNo } + } + if data.Tagline != "" { + { data.Tagline } + } +
+ if data.Date != "" { + { data.Date } + } +
+} diff --git a/masthead_templ.go b/masthead_templ.go new file mode 100644 index 0000000..5c31fb0 --- /dev/null +++ b/masthead_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.IssueNo != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "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, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Tagline != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Date != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + 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, "") + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/photo_essay.go b/photo_essay.go new file mode 100644 index 0000000..4a652a8 --- /dev/null +++ b/photo_essay.go @@ -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" + } +} diff --git a/photo_essay.templ b/photo_essay.templ new file mode 100644 index 0000000..aa037ef --- /dev/null +++ b/photo_essay.templ @@ -0,0 +1,31 @@ +package main + +import "fmt" + +// photoEssayComponent renders a numbered photo-essay grid. +templ photoEssayComponent(data PhotoEssayData) { +
+
+ if data.Title != "" { +

{ data.Title }

+ } + if len(data.Frames) == 0 { +
No frames yet.
+ } else { +
+ for _, frame := range data.Frames { +
+ { +
+ { fmt.Sprintf("%02d", frame.Number) } + if frame.Caption != "" { + { frame.Caption } + } +
+
+ } +
+ } +
+
+} diff --git a/photo_essay_templ.go b/photo_essay_templ.go new file mode 100644 index 0000000..c2d45a6 --- /dev/null +++ b/photo_essay_templ.go @@ -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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Frames) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
No frames yet.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + 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, "
\"")
") + 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, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if frame.Caption != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/plugin.mod b/plugin.mod new file mode 100644 index 0000000..edfd4b6 --- /dev/null +++ b/plugin.mod @@ -0,0 +1,12 @@ +[plugin] +name = "magazine-bold" +display_name = "Magazine Bold" +scope = "@themes" +version = "0.1.0" +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", "media"] +tags = ["fashion", "lifestyle", "streetwear", "magazine", "bold", "display", "music", "editorial", "art"] + +[compatibility] +block_core = ">=0.11.0 <0.12.0" diff --git a/presets.json b/presets.json new file mode 100644 index 0000000..52e8cef --- /dev/null +++ b/presets.json @@ -0,0 +1,152 @@ +[ + { + "id": "paper-pink", + "name": "Paper & Hot Pink", + "description": "Warm paper-tone background with a hot-pink editorial accent (default).", + "theme": { + "mode": "both", + "lightColors": { + "background": "38 30% 96%", + "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 30% 96%", + "secondary": "38 20% 90%", + "secondaryForeground": "0 0% 8%", + "muted": "38 20% 92%", + "mutedForeground": "0 0% 40%", + "accent": "330 95% 58%", + "accentForeground": "0 0% 100%", + "destructive": "0 84% 55%", + "destructiveForeground": "0 0% 100%", + "border": "0 0% 88%", + "input": "0 0% 88%", + "ring": "330 95% 58%" + }, + "darkColors": { + "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% 8%", + "secondary": "0 0% 14%", + "secondaryForeground": "38 30% 96%", + "muted": "0 0% 14%", + "mutedForeground": "38 10% 60%", + "accent": "330 95% 62%", + "accentForeground": "0 0% 8%", + "destructive": "0 84% 60%", + "destructiveForeground": "0 0% 100%", + "border": "0 0% 18%", + "input": "0 0% 18%", + "ring": "330 95% 62%" + } + } + }, + { + "id": "ink-blue", + "name": "Ink & Electric Blue", + "description": "Same paper/ink neutrals with an electric-blue accent.", + "theme": { + "mode": "both", + "lightColors": { + "background": "38 30% 96%", + "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 30% 96%", + "secondary": "38 20% 90%", + "secondaryForeground": "0 0% 8%", + "muted": "38 20% 92%", + "mutedForeground": "0 0% 40%", + "accent": "220 100% 56%", + "accentForeground": "0 0% 100%", + "destructive": "0 84% 55%", + "destructiveForeground": "0 0% 100%", + "border": "0 0% 88%", + "input": "0 0% 88%", + "ring": "220 100% 56%" + }, + "darkColors": { + "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% 8%", + "secondary": "0 0% 14%", + "secondaryForeground": "38 30% 96%", + "muted": "0 0% 14%", + "mutedForeground": "38 10% 60%", + "accent": "220 100% 62%", + "accentForeground": "0 0% 8%", + "destructive": "0 84% 60%", + "destructiveForeground": "0 0% 100%", + "border": "0 0% 18%", + "input": "0 0% 18%", + "ring": "220 100% 62%" + } + } + }, + { + "id": "chalk-lime", + "name": "Chalk & Lime", + "description": "Cooler chalk background with a lime accent — fresh, contemporary edge.", + "theme": { + "mode": "both", + "lightColors": { + "background": "60 15% 97%", + "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": "60 15% 97%", + "secondary": "60 12% 91%", + "secondaryForeground": "0 0% 8%", + "muted": "60 12% 93%", + "mutedForeground": "0 0% 40%", + "accent": "78 90% 50%", + "accentForeground": "0 0% 8%", + "destructive": "0 84% 55%", + "destructiveForeground": "0 0% 100%", + "border": "0 0% 88%", + "input": "0 0% 88%", + "ring": "78 90% 50%" + }, + "darkColors": { + "background": "0 0% 6%", + "foreground": "60 15% 95%", + "card": "0 0% 10%", + "cardForeground": "60 15% 95%", + "popover": "0 0% 10%", + "popoverForeground": "60 15% 95%", + "primary": "60 15% 95%", + "primaryForeground": "0 0% 6%", + "secondary": "0 0% 13%", + "secondaryForeground": "60 15% 95%", + "muted": "0 0% 13%", + "mutedForeground": "60 8% 60%", + "accent": "78 90% 55%", + "accentForeground": "0 0% 8%", + "destructive": "0 84% 60%", + "destructiveForeground": "0 0% 100%", + "border": "0 0% 17%", + "input": "0 0% 17%", + "ring": "78 90% 55%" + } + } + } +] diff --git a/pull_quote.go b/pull_quote.go new file mode 100644 index 0000000..796c03f --- /dev/null +++ b/pull_quote.go @@ -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() +} diff --git a/pull_quote.templ b/pull_quote.templ new file mode 100644 index 0000000..f8b2fee --- /dev/null +++ b/pull_quote.templ @@ -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) { + +} diff --git a/pull_quote_templ.go b/pull_quote_templ.go new file mode 100644 index 0000000..a0d3f96 --- /dev/null +++ b/pull_quote_templ.go @@ -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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/register.go b/register.go new file mode 100644 index 0000000..58b4e08 --- /dev/null +++ b/register.go @@ -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, + }, + }, + }, + } +} diff --git a/registration.go b/registration.go new file mode 100644 index 0000000..9115ed7 --- /dev/null +++ b/registration.go @@ -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, + } +} diff --git a/schemas/colophon.schema.json b/schemas/colophon.schema.json new file mode 100644 index 0000000..64f3583 --- /dev/null +++ b/schemas/colophon.schema.json @@ -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" + } + } +} diff --git a/schemas/cover_story.schema.json b/schemas/cover_story.schema.json new file mode 100644 index 0000000..e25bf86 --- /dev/null +++ b/schemas/cover_story.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cover Story", + "description": "12-column asymmetric hero anchoring the issue landing page.", + "type": "object", + "properties": { + "kicker": { + "type": "string", + "title": "Kicker", + "description": "Small-caps label above the headline.", + "x-editor": "text" + }, + "headline": { + "type": "string", + "title": "Headline", + "description": "Display headline (rich text — allows italics).", + "x-editor": "richtext" + }, + "deck": { + "type": "string", + "title": "Deck", + "description": "Sub-headline / standfirst paragraph.", + "x-editor": "richtext" + }, + "image": { + "type": "string", + "title": "Cover Image", + "description": "Hero image for the cover story.", + "x-editor": "media" + }, + "link": { + "type": "string", + "title": "Read Story Link", + "description": "URL to the full feature.", + "x-editor": "link" + } + } +} diff --git a/schemas/issue_archive.schema.json b/schemas/issue_archive.schema.json new file mode 100644 index 0000000..142cd3a --- /dev/null +++ b/schemas/issue_archive.schema.json @@ -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 + } + } +} diff --git a/schemas/masthead.schema.json b/schemas/masthead.schema.json new file mode 100644 index 0000000..446ccb3 --- /dev/null +++ b/schemas/masthead.schema.json @@ -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" + } + } +} diff --git a/schemas/photo_essay.schema.json b/schemas/photo_essay.schema.json new file mode 100644 index 0000000..8fd54f3 --- /dev/null +++ b/schemas/photo_essay.schema.json @@ -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"] + } + } + } +} diff --git a/schemas/pull_quote.schema.json b/schemas/pull_quote.schema.json new file mode 100644 index 0000000..b104c6a --- /dev/null +++ b/schemas/pull_quote.schema.json @@ -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" + } + } +} diff --git a/template.templ b/template.templ new file mode 100644 index 0000000..4ff0217 --- /dev/null +++ b/template.templ @@ -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) { + + + @mbHead(data) + + @bn.AdminBypassBanner(data.SiteSettings) +
+
+ @templ.Raw(data.Slots["masthead"]) +
+
+
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
No content blocks assigned to this page.
+ } +
+
+
+
+ @templ.Raw(data.Slots["colophon"]) +
+
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// MagazineBoldLanding — issue landing page template. +// Slots: cover, secondary, main, colophon. +templ MagazineBoldLanding(data PageData) { + + + @mbHead(data) + + @bn.AdminBypassBanner(data.SiteSettings) +
+ @templ.Raw(data.Slots["cover"]) +
+
+
+ @templ.Raw(data.Slots["secondary"]) +
+
+
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
No content blocks assigned to this page.
+ } +
+
+
+
+ @templ.Raw(data.Slots["colophon"]) +
+
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// MagazineBoldArticle — long-form feature template with deck and pull-quote gutter. +// Slots: masthead, deck, main, colophon. +templ MagazineBoldArticle(data PageData) { + + + @mbHead(data) + + @bn.AdminBypassBanner(data.SiteSettings) +
+
+ @templ.Raw(data.Slots["masthead"]) +
+
+
+
+ @templ.Raw(data.Slots["deck"]) +
+
+
+
+ if main, ok := data.Slots["main"]; ok && main != "" { +
+ @templ.Raw(main) +
+ } else { +
No content blocks assigned to this page.
+ } +
+
+
+
+ @templ.Raw(data.Slots["colophon"]) +
+
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// MagazineBoldFullWidth — edge-to-edge photo-essay template. +// Slots: masthead, main, colophon. +templ MagazineBoldFullWidth(data PageData) { + + + @mbHead(data) + + @bn.AdminBypassBanner(data.SiteSettings) +
+
+ @templ.Raw(data.Slots["masthead"]) +
+
+
+ if main, ok := data.Slots["main"]; ok && main != "" { + @templ.Raw(main) + } else { +
No content blocks assigned to this page.
+ } +
+
+
+ @templ.Raw(data.Slots["colophon"]) +
+
+ @bn.BodyEnd(data.SiteSettings) + + +} + +// 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)) +} diff --git a/template_templ.go b/template_templ.go new file mode 100644 index 0000000..c07ec50 --- /dev/null +++ b/template_templ.go @@ -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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
No content blocks assigned to this page.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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, "
") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
") + 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, "
No content blocks assigned to this page.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(data.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, "
") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
No content blocks assigned to this page.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + 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, "
") + 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, "") + 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
No content blocks assigned to this page.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + 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, "
") + 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, "") + 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 diff --git a/text_override.go b/text_override.go new file mode 100644 index 0000000..d260fc5 --- /dev/null +++ b/text_override.go @@ -0,0 +1,19 @@ +package main + +import ( + "bytes" + "context" +) + +// TextOverrideBlock renders body text with the Magazine Bold Inter cadence +// and a margin folio. +// +// Built-in text content shape: {"text": "...", "class": "..."} +func TextOverrideBlock(ctx context.Context, content map[string]any) string { + text := getString(content, "text") + class := getString(content, "class") + + var buf bytes.Buffer + _ = mbTextComponent(text, class).Render(ctx, &buf) + return buf.String() +} diff --git a/text_override.templ b/text_override.templ new file mode 100644 index 0000000..cfe45c4 --- /dev/null +++ b/text_override.templ @@ -0,0 +1,8 @@ +package main + +// mbTextComponent renders a text run in Magazine Bold body cadence (Inter, 16–18px tight leading). +templ mbTextComponent(text string, class string) { +
+ @templ.Raw(text) +
+} diff --git a/text_override_templ.go b/text_override_templ.go new file mode 100644 index 0000000..f868bf2 --- /dev/null +++ b/text_override_templ.go @@ -0,0 +1,67 @@ +// 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" + +// mbTextComponent renders a text run in Magazine Bold body cadence (Inter, 16–18px tight leading). +func mbTextComponent(text string, class 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) + var templ_7745c5c3_Var2 = []any{"mb-body font-sans", class} + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(text).Render(ctx, templ_7745c5c3_Buffer) + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate