initial: theme plugin brutalist

Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously
an unversioned directory inside ~/src/blockninja-themes/brutalist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-06-06 14:11:21 +08:00
commit 771a286fa9
61 changed files with 4506 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*.so
*.test
tmp/
.idea/
.vscode/

183
BUILD_REPORT.md Normal file
View File

@ -0,0 +1,183 @@
# Brutalist — Build Report
Theme slug: `brutalist`
Module path: `git.dev.alexdunmow.com/block/themes/brutalist`
Tech: templ-style (gotham-pattern), per spec §11.
Implementation pass: wave-1 (fonts policy applies — `fonts.json = []`).
## What landed
### Module + metadata
- `plugin.mod` per spec §2 verbatim: `kind = "theme"`, `scope = "@themes"`, `version = "0.1.0"`, `categories = ["templates"]`, tags array exactly the 8 spec values, `[compatibility] block_core = ">=0.11.0 <0.12.0"`.
- `go.mod` pinned to `git.dev.alexdunmow.com/block/core v0.11.1`, `go 1.26.4`, no `replace` directives.
- `Makefile` default target builds `brutalist.so` via `CGO_ENABLED=1 go build -buildmode=plugin`. No `rebuild` target (per agent scope; deploy is explicit).
### System / page templates / blocks
- System template registered: `brutalist` ("Brutalist", spec description verbatim).
- 4 page templates registered with exact spec slot arrays:
- `default``{header, main, footer}``RenderBrutalist`
- `landing` ("Index Sheet") → `{hero, ledger, footer}``RenderBrutalistLanding`
- `article` ("Case Study") → `{header, meta, main, footer}``RenderBrutalistArticle`
- `full-width` ("Full Bleed") → `{header, main, footer}``RenderBrutalistFullWidth`
- 7 theme blocks registered (all `Source: "brutalist"`):
- `masthead`, `project_ledger`, `concrete_hero`, `meta_strip`, `caption_image`, `pull_quote`, `colophon`
- 4 built-in block overrides via `RegisterTemplateOverride("brutalist", ...)`:
- `heading`, `text`, `button`, `image` — spec §9 treatments (square corners, font-display, mono code, figure wrapping).
- Email wrapper registered via `tr.RegisterEmailWrapper("brutalist", BrutalistEmailWrapper)`.
### Master pages
3 master pages in `DefaultMasterPages()` per spec §7 row-by-row:
- `brutalist:default-master` → templates `[default, article]`, blocks navbar/masthead/slot(main)/colophon(showAddress=true)
- `brutalist:index-master` → template `[landing]`, blocks masthead(hero)/slot(ledger)/colophon(showAddress=true)
- `brutalist:fullbleed-master` → template `[full-width]`, blocks navbar/slot(main)/colophon(showAddress=false)
### Schemas
7 draft-07 schemas in `schemas/<key>.schema.json`. `x-editor` values used: `text`, `richtext`, `media`, `select`, `textarea`, `collection`, `link`. All inside the allowed set per CLAUDE.md.
- `br.LoadSchemasFromFS(Schemas())` is called BEFORE any `br.Register(...)` in `register.go` — verified by reading top-to-bottom.
### Presets
`presets.json` ships two presets:
- `concrete-red` (mode `both`) with `lightColors` + `darkColors`, all 19 tokens each, HSL triple strings only.
- `hazard-yellow` (mode `dark`) with both `darkColors` and a mirror `lightColors` for editor preview, all 19 tokens each.
- Spot values from spec verbatim: `concrete-red.lightColors.background = "40 14% 93%"`, `concrete-red.lightColors.primary = "4 86% 48%"`, `hazard-yellow.darkColors.primary = "48 100% 50%"`, `hazard-yellow.darkColors.background = "0 0% 6%"`.
- check-safety presets check (Check 21): OK.
### CSS
`CSSManifest.InputCSSAppend` ships utility CSS via `ThemeCSSManifest()`:
- `:where(.brutalist-page, .brutalist-page *) { border-radius: 0 !important; }` (kills rounding theme-wide).
- `.brutalist-display`, `.brutalist-mono` font utilities flowing through `var(--font-heading|body|mono, <fallback-stack>)`.
- Hairline / thick rule utilities (`.brutalist-rule-ink`, `.brutalist-rule-thick`, `.brutalist-divider`).
- 12-column grid scaffolding (`.brutalist-grid-12`, `.brutalist-col-span-8`).
- Block-specific styles for `.brutalist-masthead`, `.brutalist-hero`, `.brutalist-ledger-row`, `.brutalist-meta-strip`, `.brutalist-pullquote`, `.brutalist-figure figcaption`, `.brutalist-colophon`.
- Square buttons + hover-invert-to-accent (spec §9 button override surface).
- Togglable `body.brutalist-grid-debug` 12-col overlay (UAT §13.11).
- All color references go through `hsl(var(--token))`; no hex/rgb/named colors anywhere in `*.go`, `*.templ`, or the appended CSS (Check 6 OK).
### Fonts (wave-1 policy)
- `fonts.json = []` (per FONTS.md override of spec §5 / UAT §11).
- `assets/` contains only a `.gitkeep`; no woff2 bundled.
- All `font-family` declarations in templates and CSS go through `var(--font-heading|body|mono, <fallback>)` with fallback stacks derived from the spec typography list (Space Grotesk → Inter Tight → Helvetica; Inter → -apple-system → Segoe UI; JetBrains Mono → IBM Plex Mono → ui-monospace).
- `RECOMMENDED_FONTS.md` written at the theme root listing the spec §5 fonts as Google Fonts picker recommendations with admin instructions.
## Build output
```
$ make
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o brutalist.so .
$ ls -la brutalist.so
-rw-rw-r-- 1 alex alex 21522624 brutalist.so
```
- File: `brutalist.so` (~20.5 MB; gotham reference `.so` is 21.1 MB, lcars is similar).
- No stderr warnings.
- `templ generate` produced 13 `_templ.go` files (committed alongside `.templ` sources).
## Safety check
Run from the actual check-safety location (the path in the spec's instructions points at `~/src/blockninja/backend/cmd/check-safety` but the real tool lives at `~/src/blockninja/check-safety/`).
```
$ cd /home/alex/src/blockninja/check-safety
$ go run . /home/alex/src/blockninja/themes/brutalist --plugin-dir /home/alex/src/blockninja/themes/brutalist
... (all 22 checks)
exit=0
```
Key results:
- **Check 2c** (Standalone plugin SDK import boundaries): OK — pinned to v0.11.1, no `replace` directives.
- **Check 3** (Go lint pipeline): OK after dropping unused `brutalistAccent`, `brutalistAccentFg`, `brutalistEmailCTAStyle`, and `renderPage` helpers.
- **Check 6** (No hardcoded colors in .templ): OK — all color references use `hsl(var(--token))`.
- **Check 11** (No placeholder code): OK.
- **Check 17** (No TODO markers): OK.
- **Check 21** (presets.json validation): OK.
- **Check 22** (No hand-rolled HTML sanitization): OK.
Warnings (non-fatal):
- **Check 2e** (`any` usage): 30 warnings, all from the required SDK signature `func(ctx context.Context, content map[string]any) string` and `MasterPageBlock.Content map[string]any`. Cannot be removed without breaking the SDK contract; same surface gotham exposes.
Overall: **exit 0, plugin builds, safety passes.**
## Open items / deferred
These items are explicitly deferred per the agent's hard scope rules ("local-build only, no woff2 bundling, no live deploy, no screenshots") and the FONTS.md wave-1 policy:
1. **Bundled woff2 files** — wave-2 follow-up. Currently `fonts.json = []`; `RECOMMENDED_FONTS.md` covers the admin path until Space Grotesk, Inter, and JetBrains Mono are bundled.
2. **`LICENSES.md`** — not written this pass (FONTS.md §"Wave-1 implementation policy" explicitly says "No `LICENSES.md` needed in this pass").
3. **Marketplace screenshots (spec §13.1)** — 6 frames at 1440×900 deferred until a running CMS instance is available; agent scope says "no `make rebuild`".
4. **Demo-content seed (spec §13.2)** — not implemented in this pass.
5. **`make rebuild` against running CMS** — UAT §2 "instance-brutalist Up within 15s" check cannot run from this agent's scope; needs a live instance.
6. **UAT §6, §7, §13 runtime checks** — accessibility, responsive, and aesthetic gates require a running container and visual verification; out of scope for the build agent.
7. **Email wrapper Litmus/cross-client verification (UAT §10)** — wrapper compiles and registers; live capture testing deferred.
8. **`concrete_hero.media` video support** (spec open question) — schema is `media` (image only) per the spec, deferred to v0.2 if the media type needs to widen.
## File inventory
```
brutalist/
├── BUILD_REPORT.md
├── Makefile
├── RECOMMENDED_FONTS.md
├── assets/
│ └── .gitkeep
├── brutalist.so
├── button_override.go
├── button_override.templ
├── button_override_templ.go
├── caption_image.go
├── caption_image.templ
├── caption_image_templ.go
├── colophon.go
├── colophon.templ
├── colophon_templ.go
├── concrete_hero.go
├── concrete_hero.templ
├── concrete_hero_templ.go
├── css_append.go
├── email_wrapper.go
├── email_wrapper.templ
├── email_wrapper_templ.go
├── embed.go
├── fonts.json
├── go.mod
├── go.sum
├── heading_override.go
├── heading_override.templ
├── heading_override_templ.go
├── helpers.go
├── image_override.go
├── image_override.templ
├── image_override_templ.go
├── masthead.go
├── masthead.templ
├── masthead_templ.go
├── meta_strip.go
├── meta_strip.templ
├── meta_strip_templ.go
├── page_data.go
├── plugin.mod
├── presets.json
├── project_ledger.go
├── project_ledger.templ
├── project_ledger_templ.go
├── pull_quote.go
├── pull_quote.templ
├── pull_quote_templ.go
├── register.go
├── registration.go
├── schemas/
│ ├── caption_image.schema.json
│ ├── colophon.schema.json
│ ├── concrete_hero.schema.json
│ ├── masthead.schema.json
│ ├── meta_strip.schema.json
│ ├── project_ledger.schema.json
│ └── pull_quote.schema.json
├── template.templ
├── template_templ.go
├── text_override.go
├── text_override.templ
└── text_override_templ.go
```
7 blocks, 7 schemas, 4 page templates, 4 built-in overrides, 1 email wrapper, 3 master pages, 2 presets. brutalist.so 21.5 MB. Safety check exit 0.

29
Makefile Normal file
View File

@ -0,0 +1,29 @@
# Brutalist — local build helpers (.so plugin)
#
# Local single-shot build only. Does NOT deploy to a running CMS container.
# Use `make` (default) to build brutalist.so via CGO go build -buildmode=plugin.
.PHONY: all clean templ help
PLUGIN_NAME := brutalist
# Default target: build the .so locally.
all: $(PLUGIN_NAME).so
# Local plugin build (no container). Useful for CI / quick checks.
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod presets.json fonts.json $(wildcard schemas/*.json)
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
# Regenerate templ Go files locally.
templ:
templ generate
# Remove built artefacts.
clean:
rm -f $(PLUGIN_NAME).so
help:
@echo "Targets:"
@echo " all Build $(PLUGIN_NAME).so locally (default)"
@echo " templ Regenerate templ-generated Go files"
@echo " clean Remove built .so"

33
RECOMMENDED_FONTS.md Normal file
View File

@ -0,0 +1,33 @@
# Recommended Fonts — Brutalist
`fonts.json` ships empty (`[]`) per the wave-1 policy in
`/home/alex/src/blockninja/themes/docs/FONTS.md`. No woff2 files are bundled in
this build pass; the theme uses CSS variable fallback stacks until an admin
assigns fonts in the typography panel.
All three roles below are available in the Google Fonts tab of the typography
picker. Open the typography panel, pick from the **Google Fonts** tab, and
assign:
| Role | Source | Family | Notes |
|---|---|---|---|
| Heading | `google:Space Grotesk` | Space Grotesk | Display / wordmark face. The whole point of the theme is that it lives at 200pt+. Bold (700) is what the templates default to. |
| Body | `google:Inter` | Inter | 1618px body copy with generous tracking. Weights 400 and 600. (Inter Tight is an acceptable swap if you prefer tighter setting.) |
| Mono | `google:JetBrains Mono` | JetBrains Mono | 11px mono captions, figure numbers, timestamps, metadata. Weights 400 and 700. |
## CSS fallback chain (already in the theme)
Until those assignments land, every `font-family` declaration in the theme
flows through CSS custom properties with the following fallback stacks:
```css
var(--font-heading, "Space Grotesk", "Inter Tight", "Helvetica Neue", Helvetica, Arial, sans-serif)
var(--font-body, "Inter", "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif)
var(--font-mono, "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)
```
## Wave-2 follow-up (out of scope)
The wider design system roadmap calls for shipping a custom display face for
Brutalist. When that lands, replace this doc with a `LICENSES.md` and add the
woff2 family to `fonts.json` per the schema in `docs/FONTS.md`.

0
assets/.gitkeep Normal file
View File

20
button_override.go Normal file
View File

@ -0,0 +1,20 @@
package main
import (
"bytes"
"context"
)
// BrutalistButtonBlock applies square corners, 1px solid ink border,
// hover inverts to accent fill — per the spec block-overrides section.
func BrutalistButtonBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
href := getString(content, "href")
if href == "" {
href = getString(content, "url")
}
var buf bytes.Buffer
_ = brutalistButtonComponent(text, resolveURL(href)).Render(ctx, &buf)
return buf.String()
}

12
button_override.templ Normal file
View File

@ -0,0 +1,12 @@
package main
templ brutalistButtonComponent(text, href string) {
<a
href={ templ.SafeURL(href) }
class="brutalist-btn"
data-brutalist-btn
style="display: inline-flex; align-items: center; padding: 0.75rem 1.5rem; font-family: var(--font-mono, 'JetBrains Mono', ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; background-color: transparent; color: hsl(var(--foreground)); border: 1px solid hsl(var(--border)); border-radius: 0; text-decoration: none;"
>
{ text }
</a>
}

66
button_override_templ.go Normal file
View File

@ -0,0 +1,66 @@
// 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"
func brutalistButtonComponent(text, href string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 5, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"brutalist-btn\" data-brutalist-btn style=\"display: inline-flex; align-items: center; padding: 0.75rem 1.5rem; font-family: var(--font-mono, 'JetBrains Mono', ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; background-color: transparent; color: hsl(var(--foreground)); border: 1px solid hsl(var(--border)); border-radius: 0; text-decoration: none;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 10, Col: 8}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

44
caption_image.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// CaptionImageBlockMeta defines the Captioned Image block.
var CaptionImageBlockMeta = blocks.BlockMeta{
Key: "caption_image",
Title: "Captioned Image",
Description: "Image with 11px mono caption in the left gutter",
Category: blocks.CategoryContent,
Source: "brutalist",
}
// CaptionImageData carries data for the caption-image component.
type CaptionImageData struct {
ImageURL string
Caption string
FigureNumber string
}
// CaptionImageBlock renders the captioned image.
// Content: {"image": "media:<id>", "caption": "...", "figureNumber": "Fig. 01"}
func CaptionImageBlock(ctx context.Context, content map[string]any) string {
img := getString(content, "image")
resolvedImage := ""
if img != "" {
resolvedImage = blocks.ResolveMediaPath(img)
}
data := CaptionImageData{
ImageURL: resolvedImage,
Caption: getString(content, "caption"),
FigureNumber: getString(content, "figureNumber"),
}
var buf bytes.Buffer
_ = captionImageComponent(data).Render(ctx, &buf)
return buf.String()
}

19
caption_image.templ Normal file
View File

@ -0,0 +1,19 @@
package main
templ captionImageComponent(data CaptionImageData) {
<figure class="brutalist-figure" data-block="brutalist:caption_image" style="margin: 2rem 0;">
if data.ImageURL != "" {
<img src={ data.ImageURL } alt={ data.Caption } style="display: block; width: 100%; height: auto;"/>
}
if data.Caption != "" || data.FigureNumber != "" {
<figcaption>
if data.FigureNumber != "" {
<span style="margin-right: 0.5rem;">{ data.FigureNumber }</span>
}
if data.Caption != "" {
<span>{ data.Caption }</span>
}
</figcaption>
}
</figure>
}

124
caption_image_templ.go Normal file
View File

@ -0,0 +1,124 @@
// 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"
func captionImageComponent(data CaptionImageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<figure class=\"brutalist-figure\" data-block=\"brutalist:caption_image\" style=\"margin: 2rem 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ImageURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.ImageURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `caption_image.templ`, Line: 6, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Caption)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `caption_image.templ`, Line: 6, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" style=\"display: block; width: 100%; height: auto;\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Caption != "" || data.FigureNumber != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.FigureNumber != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span style=\"margin-right: 0.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.FigureNumber)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `caption_image.templ`, Line: 11, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.Caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Caption)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `caption_image.templ`, Line: 14, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

58
colophon.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"bytes"
"context"
"time"
"git.dev.alexdunmow.com/block/core/blocks"
)
// ColophonBlockMeta defines the Colophon Footer block.
var ColophonBlockMeta = blocks.BlockMeta{
Key: "colophon",
Title: "Colophon Footer",
Description: "Three-row mono footer with copyright stamp",
Category: blocks.CategoryLayout,
Source: "brutalist",
}
// ColophonSocialLink represents a single social link.
type ColophonSocialLink struct {
Label string
URL string
}
// ColophonData carries data for the colophon component.
type ColophonData struct {
Address string
Email string
ShowAddress bool
Social []ColophonSocialLink
Year string
}
// ColophonBlock renders the colophon footer.
// Content: {"address": "...", "email": "...", "showAddress": "true", "social": [{"label":"...","url":"..."}]}
func ColophonBlock(ctx context.Context, content map[string]any) string {
raw := getSlice(content, "social")
social := make([]ColophonSocialLink, 0, len(raw))
for _, item := range raw {
social = append(social, ColophonSocialLink{
Label: getString(item, "label"),
URL: getString(item, "url"),
})
}
data := ColophonData{
Address: getString(content, "address"),
Email: getString(content, "email"),
ShowAddress: getBool(content, "showAddress", true),
Social: social,
Year: time.Now().UTC().Format("2006"),
}
var buf bytes.Buffer
_ = colophonComponent(data).Render(ctx, &buf)
return buf.String()
}

18
colophon.templ Normal file
View File

@ -0,0 +1,18 @@
package main
templ colophonComponent(data ColophonData) {
<section class="brutalist-colophon" data-block="brutalist:colophon">
if data.ShowAddress && data.Address != "" {
<address style="font-style: normal; white-space: pre-line;">{ data.Address }</address>
}
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
if data.Email != "" {
<a href={ templ.SafeURL("mailto:" + data.Email) } style="color: inherit; text-decoration: none;">{ data.Email }</a>
}
for _, link := range data.Social {
<a href={ templ.SafeURL(resolveURL(link.URL)) } target="_blank" rel="noopener" style="color: inherit; text-decoration: none;">{ link.Label }</a>
}
</div>
<div class="stamp">© { data.Year }</div>
</section>
}

144
colophon_templ.go Normal file
View File

@ -0,0 +1,144 @@
// 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"
func colophonComponent(data ColophonData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"brutalist-colophon\" data-block=\"brutalist:colophon\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowAddress && data.Address != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<address style=\"font-style: normal; white-space: pre-line;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Address)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 6, Col: 77}
}
_, 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, "</address>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display: flex; flex-direction: column; gap: 0.25rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Email != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("mailto:" + data.Email))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 10, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" style=\"color: inherit; text-decoration: none;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 10, Col: 113}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
for _, link := range data.Social {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(resolveURL(link.URL)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 13, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" target=\"_blank\" rel=\"noopener\" style=\"color: inherit; text-decoration: none;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(link.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 13, Col: 142}
}
_, 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, 10, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div><div class=\"stamp\">© ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.Year)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 16, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

44
concrete_hero.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// ConcreteHeroBlockMeta defines the Concrete Hero block.
var ConcreteHeroBlockMeta = blocks.BlockMeta{
Key: "concrete_hero",
Title: "Concrete Hero",
Description: "200pt+ display headline with optional eyebrow and full-bleed image",
Category: blocks.CategoryLayout,
Source: "brutalist",
}
// ConcreteHeroData carries data for the concrete hero component.
type ConcreteHeroData struct {
Headline string
Eyebrow string
MediaURL string
}
// ConcreteHeroBlock renders the concrete hero.
// Content: {"headline": "...", "eyebrow": "...", "media": "media:<id>"}
func ConcreteHeroBlock(ctx context.Context, content map[string]any) string {
media := getString(content, "media")
resolvedMedia := ""
if media != "" {
resolvedMedia = blocks.ResolveMediaPath(media)
}
data := ConcreteHeroData{
Headline: getString(content, "headline"),
Eyebrow: getString(content, "eyebrow"),
MediaURL: resolvedMedia,
}
var buf bytes.Buffer
_ = concreteHeroComponent(data).Render(ctx, &buf)
return buf.String()
}

24
concrete_hero.templ Normal file
View File

@ -0,0 +1,24 @@
package main
import "fmt"
templ concreteHeroComponent(data ConcreteHeroData) {
<section class="brutalist-hero" data-block="brutalist:concrete_hero" data-has-media={ heroHasMedia(data) }>
if data.MediaURL != "" {
<div class="media-bg" style={ fmt.Sprintf("background-image: url('%s');", data.MediaURL) } role="presentation"></div>
}
<div class="inner">
if data.Eyebrow != "" {
<p class="eyebrow">{ data.Eyebrow }</p>
}
<h1 class="headline">{ data.Headline }</h1>
</div>
</section>
}
func heroHasMedia(data ConcreteHeroData) string {
if data.MediaURL != "" {
return "true"
}
return "false"
}

121
concrete_hero_templ.go Normal file
View File

@ -0,0 +1,121 @@
// 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"
func concreteHeroComponent(data ConcreteHeroData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"brutalist-hero\" data-block=\"brutalist:concrete_hero\" data-has-media=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(heroHasMedia(data))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `concrete_hero.templ`, Line: 6, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.MediaURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"media-bg\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-image: url('%s');", data.MediaURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `concrete_hero.templ`, Line: 8, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" role=\"presentation\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"inner\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Eyebrow != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p class=\"eyebrow\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Eyebrow)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `concrete_hero.templ`, Line: 12, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<h1 class=\"headline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Headline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `concrete_hero.templ`, Line: 14, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h1></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func heroHasMedia(data ConcreteHeroData) string {
if data.MediaURL != "" {
return "true"
}
return "false"
}
var _ = templruntime.GeneratedTemplate

371
css_append.go Normal file
View File

@ -0,0 +1,371 @@
package main
// brutalistAppendCSS is appended to the host Tailwind input via CSSManifest.
// It declares font fallback stacks for the three font-* CSS variables, kills
// border radii on the standard interactive elements, defines hairline rule
// utilities, and ships a togglable 12-column debug grid overlay.
//
// All color references use the shadcn token pattern: hsl(var(--token)).
// No hex / rgb / named colors. Fonts go through var(--font-*) with fallbacks
// derived from the spec's typography list (Space Grotesk display, Inter body,
// JetBrains Mono mono). Per FONTS.md, no @font-face is emitted here.
const brutalistAppendCSS = `
/* === Brutalist theme utilities === */
:where(.brutalist-page, .brutalist-page *) {
border-radius: 0 !important;
}
.brutalist-page {
font-family: var(--font-body, "Inter", "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif);
color: hsl(var(--foreground));
background-color: hsl(var(--background));
letter-spacing: 0.005em;
}
.brutalist-display {
font-family: var(--font-heading, "Space Grotesk", "Inter Tight", "Helvetica Neue", Helvetica, Arial, sans-serif);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 0.9;
text-transform: uppercase;
}
.brutalist-mono {
font-family: var(--font-mono, "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
letter-spacing: 0.04em;
text-transform: uppercase;
}
/* --- Hairline rules and solid slabs --- */
.brutalist-rule-ink {
border-color: hsl(var(--border));
border-style: solid;
border-width: 1px;
}
.brutalist-rule-thick {
border-color: hsl(var(--border));
border-style: solid;
border-width: 4px;
}
.brutalist-divider {
border-top: 1px solid hsl(var(--border));
}
/* --- 12-column grid scaffolding --- */
.brutalist-grid-12 {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
column-gap: 0;
row-gap: 0;
}
.brutalist-col-span-8 {
grid-column: span 8 / span 8;
}
/* Project ledger row: 12-col grid with mono leading number. */
.brutalist-ledger-row {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
align-items: baseline;
padding: 1.25rem 0;
border-bottom: 1px solid hsl(var(--border));
}
.brutalist-ledger-row > * {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.875rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.brutalist-ledger-row .ledger-no { grid-column: span 1; }
.brutalist-ledger-row .ledger-year { grid-column: span 2; }
.brutalist-ledger-row .ledger-client { grid-column: span 5; font-family: var(--font-heading, "Space Grotesk", sans-serif); font-size: 1.125rem; text-transform: none; letter-spacing: -0.005em; }
.brutalist-ledger-row .ledger-role { grid-column: span 3; }
.brutalist-ledger-row .ledger-arrow { grid-column: span 1; text-align: right; }
@media (max-width: 768px) {
.brutalist-ledger-row {
grid-template-columns: 1fr;
row-gap: 0.25rem;
padding: 1rem 0;
}
.brutalist-ledger-row .ledger-no,
.brutalist-ledger-row .ledger-year,
.brutalist-ledger-row .ledger-client,
.brutalist-ledger-row .ledger-role,
.brutalist-ledger-row .ledger-arrow { grid-column: 1 / -1; text-align: left; }
}
/* --- Caption block --- */
.brutalist-figure figcaption {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 11px;
line-height: 1.4;
letter-spacing: 0.08em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
padding-top: 0.5rem;
}
/* --- Pull quote --- */
.brutalist-pullquote {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
padding: 3rem 0;
border-top: 1px solid hsl(var(--border));
border-bottom: 1px solid hsl(var(--border));
}
.brutalist-pullquote blockquote {
grid-column: 3 / span 8;
font-family: var(--font-heading, "Space Grotesk", sans-serif);
font-weight: 700;
font-size: clamp(1.75rem, 5vw, 3.5rem);
line-height: 1;
letter-spacing: -0.02em;
color: hsl(var(--foreground));
}
.brutalist-pullquote cite {
grid-column: 3 / span 8;
margin-top: 1.5rem;
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
font-style: normal;
color: hsl(var(--muted-foreground));
}
@media (max-width: 768px) {
.brutalist-pullquote blockquote,
.brutalist-pullquote cite { grid-column: 1 / -1; }
}
/* --- Meta strip --- */
.brutalist-meta-strip {
display: flex;
flex-wrap: wrap;
gap: 2rem;
padding: 1rem 0;
border-top: 1px solid hsl(var(--border));
border-bottom: 1px solid hsl(var(--border));
}
.brutalist-meta-strip dt {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
}
.brutalist-meta-strip dd {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.75rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: hsl(var(--foreground));
margin: 0;
}
/* --- Buttons: square, 1px ink border, hover inverts to accent --- */
.brutalist-page button,
.brutalist-page .brutalist-btn,
.brutalist-page [data-brutalist-btn] {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
background-color: transparent;
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
border-radius: 0;
cursor: pointer;
transition: background-color 80ms linear, color 80ms linear;
}
.brutalist-page button:hover,
.brutalist-page .brutalist-btn:hover,
.brutalist-page [data-brutalist-btn]:hover {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.brutalist-page button:focus-visible,
.brutalist-page .brutalist-btn:focus-visible,
.brutalist-page [data-brutalist-btn]:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Inputs: square --- */
.brutalist-page input,
.brutalist-page select,
.brutalist-page textarea {
border-radius: 0;
border: 1px solid hsl(var(--input));
background-color: transparent;
color: hsl(var(--foreground));
padding: 0.5rem 0.75rem;
}
.brutalist-page input:focus,
.brutalist-page select:focus,
.brutalist-page textarea:focus {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Masthead --- */
.brutalist-masthead {
position: relative;
padding: 4rem 0 2rem;
}
.brutalist-masthead .studio-name {
font-family: var(--font-heading, "Space Grotesk", sans-serif);
font-weight: 700;
font-size: clamp(3rem, 14vw, 16rem);
line-height: 0.85;
letter-spacing: -0.03em;
text-transform: uppercase;
color: hsl(var(--foreground));
margin: 0;
}
.brutalist-masthead .tagline {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
margin-top: 1rem;
}
.brutalist-masthead .index-counter {
position: absolute;
top: 1.5rem;
right: 0;
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
}
/* --- Concrete hero --- */
.brutalist-hero {
position: relative;
padding: 6rem 0 4rem;
overflow: hidden;
}
.brutalist-hero .eyebrow {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.75rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
margin-bottom: 1.5rem;
}
.brutalist-hero .headline {
font-family: var(--font-heading, "Space Grotesk", sans-serif);
font-weight: 700;
font-size: clamp(3rem, 22vw, 18rem);
line-height: 0.85;
letter-spacing: -0.04em;
text-transform: uppercase;
color: hsl(var(--foreground));
margin: 0;
}
.brutalist-hero[data-has-media="true"] .headline {
color: hsl(var(--background));
mix-blend-mode: difference;
}
.brutalist-hero .media-bg {
position: absolute;
inset: 0;
z-index: 0;
background-size: cover;
background-position: center;
}
.brutalist-hero .media-bg::after {
content: "";
position: absolute;
inset: 0;
background-color: hsl(var(--background) / 0.4);
}
.brutalist-hero .inner {
position: relative;
z-index: 1;
}
/* --- Colophon --- */
.brutalist-colophon {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 2rem;
padding: 3rem 0 1.5rem;
border-top: 4px solid hsl(var(--border));
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
}
.brutalist-colophon .stamp {
text-align: right;
color: hsl(var(--foreground));
}
@media (max-width: 768px) {
.brutalist-colophon {
grid-template-columns: 1fr;
}
.brutalist-colophon .stamp { text-align: left; }
}
/* --- Grid overlay debug --- */
body.brutalist-grid-debug::before {
content: "";
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
background-image: repeating-linear-gradient(
to right,
hsl(var(--ring) / 0.18) 0,
hsl(var(--ring) / 0.18) 1px,
transparent 1px,
transparent calc(100% / 12)
);
}
`

91
email_wrapper.go Normal file
View File

@ -0,0 +1,91 @@
package main
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// BrutalistEmailWrapper renders the Brutalist branded email wrapper:
// off-white concrete background, 1px ink outer border, Space Grotesk
// headline, Inter body, JetBrains Mono signoff.
//
// Colors flow from emailCtx.Colors when present, falling back to HSL
// values derived from the spec's concrete-red light palette. The hot
// accent is reserved for the single CTA button.
func BrutalistEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
_ = brutalistEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// Spec defaults for email rendering (matches concrete-red light preset HSL
// triples converted to hex-equivalent so email clients can compute colors).
const (
brutalistEmailBgDefault = "#F2EFE8" // background 40 14% 93%
brutalistEmailCardDefault = "#F7F4ED" // card 40 10% 96%
brutalistEmailFgDefault = "#0A0A0A" // foreground 0 0% 4%
brutalistEmailMutedFgDefault = "#595959" // mutedForeground 0 0% 35%
brutalistEmailBorderDefault = "#0A0A0A" // border 0 0% 4%
)
func brutalistEmailColor(v, fallback string) string {
if v != "" {
return v
}
return fallback
}
func brutalistBg(emailCtx templates.EmailContext) string {
return brutalistEmailColor(emailCtx.Colors.Background, brutalistEmailBgDefault)
}
func brutalistCard(emailCtx templates.EmailContext) string {
return brutalistEmailColor(emailCtx.Colors.Card, brutalistEmailCardDefault)
}
func brutalistFg(emailCtx templates.EmailContext) string {
return brutalistEmailColor(emailCtx.Colors.Foreground, brutalistEmailFgDefault)
}
func brutalistMutedFg(emailCtx templates.EmailContext) string {
return brutalistEmailColor(emailCtx.Colors.MutedForeground, brutalistEmailMutedFgDefault)
}
func brutalistBorder(emailCtx templates.EmailContext) string {
return brutalistEmailColor(emailCtx.Colors.Border, brutalistEmailBorderDefault)
}
func brutalistEmailBodyStyle(emailCtx templates.EmailContext) string {
return fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; color: %s;", brutalistBg(emailCtx), brutalistFg(emailCtx))
}
func brutalistEmailOuterCellStyle(emailCtx templates.EmailContext) string {
return fmt.Sprintf("padding: 48px 16px; background-color: %s;", brutalistBg(emailCtx))
}
func brutalistEmailContainerStyle(emailCtx templates.EmailContext) string {
return fmt.Sprintf("max-width: 600px; background-color: %s; border: 1px solid %s; border-radius: 0;", brutalistCard(emailCtx), brutalistBorder(emailCtx))
}
func brutalistEmailHeaderStyle(emailCtx templates.EmailContext) string {
return fmt.Sprintf("padding: 32px 40px; border-bottom: 1px solid %s;", brutalistBorder(emailCtx))
}
func brutalistEmailHeadlineStyle(emailCtx templates.EmailContext) string {
return fmt.Sprintf("margin: 0; font-family: 'Space Grotesk', 'Inter Tight', Helvetica, Arial, sans-serif; font-weight: 700; font-size: 26px; letter-spacing: -0.02em; color: %s; text-transform: uppercase;", brutalistFg(emailCtx))
}
func brutalistEmailBodyCellStyle(emailCtx templates.EmailContext) string {
return fmt.Sprintf("padding: 40px 48px; color: %s; font-size: 16px; line-height: 1.7;", brutalistFg(emailCtx))
}
func brutalistEmailFooterStyle(emailCtx templates.EmailContext) string {
return fmt.Sprintf("padding: 24px 48px 32px; border-top: 1px solid %s; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: %s;", brutalistBorder(emailCtx), brutalistMutedFg(emailCtx))
}
func brutalistEmailLinkStyle(emailCtx templates.EmailContext) string {
return fmt.Sprintf("color: %s; text-decoration: none; border-bottom: 1px solid %s;", brutalistFg(emailCtx), brutalistBorder(emailCtx))
}

65
email_wrapper.templ Normal file
View File

@ -0,0 +1,65 @@
package main
import "git.dev.alexdunmow.com/block/core/templates"
templ brutalistEmailTemplate(emailCtx templates.EmailContext, body string) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>{ emailCtx.SiteSettings.SiteName }</title>
<style type="text/css">
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
@media only screen and (max-width: 620px) {
.brutalist-email-container { width: 100% !important; max-width: 100% !important; }
.brutalist-email-pad { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style={ brutalistEmailBodyStyle(emailCtx) }>
if emailCtx.PreviewText != "" {
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{ emailCtx.PreviewText }</div>
}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ brutalistEmailOuterCellStyle(emailCtx) }>
<table role="presentation" class="brutalist-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style={ brutalistEmailContainerStyle(emailCtx) }>
<tr>
<td align="left" class="brutalist-email-pad" style={ brutalistEmailHeaderStyle(emailCtx) }>
if emailCtx.SiteSettings.SiteName != "" {
<h1 style={ brutalistEmailHeadlineStyle(emailCtx) }>{ emailCtx.SiteSettings.SiteName }</h1>
}
</td>
</tr>
<tr>
<td class="brutalist-email-pad" style={ brutalistEmailBodyCellStyle(emailCtx) }>
@templ.Raw(body)
</td>
</tr>
<tr>
<td class="brutalist-email-pad" style={ brutalistEmailFooterStyle(emailCtx) }>
if emailCtx.SiteSettings.SiteURL != "" {
<div style="margin-bottom: 8px;">
<a href={ templ.SafeURL(emailCtx.SiteSettings.SiteURL) } style={ brutalistEmailLinkStyle(emailCtx) }>{ emailCtx.SiteSettings.SiteURL }</a>
</div>
}
if emailCtx.UnsubscribeURL != "" {
<div>
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ brutalistEmailLinkStyle(emailCtx) }>UNSUBSCRIBE</a>
</div>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}

281
email_wrapper_templ.go Normal file
View File

@ -0,0 +1,281 @@
// 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 "git.dev.alexdunmow.com/block/core/templates"
func brutalistEmailTemplate(emailCtx templates.EmailContext, body string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 13, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><style type=\"text/css\">\n\t\t\t\tbody, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }\n\t\t\t\ttable, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }\n\t\t\t\timg { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }\n\t\t\t\tbody { margin: 0 !important; padding: 0 !important; width: 100% !important; }\n\t\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t\t.brutalist-email-container { width: 100% !important; max-width: 100% !important; }\n\t\t\t\t\t.brutalist-email-pad { padding-left: 24px !important; padding-right: 24px !important; }\n\t\t\t\t}\n\t\t\t</style></head><body style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistEmailBodyStyle(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 25, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.PreviewText != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display: none; max-height: 0; overflow: hidden; mso-hide: all;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 27, Col: 102}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistEmailOuterCellStyle(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 31, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><table role=\"presentation\" class=\"brutalist-email-container\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistEmailContainerStyle(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 32, Col: 168}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><tr><td align=\"left\" class=\"brutalist-email-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistEmailHeaderStyle(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 34, Col: 96}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.SiteName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h1 style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistEmailHeadlineStyle(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 36, Col: 59}
}
_, 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, 11, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, 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: 36, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</td></tr><tr><td class=\"brutalist-email-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistEmailBodyCellStyle(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 41, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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, 15, "</td></tr><tr><td class=\"brutalist-email-pad\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistEmailFooterStyle(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 46, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.SiteURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div style=\"margin-bottom: 8px;\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 templ.SafeURL
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.SiteSettings.SiteURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 49, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistEmailLinkStyle(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 49, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 49, Col: 143}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 templ.SafeURL
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.UnsubscribeURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 54, Col: 59}
}
_, 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, 22, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistEmailLinkStyle(emailCtx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 54, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">UNSUBSCRIBE</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

55
embed.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"embed"
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/plugin"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
// Assets returns the embedded assets filesystem.
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "assets")
return sub
}
// Schemas returns the embedded schemas filesystem.
func Schemas() fs.FS {
sub, _ := fs.Sub(schemasFS, "schemas")
return sub
}
// AssetsHandler serves the embedded assets via HTTP.
func AssetsHandler() http.Handler {
return http.FileServer(http.FS(Assets()))
}
// ThemePresets returns the embedded preset JSON bytes.
func ThemePresets() []byte { return presetsData }
// BundledFonts returns the embedded fonts manifest JSON bytes.
func BundledFonts() []byte { return fontsData }
// ThemeCSSManifest returns the Brutalist CSS additions (hairlines, no-radius,
// grid overlay, font-family fallback stacks via CSS custom properties).
func ThemeCSSManifest() *plugin.CSSManifest {
return &plugin.CSSManifest{
InputCSSAppend: brutalistAppendCSS,
}
}

1
fonts.json Normal file
View File

@ -0,0 +1 @@
[]

20
go.mod Normal file
View File

@ -0,0 +1,20 @@
module git.dev.alexdunmow.com/block/themes/brutalist
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
)

42
go.sum Normal file
View File

@ -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=

39
heading_override.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"bytes"
"context"
"strconv"
)
// BrutalistHeadingBlock applies brutalist treatment to heading blocks: kills
// default rounding/leading, uses var(--font-heading), tracking-tight,
// uppercase on h1/h2.
func BrutalistHeadingBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
textClass := getString(content, "textClass")
level := parseBrutalistHeadingLevel(content)
var buf bytes.Buffer
_ = brutalistHeadingComponent(level, text, textClass).Render(ctx, &buf)
return buf.String()
}
func parseBrutalistHeadingLevel(content map[string]any) int {
if v, ok := content["level"].(float64); ok {
if l := int(v); l >= 1 && l <= 6 {
return l
}
}
if v, ok := content["level"].(int); ok {
if v >= 1 && v <= 6 {
return v
}
}
if v, ok := content["level"].(string); ok {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 6 {
return n
}
}
return 2
}

38
heading_override.templ Normal file
View File

@ -0,0 +1,38 @@
package main
func brutalistHeadingStyle(level int) string {
switch level {
case 1:
return "font-family: var(--font-heading, 'Space Grotesk', 'Inter Tight', sans-serif); font-weight: 700; font-size: clamp(2.5rem, 6vw, 5rem); line-height: 0.9; letter-spacing: -0.03em; text-transform: uppercase; color: hsl(var(--foreground)); margin: 0;"
case 2:
return "font-family: var(--font-heading, 'Space Grotesk', 'Inter Tight', sans-serif); font-weight: 700; font-size: clamp(1.875rem, 4vw, 3rem); line-height: 0.95; letter-spacing: -0.02em; text-transform: uppercase; color: hsl(var(--foreground)); margin: 0;"
case 3:
return "font-family: var(--font-heading, 'Space Grotesk', sans-serif); font-weight: 700; font-size: 1.5rem; line-height: 1; letter-spacing: -0.015em; color: hsl(var(--foreground)); margin: 0;"
case 4:
return "font-family: var(--font-heading, 'Space Grotesk', sans-serif); font-weight: 600; font-size: 1.25rem; line-height: 1.1; letter-spacing: -0.01em; color: hsl(var(--foreground)); margin: 0;"
case 5:
return "font-family: var(--font-heading, 'Space Grotesk', sans-serif); font-weight: 600; font-size: 1.125rem; line-height: 1.15; color: hsl(var(--foreground)); margin: 0;"
case 6:
return "font-family: var(--font-heading, 'Space Grotesk', sans-serif); font-weight: 600; font-size: 1rem; line-height: 1.2; color: hsl(var(--foreground)); margin: 0;"
}
return "font-family: var(--font-heading, 'Space Grotesk', sans-serif); font-weight: 700; color: hsl(var(--foreground)); margin: 0;"
}
templ brutalistHeadingComponent(level int, text, textClass string) {
switch level {
case 1:
<h1 class={ textClass } style={ brutalistHeadingStyle(1) }>{ text }</h1>
case 2:
<h2 class={ textClass } style={ brutalistHeadingStyle(2) }>{ text }</h2>
case 3:
<h3 class={ textClass } style={ brutalistHeadingStyle(3) }>{ text }</h3>
case 4:
<h4 class={ textClass } style={ brutalistHeadingStyle(4) }>{ text }</h4>
case 5:
<h5 class={ textClass } style={ brutalistHeadingStyle(5) }>{ text }</h5>
case 6:
<h6 class={ textClass } style={ brutalistHeadingStyle(6) }>{ text }</h6>
default:
<h2 class={ textClass } style={ brutalistHeadingStyle(2) }>{ text }</h2>
}
}

399
heading_override_templ.go Normal file
View File

@ -0,0 +1,399 @@
// 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"
func brutalistHeadingStyle(level int) string {
switch level {
case 1:
return "font-family: var(--font-heading, 'Space Grotesk', 'Inter Tight', sans-serif); font-weight: 700; font-size: clamp(2.5rem, 6vw, 5rem); line-height: 0.9; letter-spacing: -0.03em; text-transform: uppercase; color: hsl(var(--foreground)); margin: 0;"
case 2:
return "font-family: var(--font-heading, 'Space Grotesk', 'Inter Tight', sans-serif); font-weight: 700; font-size: clamp(1.875rem, 4vw, 3rem); line-height: 0.95; letter-spacing: -0.02em; text-transform: uppercase; color: hsl(var(--foreground)); margin: 0;"
case 3:
return "font-family: var(--font-heading, 'Space Grotesk', sans-serif); font-weight: 700; font-size: 1.5rem; line-height: 1; letter-spacing: -0.015em; color: hsl(var(--foreground)); margin: 0;"
case 4:
return "font-family: var(--font-heading, 'Space Grotesk', sans-serif); font-weight: 600; font-size: 1.25rem; line-height: 1.1; letter-spacing: -0.01em; color: hsl(var(--foreground)); margin: 0;"
case 5:
return "font-family: var(--font-heading, 'Space Grotesk', sans-serif); font-weight: 600; font-size: 1.125rem; line-height: 1.15; color: hsl(var(--foreground)); margin: 0;"
case 6:
return "font-family: var(--font-heading, 'Space Grotesk', sans-serif); font-weight: 600; font-size: 1rem; line-height: 1.2; color: hsl(var(--foreground)); margin: 0;"
}
return "font-family: var(--font-heading, 'Space Grotesk', sans-serif); font-weight: 700; color: hsl(var(--foreground)); margin: 0;"
}
func brutalistHeadingComponent(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{textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistHeadingStyle(1))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 24, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 24, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 2:
var templ_7745c5c3_Var6 = []any{textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var6).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistHeadingStyle(2))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 26, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 26, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 3:
var templ_7745c5c3_Var10 = []any{textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<h3 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var10).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistHeadingStyle(3))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 28, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 28, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 4:
var templ_7745c5c3_Var14 = []any{textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h4 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistHeadingStyle(4))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 30, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 30, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 5:
var templ_7745c5c3_Var18 = []any{textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<h5 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var18).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistHeadingStyle(5))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 32, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 32, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</h5>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 6:
var templ_7745c5c3_Var22 = []any{textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<h6 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var22).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistHeadingStyle(6))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 34, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 34, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</h6>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
var templ_7745c5c3_Var26 = []any{textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var26...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var26).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(brutalistHeadingStyle(2))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 36, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 36, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

51
helpers.go Normal file
View File

@ -0,0 +1,51 @@
package main
import "strings"
// getString extracts a string value from a content map.
func getString(content map[string]any, key string) string {
if v, ok := content[key].(string); ok {
return v
}
return ""
}
// getSlice extracts a slice of map[string]any from a content map.
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
}
// getBool extracts a boolean-ish value from a content map.
// Accepts native bool plus the string forms "true"/"false" emitted by select-style editors.
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 strings.ToLower(strings.TrimSpace(v)) {
case "true", "yes", "1":
return true
case "false", "no", "0":
return false
}
}
return defaultVal
}
// resolveURL returns a safe-ish href value, defaulting to "#" when blank.
func resolveURL(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return "#"
}
return s
}

25
image_override.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// BrutalistImageBlock wraps image content in a <figure> with a mono caption slot.
func BrutalistImageBlock(ctx context.Context, content map[string]any) string {
src := getString(content, "src")
if src == "" {
src = getString(content, "url")
}
if src != "" {
src = blocks.ResolveMediaPath(src)
}
alt := getString(content, "alt")
caption := getString(content, "caption")
var buf bytes.Buffer
_ = brutalistImageComponent(src, alt, caption).Render(ctx, &buf)
return buf.String()
}

12
image_override.templ Normal file
View File

@ -0,0 +1,12 @@
package main
templ brutalistImageComponent(src, alt, caption string) {
<figure class="brutalist-figure" style="margin: 2rem 0;">
if src != "" {
<img src={ src } alt={ alt } style="display: block; width: 100%; height: auto;"/>
}
if caption != "" {
<figcaption>{ caption }</figcaption>
}
</figure>
}

95
image_override_templ.go Normal file
View File

@ -0,0 +1,95 @@
// 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"
func brutalistImageComponent(src, alt, caption string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<figure class=\"brutalist-figure\" style=\"margin: 2rem 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if src != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(src)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 6, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(alt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 6, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" style=\"display: block; width: 100%; height: auto;\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(caption)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 9, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

44
masthead.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// MastheadBlockMeta defines the Studio Masthead block.
var MastheadBlockMeta = blocks.BlockMeta{
Key: "masthead",
Title: "Studio Masthead",
Description: "Oversized studio wordmark with mono index counter top-right",
Category: blocks.CategoryLayout,
Source: "brutalist",
}
// MastheadData carries display data for the masthead component.
type MastheadData struct {
StudioName string
Tagline string
IndexNumber string
}
// MastheadBlock renders the studio masthead.
// Content: {"studioName": "...", "tagline": "...", "indexNumber": "01 / 14"}
func MastheadBlock(ctx context.Context, content map[string]any) string {
data := MastheadData{
StudioName: getString(content, "studioName"),
Tagline: getString(content, "tagline"),
IndexNumber: getString(content, "indexNumber"),
}
if data.StudioName == "" {
data.StudioName = "STUDIO"
}
if data.IndexNumber == "" {
data.IndexNumber = "01 / 14"
}
var buf bytes.Buffer
_ = mastheadComponent(data).Render(ctx, &buf)
return buf.String()
}

11
masthead.templ Normal file
View File

@ -0,0 +1,11 @@
package main
templ mastheadComponent(data MastheadData) {
<section class="brutalist-masthead" data-block="brutalist:masthead">
<span class="index-counter brutalist-mono">{ data.IndexNumber }</span>
<h1 class="studio-name">{ data.StudioName }</h1>
if data.Tagline != "" {
<p class="tagline">{ data.Tagline }</p>
}
</section>
}

89
masthead_templ.go Normal file
View File

@ -0,0 +1,89 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
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, "<section class=\"brutalist-masthead\" data-block=\"brutalist:masthead\"><span class=\"index-counter brutalist-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.IndexNumber)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 5, Col: 63}
}
_, 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, "</span><h1 class=\"studio-name\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.StudioName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 6, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Tagline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<p class=\"tagline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Tagline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 8, Col: 36}
}
_, 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, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

45
meta_strip.go Normal file
View File

@ -0,0 +1,45 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// MetaStripBlockMeta defines the Metadata Strip block.
var MetaStripBlockMeta = blocks.BlockMeta{
Key: "meta_strip",
Title: "Metadata Strip",
Description: "Mono uppercase strip: CLIENT / YEAR / DISCIPLINE / LOCATION",
Category: blocks.CategoryContent,
Source: "brutalist",
}
// MetaStripItem is one label/value pair in the metadata strip.
type MetaStripItem struct {
Label string
Value string
}
// MetaStripData carries data for the metadata strip component.
type MetaStripData struct {
Items []MetaStripItem
}
// MetaStripBlock renders the metadata strip.
// Content: {"items": [{"label":"CLIENT","value":"..."}, ...]}
func MetaStripBlock(ctx context.Context, content map[string]any) string {
raw := getSlice(content, "items")
items := make([]MetaStripItem, 0, len(raw))
for _, item := range raw {
items = append(items, MetaStripItem{
Label: getString(item, "label"),
Value: getString(item, "value"),
})
}
var buf bytes.Buffer
_ = metaStripComponent(MetaStripData{Items: items}).Render(ctx, &buf)
return buf.String()
}

12
meta_strip.templ Normal file
View File

@ -0,0 +1,12 @@
package main
templ metaStripComponent(data MetaStripData) {
<dl class="brutalist-meta-strip" data-block="brutalist:meta_strip">
for _, item := range data.Items {
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
<dt>{ item.Label }</dt>
<dd>{ item.Value }</dd>
</div>
}
</dl>
}

76
meta_strip_templ.go Normal file
View File

@ -0,0 +1,76 @@
// 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"
func metaStripComponent(data MetaStripData) 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, "<dl class=\"brutalist-meta-strip\" data-block=\"brutalist:meta_strip\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range data.Items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div style=\"display: flex; flex-direction: column; gap: 0.25rem;\"><dt>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `meta_strip.templ`, Line: 7, Col: 20}
}
_, 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, "</dt><dd>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `meta_strip.templ`, Line: 8, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</dd></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</dl>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

69
page_data.go Normal file
View File

@ -0,0 +1,69 @@
package main
import (
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData carries the data passed into Brutalist page templates.
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 parseBrutalistPageData(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
}
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: bn.ParseSiteSettings(doc),
PageMeta: bn.ParsePageMeta(doc),
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: bn.ParseEngagementConfig(doc),
}
}

12
plugin.mod Normal file
View File

@ -0,0 +1,12 @@
[plugin]
name = "brutalist"
display_name = "Brutalist"
scope = "@themes"
version = "0.1.0"
description = "Concrete, oversized type, hard 12-col grid theme for design studios, architecture firms and art galleries."
kind = "theme"
categories = ["templates"]
tags = ["brutalist", "raw", "mono", "agency", "architecture", "gallery", "editorial", "minimal"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"

102
presets.json Normal file
View File

@ -0,0 +1,102 @@
[
{
"id": "concrete-red",
"name": "Concrete & Cadmium",
"description": "Off-white concrete with cadmium red accent. Light mode primary; dark mode for case-study deep-dives.",
"theme": {
"mode": "both",
"lightColors": {
"background": "40 14% 93%",
"foreground": "0 0% 4%",
"card": "40 10% 96%",
"cardForeground": "0 0% 4%",
"popover": "40 10% 96%",
"popoverForeground": "0 0% 4%",
"primary": "4 86% 48%",
"primaryForeground": "0 0% 100%",
"secondary": "40 8% 88%",
"secondaryForeground": "0 0% 4%",
"muted": "40 6% 90%",
"mutedForeground": "0 0% 35%",
"accent": "4 86% 48%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 50%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 4%",
"input": "0 0% 4%",
"ring": "4 86% 48%"
},
"darkColors": {
"background": "0 0% 6%",
"foreground": "40 14% 93%",
"card": "0 0% 9%",
"cardForeground": "40 14% 93%",
"popover": "0 0% 9%",
"popoverForeground": "40 14% 93%",
"primary": "4 86% 54%",
"primaryForeground": "0 0% 6%",
"secondary": "0 0% 14%",
"secondaryForeground": "40 14% 93%",
"muted": "0 0% 12%",
"mutedForeground": "40 6% 65%",
"accent": "4 86% 54%",
"accentForeground": "0 0% 6%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "40 14% 93%",
"input": "40 14% 93%",
"ring": "4 86% 54%"
}
}
},
{
"id": "hazard-yellow",
"name": "Hazard",
"description": "Construction-site safety yellow on ink black. Dark mode primary.",
"theme": {
"mode": "dark",
"lightColors": {
"background": "40 14% 93%",
"foreground": "0 0% 4%",
"card": "40 10% 96%",
"cardForeground": "0 0% 4%",
"popover": "40 10% 96%",
"popoverForeground": "0 0% 4%",
"primary": "48 100% 45%",
"primaryForeground": "0 0% 4%",
"secondary": "40 8% 88%",
"secondaryForeground": "0 0% 4%",
"muted": "40 6% 90%",
"mutedForeground": "0 0% 35%",
"accent": "48 100% 45%",
"accentForeground": "0 0% 4%",
"destructive": "0 84% 50%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 4%",
"input": "0 0% 4%",
"ring": "48 100% 45%"
},
"darkColors": {
"background": "0 0% 6%",
"foreground": "48 100% 88%",
"card": "0 0% 9%",
"cardForeground": "48 100% 88%",
"popover": "0 0% 9%",
"popoverForeground": "48 100% 88%",
"primary": "48 100% 50%",
"primaryForeground": "0 0% 6%",
"secondary": "0 0% 14%",
"secondaryForeground": "48 100% 88%",
"muted": "0 0% 12%",
"mutedForeground": "48 20% 65%",
"accent": "48 100% 50%",
"accentForeground": "0 0% 6%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "48 100% 88%",
"input": "48 100% 88%",
"ring": "48 100% 50%"
}
}
}
]

51
project_ledger.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// ProjectLedgerBlockMeta defines the Project Ledger block.
var ProjectLedgerBlockMeta = blocks.BlockMeta{
Key: "project_ledger",
Title: "Project Ledger",
Description: "Ordered project list on a 12-col grid: number / year / client / role / arrow",
Category: blocks.CategoryContent,
Source: "brutalist",
}
// ProjectLedgerRow is one row in the ledger.
type ProjectLedgerRow struct {
No string
Year string
Client string
Role string
Link string
}
// ProjectLedgerData carries data for the ledger component.
type ProjectLedgerData struct {
Rows []ProjectLedgerRow
}
// ProjectLedgerBlock renders the project ledger.
// Content: {"rows": [{"no":"01","year":"2024","client":"...","role":"...","link":"..."}]}
func ProjectLedgerBlock(ctx context.Context, content map[string]any) string {
raw := getSlice(content, "rows")
rows := make([]ProjectLedgerRow, 0, len(raw))
for _, item := range raw {
rows = append(rows, ProjectLedgerRow{
No: getString(item, "no"),
Year: getString(item, "year"),
Client: getString(item, "client"),
Role: getString(item, "role"),
Link: getString(item, "link"),
})
}
var buf bytes.Buffer
_ = projectLedgerComponent(ProjectLedgerData{Rows: rows}).Render(ctx, &buf)
return buf.String()
}

31
project_ledger.templ Normal file
View File

@ -0,0 +1,31 @@
package main
templ projectLedgerComponent(data ProjectLedgerData) {
<section data-block="brutalist:project_ledger">
if len(data.Rows) == 0 {
<p class="brutalist-mono" style="padding: 1rem 0; color: hsl(var(--muted-foreground));">No projects listed.</p>
} else {
<ol style="list-style: none; padding: 0; margin: 0;">
for _, row := range data.Rows {
<li class="brutalist-ledger-row">
<span class="ledger-no">{ row.No }</span>
<span class="ledger-year">{ row.Year }</span>
<span class="ledger-client">
if row.Link != "" {
<a href={ templ.SafeURL(resolveURL(row.Link)) } style="color: inherit; text-decoration: none;">{ row.Client }</a>
} else {
{ row.Client }
}
</span>
<span class="ledger-role">{ row.Role }</span>
<span class="ledger-arrow">
if row.Link != "" {
<a href={ templ.SafeURL(resolveURL(row.Link)) } aria-label="View project" style="color: inherit; text-decoration: none;">→</a>
}
</span>
</li>
}
</ol>
}
</section>
}

173
project_ledger_templ.go Normal file
View File

@ -0,0 +1,173 @@
// 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"
func projectLedgerComponent(data ProjectLedgerData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section data-block=\"brutalist:project_ledger\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Rows) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"brutalist-mono\" style=\"padding: 1rem 0; color: hsl(var(--muted-foreground));\">No projects listed.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<ol style=\"list-style: none; padding: 0; margin: 0;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, row := range data.Rows {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<li class=\"brutalist-ledger-row\"><span class=\"ledger-no\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(row.No)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `project_ledger.templ`, Line: 11, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span> <span class=\"ledger-year\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(row.Year)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `project_ledger.templ`, Line: 12, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span> <span class=\"ledger-client\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if row.Link != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(resolveURL(row.Link)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `project_ledger.templ`, Line: 15, Col: 53}
}
_, 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, "\" style=\"color: inherit; text-decoration: none;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(row.Client)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `project_ledger.templ`, Line: 15, Col: 115}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(row.Client)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `project_ledger.templ`, Line: 17, Col: 20}
}
_, 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, 10, "</span> <span class=\"ledger-role\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(row.Role)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `project_ledger.templ`, Line: 20, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span> <span class=\"ledger-arrow\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if row.Link != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(resolveURL(row.Link)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `project_ledger.templ`, Line: 23, Col: 53}
}
_, 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, 13, "\" aria-label=\"View project\" style=\"color: inherit; text-decoration: none;\">→</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</ol>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

36
pull_quote.go Normal file
View File

@ -0,0 +1,36 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// PullQuoteBlockMeta defines the Pull Quote block.
var PullQuoteBlockMeta = blocks.BlockMeta{
Key: "pull_quote",
Title: "Pull Quote",
Description: "Massive quote spanning 8 columns with hairline rules above and below",
Category: blocks.CategoryContent,
Source: "brutalist",
}
// PullQuoteData carries data for the pull-quote component.
type PullQuoteData struct {
Quote string
Attribution string
}
// PullQuoteBlock renders the pull quote.
// Content: {"quote": "<rich html>", "attribution": "..."}
func PullQuoteBlock(ctx context.Context, content map[string]any) string {
data := PullQuoteData{
Quote: getString(content, "quote"),
Attribution: getString(content, "attribution"),
}
var buf bytes.Buffer
_ = pullQuoteComponent(data).Render(ctx, &buf)
return buf.String()
}

12
pull_quote.templ Normal file
View File

@ -0,0 +1,12 @@
package main
templ pullQuoteComponent(data PullQuoteData) {
<section class="brutalist-pullquote brutalist-grid-12 brutalist-col-span-8" data-block="brutalist:pull_quote">
<blockquote>
@templ.Raw(data.Quote)
</blockquote>
if data.Attribution != "" {
<cite>— { data.Attribution }</cite>
}
</section>
}

71
pull_quote_templ.go Normal file
View File

@ -0,0 +1,71 @@
// 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"
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, "<section class=\"brutalist-pullquote brutalist-grid-12 brutalist-col-span-8\" data-block=\"brutalist:pull_quote\"><blockquote>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Quote).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</blockquote>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Attribution != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<cite>— ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Attribution)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pull_quote.templ`, Line: 9, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</cite>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

187
register.go Normal file
View File

@ -0,0 +1,187 @@
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 a templates.TemplateFunc.
// templ.Component already implements templates.HTMLComponent via Render.
func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc {
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
return f(ctx, doc)
}
}
// Register installs the Brutalist system template, its four page templates,
// theme-specific blocks, block overrides, and the email wrapper.
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
Key: "brutalist",
Title: "Brutalist",
Description: "Concrete, oversized type, hard 12-col grid theme for design studios, architecture firms and art galleries.",
})
if err := tr.RegisterPageTemplate("brutalist", templates.PageTemplateMeta{
Key: "default",
Title: "Default",
Description: "Standard masthead + 12-col body + footer",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderBrutalist)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("brutalist", templates.PageTemplateMeta{
Key: "landing",
Title: "Index Sheet",
Description: "Oversized headline + project ledger",
Slots: []string{"hero", "ledger", "footer"},
}, wrap(RenderBrutalistLanding)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("brutalist", templates.PageTemplateMeta{
Key: "article",
Title: "Case Study",
Description: "Long-form project page with metadata strip",
Slots: []string{"header", "meta", "main", "footer"},
}, wrap(RenderBrutalistArticle)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("brutalist", templates.PageTemplateMeta{
Key: "full-width",
Title: "Full Bleed",
Description: "Edge-to-edge gallery layout",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderBrutalistFullWidth)); err != nil {
return err
}
// Load schemas BEFORE any block registration so the registry can bind
// content shapes to the block keys.
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
return err
}
br.Register(MastheadBlockMeta, MastheadBlock)
br.Register(ProjectLedgerBlockMeta, ProjectLedgerBlock)
br.Register(ConcreteHeroBlockMeta, ConcreteHeroBlock)
br.Register(MetaStripBlockMeta, MetaStripBlock)
br.Register(CaptionImageBlockMeta, CaptionImageBlock)
br.Register(PullQuoteBlockMeta, PullQuoteBlock)
br.Register(ColophonBlockMeta, ColophonBlock)
br.RegisterTemplateOverride("brutalist", "heading", BrutalistHeadingBlock)
br.RegisterTemplateOverride("brutalist", "text", BrutalistTextBlock)
br.RegisterTemplateOverride("brutalist", "button", BrutalistButtonBlock)
br.RegisterTemplateOverride("brutalist", "image", BrutalistImageBlock)
tr.RegisterEmailWrapper("brutalist", BrutalistEmailWrapper)
return nil
}
// DefaultMasterPages provisions the Brutalist default master pages on first
// theme activation. Three masters cover the four page templates per
// spec section 7.
func DefaultMasterPages() []plugin.MasterPageDefinition {
return []plugin.MasterPageDefinition{
{
Key: "brutalist:default-master",
Title: "Brutalist Default Master",
PageTemplates: []string{"default", "article"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "navbar",
Title: "Masthead Nav",
Content: map[string]any{"menuName": "main"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "brutalist:masthead",
Title: "Studio Masthead",
Content: map[string]any{"studioName": "STUDIO"},
Slot: "header",
SortOrder: 1,
},
{
BlockKey: "slot",
Title: "Main Content",
Content: map[string]any{"slotName": "main"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "brutalist:colophon",
Title: "Colophon Footer",
Content: map[string]any{"showAddress": true},
Slot: "footer",
SortOrder: 0,
},
},
},
{
Key: "brutalist:index-master",
Title: "Brutalist Index Master",
PageTemplates: []string{"landing"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "brutalist:masthead",
Title: "Studio Masthead",
Content: map[string]any{"studioName": "STUDIO"},
Slot: "hero",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Ledger Slot",
Content: map[string]any{"slotName": "ledger"},
Slot: "ledger",
SortOrder: 0,
},
{
BlockKey: "brutalist:colophon",
Title: "Colophon Footer",
Content: map[string]any{"showAddress": true},
Slot: "footer",
SortOrder: 0,
},
},
},
{
Key: "brutalist:fullbleed-master",
Title: "Brutalist Full Bleed Master",
PageTemplates: []string{"full-width"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "navbar",
Title: "Masthead Nav",
Content: map[string]any{"menuName": "main"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Full Bleed Slot",
Content: map[string]any{"slotName": "main"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "brutalist:colophon",
Title: "Colophon Footer",
Content: map[string]any{"showAddress": false},
Slot: "footer",
SortOrder: 0,
},
},
},
}
}

25
registration.go Normal file
View File

@ -0,0 +1,25 @@
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 Brutalist theme.
var Registration = plugin.PluginRegistration{
Name: "brutalist",
Version: plugin.ParseModVersion(pluginModBytes),
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
return Register(tr, br)
},
Assets: func() http.Handler { return AssetsHandler() },
Schemas: func() fs.FS { return Schemas() },
ThemePresets: func() []byte { return ThemePresets() },
BundledFonts: func() []byte { return BundledFonts() },
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
}

View File

@ -0,0 +1,26 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Captioned Image",
"description": "Image with 11px mono caption in the left gutter",
"type": "object",
"properties": {
"image": {
"type": "string",
"title": "Image",
"description": "Figure image",
"x-editor": "media"
},
"caption": {
"type": "string",
"title": "Caption",
"description": "Mono uppercase caption text",
"x-editor": "text"
},
"figureNumber": {
"type": "string",
"title": "Figure Number",
"description": "e.g. Fig. 01",
"x-editor": "text"
}
}
}

View File

@ -0,0 +1,53 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Colophon Footer",
"description": "Three-row mono footer with address, email, and social links",
"type": "object",
"properties": {
"address": {
"type": "string",
"title": "Address",
"description": "Studio postal address",
"x-editor": "textarea"
},
"email": {
"type": "string",
"title": "Contact Email",
"description": "Contact email rendered in mono",
"x-editor": "text"
},
"showAddress": {
"type": "string",
"title": "Show Address",
"description": "Toggle the address row on or off",
"x-editor": "select",
"enum": ["true", "false"],
"default": "true"
},
"social": {
"type": "array",
"title": "Social Links",
"description": "Optional list of social links",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"title": "Label",
"description": "Display label",
"x-editor": "text"
},
"url": {
"type": "string",
"title": "URL",
"description": "Target URL",
"x-editor": "link"
}
},
"required": ["label", "url"]
}
}
}
}

View File

@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Concrete Hero",
"description": "200pt+ display headline with optional eyebrow and full-bleed image",
"type": "object",
"properties": {
"headline": {
"type": "string",
"title": "Headline",
"description": "Oversized display headline",
"x-editor": "text"
},
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Mono eyebrow line shown above the headline",
"x-editor": "text"
},
"media": {
"type": "string",
"title": "Background Media",
"description": "Optional full-bleed background image",
"x-editor": "media"
}
},
"required": ["headline"]
}

View File

@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Studio Masthead",
"description": "Oversized studio wordmark with mono index counter top-right",
"type": "object",
"properties": {
"studioName": {
"type": "string",
"title": "Studio Name",
"description": "Wordmark text rendered at display size",
"x-editor": "text",
"default": "STUDIO"
},
"tagline": {
"type": "string",
"title": "Tagline",
"description": "Short tagline rendered below the wordmark",
"x-editor": "text"
},
"indexNumber": {
"type": "string",
"title": "Index Counter",
"description": "Mono index counter top-right, e.g. 01 / 14",
"x-editor": "text",
"default": "01 / 14"
}
},
"required": ["studioName"]
}

View File

@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Metadata Strip",
"description": "Mono uppercase strip: CLIENT / YEAR / DISCIPLINE / LOCATION",
"type": "object",
"properties": {
"items": {
"type": "array",
"title": "Items",
"description": "Label/value pairs displayed inline",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"title": "Label",
"description": "Short uppercase label",
"x-editor": "text"
},
"value": {
"type": "string",
"title": "Value",
"description": "Value text shown beside the label",
"x-editor": "text"
}
},
"required": ["label", "value"]
}
}
}
}

View File

@ -0,0 +1,51 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Project Ledger",
"description": "Ordered project list on a 12-col grid: number, year, client, role, link",
"type": "object",
"properties": {
"rows": {
"type": "array",
"title": "Rows",
"description": "Project rows in display order",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"no": {
"type": "string",
"title": "Number",
"description": "Two-digit project number e.g. 01",
"x-editor": "text"
},
"year": {
"type": "string",
"title": "Year",
"description": "Year of the project",
"x-editor": "text"
},
"client": {
"type": "string",
"title": "Client",
"description": "Client name",
"x-editor": "text"
},
"role": {
"type": "string",
"title": "Role",
"description": "Discipline / role on the project",
"x-editor": "text"
},
"link": {
"type": "string",
"title": "Link",
"description": "Optional link to the project page",
"x-editor": "link"
}
},
"required": ["no", "client"]
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Pull Quote",
"description": "Massive quote spanning 8 columns with hairline rules above and below",
"type": "object",
"properties": {
"quote": {
"type": "string",
"title": "Quote",
"description": "The quoted text",
"x-editor": "richtext"
},
"attribution": {
"type": "string",
"title": "Attribution",
"description": "Person or source the quote is attributed to",
"x-editor": "text"
}
},
"required": ["quote"]
}

188
template.templ Normal file
View File

@ -0,0 +1,188 @@
package main
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// Brutalist default page template: header / 12-col body / colophon footer.
templ Brutalist(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/brutalist/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="default">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">
@templ.Raw(data.Slots["header"])
</div>
</header>
<main class="flex-grow w-full">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 2rem 1.5rem 4rem;">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<p class="brutalist-mono" style="color: hsl(var(--muted-foreground)); padding: 4rem 0;">NO CONTENT BLOCKS ASSIGNED TO THIS PAGE.</p>
}
</div>
</main>
<footer class="w-full mt-auto">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// BrutalistLanding: oversized headline + project ledger.
templ BrutalistLanding(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/brutalist/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="landing">
@bn.AdminBypassBanner(data.SiteSettings)
<section class="w-full">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">
@templ.Raw(data.Slots["hero"])
</div>
</section>
<main class="flex-grow w-full">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 2rem 1.5rem;">
if ledger, ok := data.Slots["ledger"]; ok && ledger != "" {
@templ.Raw(ledger)
}
</div>
</main>
<footer class="w-full mt-auto">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// BrutalistArticle: case study with metadata strip.
templ BrutalistArticle(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/brutalist/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="article">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">
@templ.Raw(data.Slots["header"])
</div>
</header>
<section class="w-full">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 1rem 1.5rem;">
@templ.Raw(data.Slots["meta"])
</div>
</section>
<main class="flex-grow w-full">
<article class="brutalist-shell" style="max-width: 60rem; margin: 0 auto; padding: 2rem 1.5rem 4rem;">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
}
</article>
</main>
<footer class="w-full mt-auto">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// BrutalistFullWidth: edge-to-edge gallery layout.
templ BrutalistFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/brutalist/style.css"},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="full-width">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">
@templ.Raw(data.Slots["header"])
</div>
</header>
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
}
</main>
<footer class="w-full mt-auto">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">
@templ.Raw(data.Slots["footer"])
</div>
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
func RenderBrutalist(ctx context.Context, doc map[string]any) templ.Component {
return Brutalist(parseBrutalistPageData(doc))
}
func RenderBrutalistLanding(ctx context.Context, doc map[string]any) templ.Component {
return BrutalistLanding(parseBrutalistPageData(doc))
}
func RenderBrutalistArticle(ctx context.Context, doc map[string]any) templ.Component {
return BrutalistArticle(parseBrutalistPageData(doc))
}
func RenderBrutalistFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return BrutalistFullWidth(parseBrutalistPageData(doc))
}

410
template_templ.go Normal file
View File

@ -0,0 +1,410 @@
// 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"
)
// Brutalist default page template: header / 12-col body / colophon footer.
func Brutalist(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/brutalist/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
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"brutalist-page antialiased min-h-screen flex flex-col\" data-brutalist-template=\"default\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<header class=\"w-full\" style=\"border-bottom: 1px solid hsl(var(--border));\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></header><main class=\"flex-grow w-full\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 2rem 1.5rem 4rem;\">")
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, "<p class=\"brutalist-mono\" style=\"color: hsl(var(--muted-foreground)); padding: 4rem 0;\">NO CONTENT BLOCKS ASSIGNED TO THIS PAGE.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></main><footer class=\"w-full mt-auto\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// BrutalistLanding: oversized headline + project ledger.
func BrutalistLanding(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/brutalist/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
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<body class=\"brutalist-page antialiased min-h-screen flex flex-col\" data-brutalist-template=\"landing\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<section class=\"w-full\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["hero"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></section><main class=\"flex-grow w-full\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 2rem 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if ledger, ok := data.Slots["ledger"]; ok && ledger != "" {
templ_7745c5c3_Err = templ.Raw(ledger).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></main><footer class=\"w-full mt-auto\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// BrutalistArticle: case study with metadata strip.
func BrutalistArticle(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, 16, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/brutalist/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
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<body class=\"brutalist-page antialiased min-h-screen flex flex-col\" data-brutalist-template=\"article\">")
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, 18, "<header class=\"w-full\" style=\"border-bottom: 1px solid hsl(var(--border));\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div></header><section class=\"w-full\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 1rem 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["meta"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></section><main class=\"flex-grow w-full\"><article class=\"brutalist-shell\" style=\"max-width: 60rem; margin: 0 auto; padding: 2rem 1.5rem 4rem;\">")
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
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</article></main><footer class=\"w-full mt-auto\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// BrutalistFullWidth: edge-to-edge gallery layout.
func BrutalistFullWidth(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, 24, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{"/templates/brutalist/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
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<body class=\"brutalist-page antialiased min-h-screen flex flex-col\" data-brutalist-template=\"full-width\">")
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, 26, "<header class=\"w-full\" style=\"border-bottom: 1px solid hsl(var(--border));\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div></header><main class=\"flex-grow w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</main><footer class=\"w-full mt-auto\"><div class=\"brutalist-shell\" style=\"max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</div></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func RenderBrutalist(ctx context.Context, doc map[string]any) templ.Component {
return Brutalist(parseBrutalistPageData(doc))
}
func RenderBrutalistLanding(ctx context.Context, doc map[string]any) templ.Component {
return BrutalistLanding(parseBrutalistPageData(doc))
}
func RenderBrutalistArticle(ctx context.Context, doc map[string]any) templ.Component {
return BrutalistArticle(parseBrutalistPageData(doc))
}
func RenderBrutalistFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return BrutalistFullWidth(parseBrutalistPageData(doc))
}
var _ = templruntime.GeneratedTemplate

17
text_override.go Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"bytes"
"context"
)
// BrutalistTextBlock overrides the built-in text block with brutalist
// measure (wider, no max-w-prose), Inter body, and mono <code> styling.
func BrutalistTextBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
class := getString(content, "class")
var buf bytes.Buffer
_ = brutalistTextComponent(text, class).Render(ctx, &buf)
return buf.String()
}

10
text_override.templ Normal file
View File

@ -0,0 +1,10 @@
package main
templ brutalistTextComponent(text, class string) {
<div
class={ "brutalist-text", class }
style="font-family: var(--font-body, 'Inter', -apple-system, sans-serif); font-size: 1.0625rem; line-height: 1.7; color: hsl(var(--foreground)); max-width: 70ch; letter-spacing: 0.005em;"
>
@templ.Raw(text)
</div>
}

66
text_override_templ.go Normal file
View File

@ -0,0 +1,66 @@
// 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"
func brutalistTextComponent(text, 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{"brutalist-text", 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, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `text_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" style=\"font-family: var(--font-body, 'Inter', -apple-system, sans-serif); font-size: 1.0625rem; line-height: 1.7; color: hsl(var(--foreground)); max-width: 70ch; letter-spacing: 0.005em;\">")
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, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate