Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63dfff0f19 | ||
|
|
b1650b2979 | ||
|
|
ea45d2ddb6 | ||
|
|
72f1a7c8f4 | ||
|
|
7f02a75a50 | ||
|
|
05323ea0ca | ||
|
|
db1dd3735b | ||
|
|
431545fef2 | ||
|
|
4a33dae2f7 | ||
|
|
1ca40326cf | ||
|
|
493cfe952a | ||
|
|
44f94ac93e | ||
|
|
50f58205f0 | ||
|
|
2adea9ef22 | ||
|
|
043e77c9ac | ||
|
|
6c03f6da6c | ||
|
|
979516b5e9 | ||
|
|
2066de69ba | ||
|
|
88df94ca40 | ||
|
|
ffc44da16c |
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
|||||||
tmp/
|
tmp/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
*.bnp
|
||||||
|
|||||||
60
Makefile
@ -1,29 +1,55 @@
|
|||||||
# Brutalist — local build helpers (.so plugin)
|
# brutalist — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
|
||||||
#
|
#
|
||||||
# Local single-shot build only. Does NOT deploy to a running CMS container.
|
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
|
||||||
# Use `make` (default) to build brutalist.so via CGO go build -buildmode=plugin.
|
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
|
||||||
|
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
|
||||||
|
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
|
||||||
|
# classifies the repo as codeless (no Go at the root) and packs the artifact
|
||||||
|
# with manifest.pb (codeless: true) and NO plugin.wasm.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# make build # pack brutalist-<version>.bnp (codeless — no wasm)
|
||||||
|
# make verify # validate the built .bnp (loader-identical checks)
|
||||||
|
# make archive-check # prove a clean `git archive HEAD` still builds codeless
|
||||||
|
# make publish # ninja plugin publish --bnp (to the active registry)
|
||||||
|
# make clean # remove build artefacts
|
||||||
|
|
||||||
.PHONY: all clean templ help
|
.PHONY: build verify archive-check publish clean help
|
||||||
|
|
||||||
PLUGIN_NAME := brutalist
|
PLUGIN_NAME := brutalist
|
||||||
|
NINJA := ninja
|
||||||
|
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
|
||||||
|
|
||||||
# Default target: build the .so locally.
|
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
|
||||||
all: $(PLUGIN_NAME).so
|
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
|
||||||
|
build:
|
||||||
|
$(NINJA) plugin build --dir .
|
||||||
|
|
||||||
# Local plugin build (no container). Useful for CI / quick checks.
|
# Validate the built .bnp exactly as the registry/loader will.
|
||||||
$(PLUGIN_NAME).so: $(wildcard *.go) plugin.mod go.mod presets.json fonts.json $(wildcard schemas/*.json)
|
verify:
|
||||||
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o $(PLUGIN_NAME).so .
|
$(NINJA) plugin verify $(BNP)
|
||||||
|
|
||||||
# Regenerate templ Go files locally.
|
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
|
||||||
templ:
|
archive-check:
|
||||||
templ generate
|
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
|
||||||
|
trap 'rm -rf "$$T"' EXIT && \
|
||||||
|
git archive HEAD | tar -x -C "$$T" && \
|
||||||
|
cd "$$T" && $(NINJA) plugin build --dir . && \
|
||||||
|
echo "archive-check OK"
|
||||||
|
|
||||||
# Remove built artefacts.
|
# Publish the prebuilt .bnp to the active registry.
|
||||||
|
# Pass --host https://my.blockninja.dev for the dev orchestrator.
|
||||||
|
publish: build
|
||||||
|
$(NINJA) plugin publish --bnp $(BNP)
|
||||||
|
|
||||||
|
# Remove build artefacts.
|
||||||
clean:
|
clean:
|
||||||
rm -f $(PLUGIN_NAME).so
|
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " all Build $(PLUGIN_NAME).so locally (default)"
|
@echo " build Pack the codeless .bnp (no wasm)"
|
||||||
@echo " templ Regenerate templ-generated Go files"
|
@echo " verify Validate the built .bnp (loader-identical checks)"
|
||||||
@echo " clean Remove built .so"
|
@echo " archive-check Prove a clean git archive HEAD packs codeless"
|
||||||
|
@echo " publish Publish the .bnp to the active registry"
|
||||||
|
@echo " clean Remove build artefacts"
|
||||||
|
|||||||
25
README.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Brutalist
|
||||||
|
|
||||||
|
A loud, honest theme built on chunky black borders, candy-bright accents, and hard offset shadows that snap on hover. Oversized grotesque headings sit on a strict grid, with structure left exposed on purpose. It has the sticker energy of a creative studio that wants its work to shout. The look is playful, and it stays legible.
|
||||||
|
|
||||||
|
## Good for
|
||||||
|
|
||||||
|
We built Brutalist for studios and makers who want a confident, unmissable site. Treat these as starting points, not limits. If the mood fits your brand, use it:
|
||||||
|
|
||||||
|
- Design and creative studios
|
||||||
|
- Indie software and product launches
|
||||||
|
- Architecture and interior firms
|
||||||
|
- Art galleries and zine shops
|
||||||
|
- Portfolios that want to stand out
|
||||||
|
|
||||||
|
## What you get
|
||||||
|
|
||||||
|
- Four color presets, each with a tuned light mode and a tuned dark mode: Concrete & Cadmium, Hazard, Riso, and Acid.
|
||||||
|
- Space Grotesk and Archivo Black for headings, Inter for body, and IBM Plex Mono for details, all bundled. Fonts stay admin-controlled, so you can swap them from the typography settings.
|
||||||
|
- Four persona blocks: a project ledger for indexing work, a sticker wall of pinned badges, a changelog for release notes, and a spec sheet for hard facts.
|
||||||
|
- A demo site that installs when you activate the theme: a home page, an about page, a short blog, and a contact page whose form emails your admins.
|
||||||
|
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully built starting point.
|
||||||
@ -1,33 +1,32 @@
|
|||||||
# Recommended Fonts — Brutalist
|
# Recommended fonts — Brutalist
|
||||||
|
|
||||||
`fonts.json` ships empty (`[]`) per the wave-1 policy in
|
Fonts are admin-controlled. Assign them in Admin → Appearance → Typography; the
|
||||||
`/home/alex/src/blockninja/themes/docs/FONTS.md`. No woff2 files are bundled in
|
theme consumes them only via `var(--font-heading|body|mono, <fallback>)`, never a
|
||||||
this build pass; the theme uses CSS variable fallback stacks until an admin
|
hardcoded family.
|
||||||
assigns fonts in the typography panel.
|
|
||||||
|
|
||||||
All three roles below are available in the Google Fonts tab of the typography
|
## Bundled (ship with this theme, SIL OFL 1.1)
|
||||||
picker. Open the typography panel, pick from the **Google Fonts** tab, and
|
|
||||||
assign:
|
|
||||||
|
|
||||||
| Role | Source | Family | Notes |
|
These latin-subset woff2 files are bundled in `assets/fonts/` and appear in the
|
||||||
|---|---|---|---|
|
picker as `source=template` rows (see `assets/fonts/LICENSES.md`):
|
||||||
| 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 | 16–18px 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)
|
| Role | Family | Weights | Notes |
|
||||||
|
|------|--------|---------|-------|
|
||||||
|
| Heading | **Space Grotesk** | 500, 700 | The signature display face — oversized, uppercase. |
|
||||||
|
| Display (alt) | **Archivo Black** | 400 | Heavier alternative for masthead-scale headlines. |
|
||||||
|
| Body | **Inter** | 400, 500, 700 | Neutral, high-legibility body copy. |
|
||||||
|
| Mono | **IBM Plex Mono** | 400, 500 | Meta labels, captions, buttons, tags. |
|
||||||
|
|
||||||
Until those assignments land, every `font-family` declaration in the theme
|
## Suggested assignment
|
||||||
flows through CSS custom properties with the following fallback stacks:
|
|
||||||
|
|
||||||
```css
|
- **Heading:** Space Grotesk (700). Alternative: Archivo Black for maximum weight.
|
||||||
var(--font-heading, "Space Grotesk", "Inter Tight", "Helvetica Neue", Helvetica, Arial, sans-serif)
|
- **Body:** Inter (400).
|
||||||
var(--font-body, "Inter", "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif)
|
- **Mono:** IBM Plex Mono (400). Alternative from the Google tab: JetBrains Mono.
|
||||||
var(--font-mono, "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Wave-2 follow-up (out of scope)
|
## Fallback stacks (used when no admin font is set)
|
||||||
|
|
||||||
The wider design system roadmap calls for shipping a custom display face for
|
- Heading: `"Space Grotesk", "Inter Tight", "Helvetica Neue", Helvetica, Arial, sans-serif`
|
||||||
Brutalist. When that lands, replace this doc with a `LICENSES.md` and add the
|
- Body: `"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif`
|
||||||
woff2 family to `fonts.json` per the schema in `docs/FONTS.md`.
|
- Mono: `"JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`
|
||||||
|
|
||||||
|
All four bundled families are SIL Open Font License 1.1. Swap freely from the
|
||||||
|
Google Fonts tab; the theme only references the CSS variables.
|
||||||
|
|||||||
21
RECOMMENDED_ICONS.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Recommended icons — Brutalist
|
||||||
|
|
||||||
|
This theme renders icons through the sprite pipeline
|
||||||
|
(`<svg><use href="/icons/<pack>.svg#<name>"></use></svg>`). Two packs are used by
|
||||||
|
the theme's block overrides and are declared in `plugin.mod`
|
||||||
|
(`required_icon_packs = ["lucide", "simple-icons"]`).
|
||||||
|
|
||||||
|
Until the loader auto-installs declared packs, install them manually before
|
||||||
|
activation:
|
||||||
|
|
||||||
|
1. Open `/admin/icons`.
|
||||||
|
2. Install **Lucide** (bundled — usually already present). Used for UI chrome:
|
||||||
|
`menu`, `x`, `chevron-down`, `chevron-left`, `chevron-right`, `arrow-right`,
|
||||||
|
`play`, `download`, `check`, `star`, `navigation`, `map-pin`, `flag`, `globe`,
|
||||||
|
`asterisk`.
|
||||||
|
3. Install **Simple Icons** (bundled). Used by the author-bio-hero social row:
|
||||||
|
`x`, `linkedin`, `github`.
|
||||||
|
|
||||||
|
`lucide` is the safe default for any icon-picker field. Utility-bar and footer
|
||||||
|
social icons render whatever `pack:name` string the admin assigns, so they work
|
||||||
|
with any installed pack.
|
||||||
16
assets/fonts/LICENSES.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Bundled font licenses
|
||||||
|
|
||||||
|
All bundled woff2 files in this directory are latin-subset builds redistributed
|
||||||
|
under the SIL Open Font License 1.1 (OFL). No modifications were made beyond
|
||||||
|
subsetting to the latin range.
|
||||||
|
|
||||||
|
| Family | Files | License | Source |
|
||||||
|
|--------|-------|---------|--------|
|
||||||
|
| Space Grotesk | space-grotesk-500.woff2, space-grotesk-700.woff2 | SIL OFL 1.1 | Florian Karsten / Google Fonts |
|
||||||
|
| Archivo Black | archivo-black-400.woff2 | SIL OFL 1.1 | Omnibus-Type / Google Fonts |
|
||||||
|
| Inter | inter-400.woff2, inter-500.woff2, inter-700.woff2 | SIL OFL 1.1 | Rasmus Andersson / Google Fonts |
|
||||||
|
| IBM Plex Mono | ibm-plex-mono-400.woff2, ibm-plex-mono-500.woff2 | SIL OFL 1.1 | IBM / Google Fonts |
|
||||||
|
|
||||||
|
Full OFL text: https://openfontlicense.org/. Font-family is still admin-controlled;
|
||||||
|
these bundled families appear as `source=template` rows in the fonts picker and
|
||||||
|
are consumed only via `var(--font-heading|body|mono, <fallback>)`.
|
||||||
BIN
assets/fonts/archivo-black-400.woff2
Normal file
BIN
assets/fonts/ibm-plex-mono-400.woff2
Normal file
BIN
assets/fonts/ibm-plex-mono-500.woff2
Normal file
BIN
assets/fonts/inter-400.woff2
Normal file
BIN
assets/fonts/inter-500.woff2
Normal file
BIN
assets/fonts/inter-700.woff2
Normal file
BIN
assets/fonts/space-grotesk-500.woff2
Normal file
BIN
assets/fonts/space-grotesk-700.woff2
Normal file
@ -1,15 +1,3 @@
|
|||||||
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 === */
|
/* === Brutalist theme utilities === */
|
||||||
|
|
||||||
:where(.brutalist-page, .brutalist-page *) {
|
:where(.brutalist-page, .brutalist-page *) {
|
||||||
@ -368,4 +356,3 @@ body.brutalist-grid-debug::before {
|
|||||||
transparent calc(100% / 12)
|
transparent calc(100% / 12)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
`
|
|
||||||
31
blocks/blocks.yaml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Brutalist theme persona block definitions (codeless .bnp, WO-WZ-021 / WO-TF-012).
|
||||||
|
# Keys are fully qualified (brutalist:<key>) because the codeless loader registers
|
||||||
|
# definitions verbatim. Blocks that duplicated builtins (masthead, concrete_hero,
|
||||||
|
# meta_strip, caption_image, pull_quote, colophon) were removed — they are now
|
||||||
|
# template overrides of navbar/hero/utility-bar/image/quote/footer. Only true
|
||||||
|
# persona furniture remains here.
|
||||||
|
blocks:
|
||||||
|
- key: brutalist:project_ledger
|
||||||
|
title: Project Ledger
|
||||||
|
description: "Ordered project index on a 12-col grid: number / year / client / role / arrow"
|
||||||
|
category: content
|
||||||
|
schema: project_ledger.schema.json
|
||||||
|
template: project_ledger.ninjatpl
|
||||||
|
- key: brutalist:sticker_wall
|
||||||
|
title: Sticker Wall
|
||||||
|
description: "Wall of hard-bordered candy stickers — labels, disciplines, or tags with offset shadows"
|
||||||
|
category: content
|
||||||
|
schema: sticker_wall.schema.json
|
||||||
|
template: sticker_wall.ninjatpl
|
||||||
|
- key: brutalist:changelog
|
||||||
|
title: Changelog
|
||||||
|
description: "Versioned changelog rows — version tag, date, and note per release"
|
||||||
|
category: content
|
||||||
|
schema: changelog.schema.json
|
||||||
|
template: changelog.ninjatpl
|
||||||
|
- key: brutalist:spec_sheet
|
||||||
|
title: Spec Sheet
|
||||||
|
description: "Mono label/value spec table with hairline rows"
|
||||||
|
category: content
|
||||||
|
schema: spec_sheet.schema.json
|
||||||
|
template: spec_sheet.ninjatpl
|
||||||
8
blocks/changelog.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<section class="bru-section bg-background text-foreground" data-block="brutalist:changelog">
|
||||||
|
<div class="bru-wrap-mid">
|
||||||
|
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
|
||||||
|
<div>
|
||||||
|
{% for entry in entries %}<div class="bru-changelog-row"><div><span class="bru-tag">{{ entry.version }}</span>{% if entry.date %}<div class="bru-meta mt-2">{{ entry.date }}</div>{% endif %}</div><div class="text-foreground">{% if entry.title %}<h3 class="bru-kicker" style="font-size:1.15rem;text-transform:none">{{ entry.title }}</h3>{% endif %}{% if entry.note %}<div class="mt-1 text-muted-foreground">{{ entry.note|safe }}</div>{% endif %}</div></div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
51
blocks/changelog.schema.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Changelog",
|
||||||
|
"description": "Versioned changelog rows",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"heading": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Heading",
|
||||||
|
"description": "Optional section heading",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"entries": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Entries",
|
||||||
|
"description": "Changelog entries, newest first",
|
||||||
|
"default": [],
|
||||||
|
"x-editor": "collection",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Version",
|
||||||
|
"description": "Version tag e.g. v1.2",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Date",
|
||||||
|
"description": "Release date",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Title",
|
||||||
|
"description": "Optional short title for the release",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"note": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Note",
|
||||||
|
"description": "What changed",
|
||||||
|
"x-editor": "richtext"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["version"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
blocks/project_ledger.ninjatpl
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<section class="bru-section bg-background text-foreground" data-block="brutalist:project_ledger">
|
||||||
|
<div class="bru-wrap">
|
||||||
|
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
|
||||||
|
{% if not rows %}<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 in 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="{{ row.link }}" style="color: inherit; text-decoration: none;">{{ row.client }}</a>{% else %}{{ row.client }}{% endif %}</span><span class="ledger-role">{{ row.role }}</span><span class="ledger-arrow">{% if row.link %}<a href="{{ row.link }}" aria-label="View project" style="color: inherit; text-decoration: none;">→</a>{% endif %}</span></li>{% endfor %}</ol>{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@ -4,6 +4,12 @@
|
|||||||
"description": "Ordered project list on a 12-col grid: number, year, client, role, link",
|
"description": "Ordered project list on a 12-col grid: number, year, client, role, link",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"heading": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Heading",
|
||||||
|
"description": "Optional section heading shown above the ledger",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
"rows": {
|
"rows": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"title": "Rows",
|
"title": "Rows",
|
||||||
6
blocks/spec_sheet.ninjatpl
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<section class="bru-section bg-background text-foreground" data-block="brutalist:spec_sheet">
|
||||||
|
<div class="bru-wrap-mid">
|
||||||
|
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
|
||||||
|
<dl>{% for row in rows %}<div class="bru-spec-row"><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div>{% endfor %}</dl>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@ -1,13 +1,19 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "Metadata Strip",
|
"title": "Spec Sheet",
|
||||||
"description": "Mono uppercase strip: CLIENT / YEAR / DISCIPLINE / LOCATION",
|
"description": "Mono label/value spec table with hairline rows",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"items": {
|
"heading": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Heading",
|
||||||
|
"description": "Optional section heading",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"rows": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"title": "Items",
|
"title": "Rows",
|
||||||
"description": "Label/value pairs displayed inline",
|
"description": "Spec label/value pairs in display order",
|
||||||
"default": [],
|
"default": [],
|
||||||
"x-editor": "collection",
|
"x-editor": "collection",
|
||||||
"items": {
|
"items": {
|
||||||
@ -16,13 +22,13 @@
|
|||||||
"label": {
|
"label": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Label",
|
"title": "Label",
|
||||||
"description": "Short uppercase label",
|
"description": "Spec label",
|
||||||
"x-editor": "text"
|
"x-editor": "text"
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Value",
|
"title": "Value",
|
||||||
"description": "Value text shown beside the label",
|
"description": "Spec value",
|
||||||
"x-editor": "text"
|
"x-editor": "text"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
8
blocks/sticker_wall.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<section class="bru-section bg-background text-foreground" data-block="brutalist:sticker_wall">
|
||||||
|
<div class="bru-wrap">
|
||||||
|
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
{% for item in items %}<span class="bru-sticker" style="font-size:{{ item.size|default:"1.1rem" }};{% if item.tone == "ink" %}background:hsl(var(--foreground));color:hsl(var(--background)){% elif item.tone == "paper" %}background:hsl(var(--background));color:hsl(var(--foreground)){% else %}background:hsl(var(--accent));color:hsl(var(--accent-foreground)){% endif %}">{{ item.text }}</span>{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
47
blocks/sticker_wall.schema.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Sticker Wall",
|
||||||
|
"description": "Wall of hard-bordered candy stickers",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"heading": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Heading",
|
||||||
|
"description": "Optional section heading",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"title": "Stickers",
|
||||||
|
"description": "Sticker labels in display order",
|
||||||
|
"default": [],
|
||||||
|
"x-editor": "collection",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"text": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Label",
|
||||||
|
"description": "Sticker text",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
|
"tone": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Tone",
|
||||||
|
"description": "Sticker colour",
|
||||||
|
"default": "accent",
|
||||||
|
"enum": ["accent", "ink", "paper"],
|
||||||
|
"x-editor": "select"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Size",
|
||||||
|
"description": "Optional font-size (e.g. 1.5rem)",
|
||||||
|
"x-editor": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["text"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,20 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
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
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,124 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
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
@ -1,58 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
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
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
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
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,281 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
import "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
@ -1,55 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/plugin"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assets/*
|
|
||||||
var assetsFS embed.FS
|
|
||||||
|
|
||||||
//go:embed schemas/*
|
|
||||||
var schemasFS embed.FS
|
|
||||||
|
|
||||||
//go:embed presets.json
|
|
||||||
var presetsData []byte
|
|
||||||
|
|
||||||
//go:embed fonts.json
|
|
||||||
var fontsData []byte
|
|
||||||
|
|
||||||
//go:embed plugin.mod
|
|
||||||
var pluginModBytes []byte
|
|
||||||
|
|
||||||
// Assets returns the embedded assets filesystem.
|
|
||||||
func Assets() fs.FS {
|
|
||||||
sub, _ := fs.Sub(assetsFS, "assets")
|
|
||||||
return sub
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schemas returns the embedded schemas filesystem.
|
|
||||||
func Schemas() fs.FS {
|
|
||||||
sub, _ := fs.Sub(schemasFS, "schemas")
|
|
||||||
return sub
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssetsHandler 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
35
fonts.json
@ -1 +1,34 @@
|
|||||||
[]
|
[
|
||||||
|
{
|
||||||
|
"name": "Space Grotesk",
|
||||||
|
"family": "Space Grotesk",
|
||||||
|
"variants": [
|
||||||
|
{ "weight": "500", "style": "normal", "file": "fonts/space-grotesk-500.woff2" },
|
||||||
|
{ "weight": "700", "style": "normal", "file": "fonts/space-grotesk-700.woff2" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Archivo Black",
|
||||||
|
"family": "Archivo Black",
|
||||||
|
"variants": [
|
||||||
|
{ "weight": "400", "style": "normal", "file": "fonts/archivo-black-400.woff2" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Inter",
|
||||||
|
"family": "Inter",
|
||||||
|
"variants": [
|
||||||
|
{ "weight": "400", "style": "normal", "file": "fonts/inter-400.woff2" },
|
||||||
|
{ "weight": "500", "style": "normal", "file": "fonts/inter-500.woff2" },
|
||||||
|
{ "weight": "700", "style": "normal", "file": "fonts/inter-700.woff2" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IBM Plex Mono",
|
||||||
|
"family": "IBM Plex Mono",
|
||||||
|
"variants": [
|
||||||
|
{ "weight": "400", "style": "normal", "file": "fonts/ibm-plex-mono-400.woff2" },
|
||||||
|
{ "weight": "500", "style": "normal", "file": "fonts/ibm-plex-mono-500.woff2" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
20
go.mod
@ -1,20 +0,0 @@
|
|||||||
module git.dev.alexdunmow.com/block/themes/brutalist
|
|
||||||
|
|
||||||
go 1.26.4
|
|
||||||
|
|
||||||
require (
|
|
||||||
git.dev.alexdunmow.com/block/core v0.14.1
|
|
||||||
github.com/a-h/templ v0.3.1020
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
connectrpc.com/connect v1.20.0 // indirect
|
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
|
||||||
golang.org/x/mod v0.34.0 // indirect
|
|
||||||
golang.org/x/text v0.36.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
|
||||||
)
|
|
||||||
44
go.sum
@ -1,44 +0,0 @@
|
|||||||
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
|
||||||
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
|
||||||
git.dev.alexdunmow.com/block/core v0.14.0 h1:cO6QQQPndhxwh1zbDvDOzebwrFS8Ka3WsxD8JBJLw/Y=
|
|
||||||
git.dev.alexdunmow.com/block/core v0.14.0/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
|
|
||||||
git.dev.alexdunmow.com/block/core v0.14.1 h1:63b25yzWoqrhBd3wdDFhsJzyGNM99wYmdh4WkN35hDk=
|
|
||||||
git.dev.alexdunmow.com/block/core v0.14.1/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
|
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
|
||||||
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
|
||||||
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,399 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
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
@ -1,51 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
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
|
|
||||||
682
manifest.yaml
Normal file
@ -0,0 +1,682 @@
|
|||||||
|
# Brutalist theme — codeless .bnp manifest (WO-WZ-021 Phase B1).
|
||||||
|
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
|
||||||
|
theme_presets: presets.json
|
||||||
|
bundled_fonts: fonts.json
|
||||||
|
master_pages: master_pages.json
|
||||||
|
required_icon_packs: [lucide, simple-icons]
|
||||||
|
|
||||||
|
system_templates:
|
||||||
|
- key: brutalist
|
||||||
|
title: Brutalist
|
||||||
|
description: "Concrete, oversized type, hard 12-col grid theme for design studios, architecture firms and art galleries."
|
||||||
|
|
||||||
|
page_templates:
|
||||||
|
- system: brutalist
|
||||||
|
key: default
|
||||||
|
title: Default
|
||||||
|
description: "Standard masthead + hard-ruled body + colophon footer"
|
||||||
|
slots: [header, main, footer]
|
||||||
|
- system: brutalist
|
||||||
|
key: full-width
|
||||||
|
title: Full Bleed
|
||||||
|
description: "Edge-to-edge gallery layout"
|
||||||
|
slots: [header, main, footer]
|
||||||
|
- system: brutalist
|
||||||
|
key: landing
|
||||||
|
title: Index Sheet
|
||||||
|
description: "Oversized display headline + stacked sections"
|
||||||
|
slots: [header, hero, main, cta, footer]
|
||||||
|
- system: brutalist
|
||||||
|
key: article
|
||||||
|
title: Case Study
|
||||||
|
description: "Long-form project page with post hero and byline"
|
||||||
|
slots: [header, post_hero, main, author, related, footer]
|
||||||
|
- system: brutalist
|
||||||
|
key: blog-index
|
||||||
|
title: Index Ledger
|
||||||
|
description: "Blog listing on a hard grid with featured slab"
|
||||||
|
slots: [header, featured, main, footer]
|
||||||
|
- system: brutalist
|
||||||
|
key: contact
|
||||||
|
title: Contact Sheet
|
||||||
|
description: "Contact form with optional details aside"
|
||||||
|
slots: [header, main, aside, footer]
|
||||||
|
- system: brutalist
|
||||||
|
key: auth
|
||||||
|
title: Access
|
||||||
|
description: "Centered bordered auth card"
|
||||||
|
slots: [main, footer]
|
||||||
|
|
||||||
|
template_overrides:
|
||||||
|
# Chrome (4)
|
||||||
|
- { template: brutalist, block: navbar }
|
||||||
|
- { template: brutalist, block: footer }
|
||||||
|
- { template: brutalist, block: utility-bar }
|
||||||
|
- { template: brutalist, block: announcement-bar }
|
||||||
|
# Marketing (12)
|
||||||
|
- { template: brutalist, block: hero }
|
||||||
|
- { template: brutalist, block: feature-grid }
|
||||||
|
- { template: brutalist, block: rich-section }
|
||||||
|
- { template: brutalist, block: stats }
|
||||||
|
- { template: brutalist, block: testimonial }
|
||||||
|
- { template: brutalist, block: logo-strip }
|
||||||
|
- { template: brutalist, block: pricing }
|
||||||
|
- { template: brutalist, block: team }
|
||||||
|
- { template: brutalist, block: faq }
|
||||||
|
- { template: brutalist, block: timeline }
|
||||||
|
- { template: brutalist, block: cta }
|
||||||
|
- { template: brutalist, block: countdown }
|
||||||
|
# Primitives (11)
|
||||||
|
- { template: brutalist, block: heading }
|
||||||
|
- { template: brutalist, block: text }
|
||||||
|
- { template: brutalist, block: rich-text }
|
||||||
|
- { template: brutalist, block: image }
|
||||||
|
- { template: brutalist, block: quote }
|
||||||
|
- { template: brutalist, block: divider }
|
||||||
|
- { template: brutalist, block: button }
|
||||||
|
- { template: brutalist, block: card }
|
||||||
|
- { template: brutalist, block: gallery }
|
||||||
|
- { template: brutalist, block: video-embed }
|
||||||
|
- { template: brutalist, block: audio-embed }
|
||||||
|
# Commerce / local (6)
|
||||||
|
- { template: brutalist, block: menu-list }
|
||||||
|
- { template: brutalist, block: event-list }
|
||||||
|
- { template: brutalist, block: contact-form }
|
||||||
|
- { template: brutalist, block: contact-card }
|
||||||
|
- { template: brutalist, block: hours-location }
|
||||||
|
- { template: brutalist, block: social-links }
|
||||||
|
# Blog family (12)
|
||||||
|
- { template: brutalist, block: blog-hero }
|
||||||
|
- { template: brutalist, block: featured-posts }
|
||||||
|
- { template: brutalist, block: related-posts }
|
||||||
|
- { template: brutalist, block: popular-posts }
|
||||||
|
- { template: brutalist, block: category-list }
|
||||||
|
- { template: brutalist, block: post-hero }
|
||||||
|
- { template: brutalist, block: post-metadata }
|
||||||
|
- { template: brutalist, block: author-bio }
|
||||||
|
- { template: brutalist, block: author-bio-hero }
|
||||||
|
- { template: brutalist, block: newsletter-signup }
|
||||||
|
- { template: brutalist, block: breadcrumbs }
|
||||||
|
- { template: brutalist, block: sidebar-nav }
|
||||||
|
# Auth surface (3)
|
||||||
|
- { template: brutalist, block: auth-form }
|
||||||
|
- { template: brutalist, block: password-reset-form }
|
||||||
|
- { template: brutalist, block: auth-status }
|
||||||
|
# System (1)
|
||||||
|
- { template: brutalist, block: page-suggestions }
|
||||||
|
|
||||||
|
email_wrappers:
|
||||||
|
- brutalist
|
||||||
|
|
||||||
|
css:
|
||||||
|
input_css_append: |
|
||||||
|
/* === Brutalist theme utilities === */
|
||||||
|
|
||||||
|
/* Bundled display faces. The host does not yet emit @font-face for
|
||||||
|
* bundled_fonts (LoadedPlugin.BundledFonts is unconsumed), so the theme
|
||||||
|
* self-serves them from its packed asset dir (/templates/brutalist/fonts/...).
|
||||||
|
* Presets set typography.fontHeading/body/mono to template:brutalist:<family>
|
||||||
|
* so --font-heading/body/mono resolve to these families. Families/weights
|
||||||
|
* mirror fonts.json exactly (8 woff2). */
|
||||||
|
@font-face { font-family: 'Space Grotesk'; src: url('/templates/brutalist/fonts/space-grotesk-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'Space Grotesk'; src: url('/templates/brutalist/fonts/space-grotesk-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'Archivo Black'; src: url('/templates/brutalist/fonts/archivo-black-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'Inter'; src: url('/templates/brutalist/fonts/inter-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'Inter'; src: url('/templates/brutalist/fonts/inter-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'Inter'; src: url('/templates/brutalist/fonts/inter-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'IBM Plex Mono'; src: url('/templates/brutalist/fonts/ibm-plex-mono-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
||||||
|
@font-face { font-family: 'IBM Plex Mono'; src: url('/templates/brutalist/fonts/ibm-plex-mono-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
|
||||||
|
|
||||||
|
: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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
BRUTALIST OVERRIDE KIT (Wave A) — semantic classes the block
|
||||||
|
overrides apply. Raw CSS (not Tailwind @apply) so it ships in
|
||||||
|
the codeless manifest regardless of JIT scanning. All colour
|
||||||
|
via hsl(var(--token)); all type via var(--font-*).
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.bru-section { padding: 2.75rem 0; }
|
||||||
|
@media (min-width: 768px) { .bru-section { padding: 4rem 0; } }
|
||||||
|
/* Landing: stacked full-bleed sections butt against a hard rule instead of
|
||||||
|
doubling their padding into a dead void (Wave B design-review fix). The CMS
|
||||||
|
wraps every block in its own <div data-block-id>, so the .bru-section
|
||||||
|
elements are NOT DOM siblings — the divider adjacency must target the block
|
||||||
|
wrappers ([data-block-id] + [data-block-id]), not the sections. */
|
||||||
|
[data-brutalist-template="landing"] main .bru-section { padding-top: 2.75rem; padding-bottom: 2.75rem; }
|
||||||
|
[data-brutalist-template="landing"] main [data-block-id] + [data-block-id] .bru-section { border-top: 4px solid hsl(var(--border)); }
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
[data-brutalist-template="landing"] main .bru-section { padding-top: 3rem; padding-bottom: 3rem; }
|
||||||
|
}
|
||||||
|
.bru-wrap { width: 100%; max-width: 80rem; margin: 0 auto; padding: 0 1.5rem; }
|
||||||
|
.bru-wrap-mid { width: 100%; max-width: 64rem; margin: 0 auto; padding: 0 1.5rem; }
|
||||||
|
.bru-wrap-narrow { width: 100%; max-width: 44rem; margin: 0 auto; padding: 0 1.5rem; }
|
||||||
|
|
||||||
|
/* Slab = the core bordered block. Shadow = the hard offset. */
|
||||||
|
.bru-slab {
|
||||||
|
border: 3px solid hsl(var(--border));
|
||||||
|
background-color: hsl(var(--card));
|
||||||
|
color: hsl(var(--card-foreground));
|
||||||
|
}
|
||||||
|
.bru-slab-bg {
|
||||||
|
border: 3px solid hsl(var(--border));
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
.bru-shadow { box-shadow: 6px 6px 0 0 hsl(var(--border)); }
|
||||||
|
.bru-shadow-sm { box-shadow: 4px 4px 0 0 hsl(var(--border)); }
|
||||||
|
.bru-shadow-accent { box-shadow: 6px 6px 0 0 hsl(var(--accent)); }
|
||||||
|
|
||||||
|
/* Hover snap: the block nudges into its own shadow. */
|
||||||
|
.bru-hover { transition: transform 90ms linear, box-shadow 90ms linear; }
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.bru-hover:hover { transform: translate(3px, 3px); box-shadow: 3px 3px 0 0 hsl(var(--border)); }
|
||||||
|
.bru-hover.bru-shadow-sm:hover { transform: translate(2px, 2px); box-shadow: 2px 2px 0 0 hsl(var(--border)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type utilities */
|
||||||
|
.bru-eyebrow {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
.bru-kicker {
|
||||||
|
font-family: var(--font-heading, "Space Grotesk", "Inter Tight", sans-serif);
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
line-height: 1;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
.bru-meta {
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sticker tag — candy accent chip with a hard border. */
|
||||||
|
.bru-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
border: 2px solid hsl(var(--border));
|
||||||
|
background-color: hsl(var(--accent));
|
||||||
|
color: hsl(var(--accent-foreground));
|
||||||
|
padding: 0.15rem 0.55rem;
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.4;
|
||||||
|
box-shadow: 2px 2px 0 0 hsl(var(--border));
|
||||||
|
}
|
||||||
|
.bru-tag-ghost {
|
||||||
|
background-color: transparent;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.bru-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.7rem 1.4rem;
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: 3px solid hsl(var(--border));
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 4px 4px 0 0 hsl(var(--border));
|
||||||
|
transition: transform 90ms linear, box-shadow 90ms linear;
|
||||||
|
}
|
||||||
|
.bru-btn-solid { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
|
||||||
|
.bru-btn-ghost { background-color: hsl(var(--background)); color: hsl(var(--foreground)); }
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.bru-btn:hover { transform: translate(2px, 2px); box-shadow: 2px 2px 0 0 hsl(var(--border)); }
|
||||||
|
}
|
||||||
|
.bru-btn:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
|
||||||
|
|
||||||
|
/* Hard rules */
|
||||||
|
.bru-rule { border: 0; border-top: 3px solid hsl(var(--border)); margin: 0; }
|
||||||
|
.bru-rule-thin { border: 0; border-top: 1px solid hsl(var(--border)); margin: 0; }
|
||||||
|
|
||||||
|
/* Inputs (block-level, complements the page-wide input reset above) */
|
||||||
|
.bru-field {
|
||||||
|
width: 100%;
|
||||||
|
border: 3px solid hsl(var(--input));
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
padding: 0.65rem 0.85rem;
|
||||||
|
font-family: var(--font-body, "Inter", sans-serif);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.bru-field:focus { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
|
||||||
|
.bru-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Numbered index cell (ledgers, ranks, steps) */
|
||||||
|
.bru-index {
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: hsl(var(--accent));
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sticker wall / changelog / spec-sheet persona furniture */
|
||||||
|
.bru-sticker {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
border: 3px solid hsl(var(--border));
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
font-family: var(--font-heading, "Space Grotesk", sans-serif);
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: 4px 4px 0 0 hsl(var(--border));
|
||||||
|
}
|
||||||
|
.bru-changelog-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 8rem 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
.bru-changelog-row:last-child { border-bottom: 1px solid hsl(var(--border)); }
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.bru-changelog-row { grid-template-columns: 1fr; gap: 0.5rem; }
|
||||||
|
}
|
||||||
|
.bru-spec-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.85rem 0;
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.bru-spec-row:last-child { border-bottom: 1px solid hsl(var(--border)); }
|
||||||
|
.bru-spec-row dt { text-transform: uppercase; color: hsl(var(--muted-foreground)); }
|
||||||
|
.bru-spec-row dd { margin: 0; text-align: right; color: hsl(var(--foreground)); }
|
||||||
|
|
||||||
|
/* Base media guarantee: element selector mirroring Tailwind preflight.
|
||||||
|
All utility-class layout scaffolding -- grid, grid-cols, col-span, p,
|
||||||
|
gap, space-y, aspect, object, w/h-full plus sm/md/lg variants -- was
|
||||||
|
removed here: the host Tailwind build now scans theme .ninjatpl
|
||||||
|
templates, so those classes compile natively from template usage. */
|
||||||
|
.brutalist-page img { max-width: 100%; }
|
||||||
|
|
||||||
32
master_pages.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "brutalist:default-master",
|
||||||
|
"title": "Brutalist Default",
|
||||||
|
"page_templates": ["default", "full-width", "contact", "blog-index", "article"],
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "navbar", "title": "Studio Nav", "content": { "menuName": "main", "companyName": "HARDSET", "ctaLabel": "Start a project", "ctaUrl": "/contact" }, "slot": "header", "sort_order": 0 },
|
||||||
|
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
|
||||||
|
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "columns", "companyName": "HARDSET", "tagline": "A creative studio for people who ship." }, "slot": "footer", "sort_order": 0 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "brutalist:landing-master",
|
||||||
|
"title": "Brutalist Index Sheet",
|
||||||
|
"page_templates": ["landing"],
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "navbar", "title": "Studio Nav", "content": { "menuName": "main", "companyName": "HARDSET", "ctaLabel": "Start a project", "ctaUrl": "/contact" }, "slot": "header", "sort_order": 0 },
|
||||||
|
{ "block_key": "hero", "title": "Display Hero", "content": { "template": "left-aligned", "eyebrow": "Creative studio", "headline": "WE MAKE LOUD THINGS", "subheadline": "Brand, web, and product work with a hard edge." }, "slot": "hero", "sort_order": 0 },
|
||||||
|
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
|
||||||
|
{ "block_key": "cta", "title": "Closing CTA", "content": { "background": "band", "heading": "Have a project in mind?", "text": "Tell us what you are building.", "primaryUrl": "/contact", "primaryLabel": "Start a project" }, "slot": "cta", "sort_order": 0 },
|
||||||
|
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "columns", "companyName": "HARDSET", "tagline": "A creative studio for people who ship." }, "slot": "footer", "sort_order": 0 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "brutalist:auth-master",
|
||||||
|
"title": "Brutalist Access",
|
||||||
|
"page_templates": ["auth"],
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "slot", "title": "Form", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
44
masthead.go
@ -1,44 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
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
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
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
@ -1,69 +0,0 @@
|
|||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,11 +2,8 @@
|
|||||||
name = "brutalist"
|
name = "brutalist"
|
||||||
display_name = "Brutalist"
|
display_name = "Brutalist"
|
||||||
scope = "@themes"
|
scope = "@themes"
|
||||||
version = "0.1.1"
|
version = "0.7.0"
|
||||||
description = "Concrete, oversized type, hard 12-col grid theme for design studios, architecture firms and art galleries."
|
description = "Concrete, oversized type, hard 12-col grid theme for design studios, architecture firms and art galleries."
|
||||||
kind = "theme"
|
kind = "theme"
|
||||||
categories = ["templates"]
|
categories = ["templates"]
|
||||||
tags = ["brutalist", "raw", "mono", "agency", "architecture", "gallery", "editorial", "minimal"]
|
tags = ["brutalist", "neo-brutalist", "studio", "agency", "bold", "mono", "editorial", "candy"]
|
||||||
|
|
||||||
[compatibility]
|
|
||||||
block_core = ">=0.11.0 <0.12.0"
|
|
||||||
|
|||||||
138
presets.json
@ -2,9 +2,14 @@
|
|||||||
{
|
{
|
||||||
"id": "concrete-red",
|
"id": "concrete-red",
|
||||||
"name": "Concrete & Cadmium",
|
"name": "Concrete & Cadmium",
|
||||||
"description": "Off-white concrete with cadmium red accent. Light mode primary; dark mode for case-study deep-dives.",
|
"description": "Paper-white concrete with a cadmium red accent. Black rules in light, paper rules in wet-concrete dark.",
|
||||||
"theme": {
|
"theme": {
|
||||||
"mode": "both",
|
"mode": "both",
|
||||||
|
"typography": {
|
||||||
|
"fontHeading": "template:brutalist:Space Grotesk",
|
||||||
|
"fontBody": "template:brutalist:Inter",
|
||||||
|
"fontMono": "template:brutalist:IBM Plex Mono"
|
||||||
|
},
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "40 14% 93%",
|
"background": "40 14% 93%",
|
||||||
"foreground": "0 0% 4%",
|
"foreground": "0 0% 4%",
|
||||||
@ -33,28 +38,33 @@
|
|||||||
"cardForeground": "40 14% 93%",
|
"cardForeground": "40 14% 93%",
|
||||||
"popover": "0 0% 9%",
|
"popover": "0 0% 9%",
|
||||||
"popoverForeground": "40 14% 93%",
|
"popoverForeground": "40 14% 93%",
|
||||||
"primary": "4 86% 54%",
|
"primary": "4 86% 56%",
|
||||||
"primaryForeground": "0 0% 6%",
|
"primaryForeground": "0 0% 6%",
|
||||||
"secondary": "0 0% 14%",
|
"secondary": "0 0% 14%",
|
||||||
"secondaryForeground": "40 14% 93%",
|
"secondaryForeground": "40 14% 93%",
|
||||||
"muted": "0 0% 12%",
|
"muted": "0 0% 12%",
|
||||||
"mutedForeground": "40 6% 65%",
|
"mutedForeground": "40 6% 65%",
|
||||||
"accent": "4 86% 54%",
|
"accent": "4 86% 56%",
|
||||||
"accentForeground": "0 0% 6%",
|
"accentForeground": "0 0% 6%",
|
||||||
"destructive": "0 84% 60%",
|
"destructive": "0 84% 60%",
|
||||||
"destructiveForeground": "0 0% 100%",
|
"destructiveForeground": "0 0% 100%",
|
||||||
"border": "40 14% 93%",
|
"border": "40 14% 93%",
|
||||||
"input": "40 14% 93%",
|
"input": "40 14% 93%",
|
||||||
"ring": "4 86% 54%"
|
"ring": "4 86% 56%"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "hazard-yellow",
|
"id": "hazard-yellow",
|
||||||
"name": "Hazard",
|
"name": "Hazard",
|
||||||
"description": "Construction-site safety yellow on ink black. Dark mode primary.",
|
"description": "Construction-site safety yellow. Ink type sits on yellow; wet-concrete dark glows.",
|
||||||
"theme": {
|
"theme": {
|
||||||
"mode": "dark",
|
"mode": "both",
|
||||||
|
"typography": {
|
||||||
|
"fontHeading": "template:brutalist:Space Grotesk",
|
||||||
|
"fontBody": "template:brutalist:Inter",
|
||||||
|
"fontMono": "template:brutalist:IBM Plex Mono"
|
||||||
|
},
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "40 14% 93%",
|
"background": "40 14% 93%",
|
||||||
"foreground": "0 0% 4%",
|
"foreground": "0 0% 4%",
|
||||||
@ -83,19 +93,129 @@
|
|||||||
"cardForeground": "48 100% 88%",
|
"cardForeground": "48 100% 88%",
|
||||||
"popover": "0 0% 9%",
|
"popover": "0 0% 9%",
|
||||||
"popoverForeground": "48 100% 88%",
|
"popoverForeground": "48 100% 88%",
|
||||||
"primary": "48 100% 50%",
|
"primary": "48 100% 52%",
|
||||||
"primaryForeground": "0 0% 6%",
|
"primaryForeground": "0 0% 6%",
|
||||||
"secondary": "0 0% 14%",
|
"secondary": "0 0% 14%",
|
||||||
"secondaryForeground": "48 100% 88%",
|
"secondaryForeground": "48 100% 88%",
|
||||||
"muted": "0 0% 12%",
|
"muted": "0 0% 12%",
|
||||||
"mutedForeground": "48 20% 65%",
|
"mutedForeground": "48 20% 65%",
|
||||||
"accent": "48 100% 50%",
|
"accent": "48 100% 52%",
|
||||||
"accentForeground": "0 0% 6%",
|
"accentForeground": "0 0% 6%",
|
||||||
"destructive": "0 84% 55%",
|
"destructive": "0 84% 55%",
|
||||||
"destructiveForeground": "0 0% 100%",
|
"destructiveForeground": "0 0% 100%",
|
||||||
"border": "48 100% 88%",
|
"border": "48 100% 88%",
|
||||||
"input": "48 100% 88%",
|
"input": "48 100% 88%",
|
||||||
"ring": "48 100% 50%"
|
"ring": "48 100% 52%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "riso-blue",
|
||||||
|
"name": "Riso",
|
||||||
|
"description": "Off-white with a riso-print electric blue. Cool concrete dark with a glowing blue accent.",
|
||||||
|
"theme": {
|
||||||
|
"mode": "both",
|
||||||
|
"typography": {
|
||||||
|
"fontHeading": "template:brutalist:Space Grotesk",
|
||||||
|
"fontBody": "template:brutalist:Inter",
|
||||||
|
"fontMono": "template:brutalist:IBM Plex Mono"
|
||||||
|
},
|
||||||
|
"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": "222 89% 52%",
|
||||||
|
"primaryForeground": "0 0% 100%",
|
||||||
|
"secondary": "40 8% 88%",
|
||||||
|
"secondaryForeground": "0 0% 4%",
|
||||||
|
"muted": "40 6% 90%",
|
||||||
|
"mutedForeground": "0 0% 35%",
|
||||||
|
"accent": "222 89% 52%",
|
||||||
|
"accentForeground": "0 0% 100%",
|
||||||
|
"destructive": "0 84% 50%",
|
||||||
|
"destructiveForeground": "0 0% 100%",
|
||||||
|
"border": "0 0% 4%",
|
||||||
|
"input": "0 0% 4%",
|
||||||
|
"ring": "222 89% 52%"
|
||||||
|
},
|
||||||
|
"darkColors": {
|
||||||
|
"background": "222 20% 7%",
|
||||||
|
"foreground": "210 30% 92%",
|
||||||
|
"card": "222 18% 10%",
|
||||||
|
"cardForeground": "210 30% 92%",
|
||||||
|
"popover": "222 18% 10%",
|
||||||
|
"popoverForeground": "210 30% 92%",
|
||||||
|
"primary": "222 92% 62%",
|
||||||
|
"primaryForeground": "222 20% 7%",
|
||||||
|
"secondary": "222 14% 15%",
|
||||||
|
"secondaryForeground": "210 30% 92%",
|
||||||
|
"muted": "222 14% 13%",
|
||||||
|
"mutedForeground": "213 18% 66%",
|
||||||
|
"accent": "222 92% 62%",
|
||||||
|
"accentForeground": "222 20% 7%",
|
||||||
|
"destructive": "0 84% 60%",
|
||||||
|
"destructiveForeground": "0 0% 100%",
|
||||||
|
"border": "210 30% 92%",
|
||||||
|
"input": "210 30% 92%",
|
||||||
|
"ring": "222 92% 62%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "acid-lime",
|
||||||
|
"name": "Acid",
|
||||||
|
"description": "Off-white with an acid lime accent. Ink type on lime; wet-concrete dark with a lime glow.",
|
||||||
|
"theme": {
|
||||||
|
"mode": "both",
|
||||||
|
"typography": {
|
||||||
|
"fontHeading": "template:brutalist:Space Grotesk",
|
||||||
|
"fontBody": "template:brutalist:Inter",
|
||||||
|
"fontMono": "template:brutalist:IBM Plex Mono"
|
||||||
|
},
|
||||||
|
"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": "84 78% 40%",
|
||||||
|
"primaryForeground": "0 0% 4%",
|
||||||
|
"secondary": "40 8% 88%",
|
||||||
|
"secondaryForeground": "0 0% 4%",
|
||||||
|
"muted": "40 6% 90%",
|
||||||
|
"mutedForeground": "0 0% 35%",
|
||||||
|
"accent": "84 78% 40%",
|
||||||
|
"accentForeground": "0 0% 4%",
|
||||||
|
"destructive": "0 84% 50%",
|
||||||
|
"destructiveForeground": "0 0% 100%",
|
||||||
|
"border": "0 0% 4%",
|
||||||
|
"input": "0 0% 4%",
|
||||||
|
"ring": "84 78% 40%"
|
||||||
|
},
|
||||||
|
"darkColors": {
|
||||||
|
"background": "90 12% 7%",
|
||||||
|
"foreground": "80 20% 92%",
|
||||||
|
"card": "90 10% 10%",
|
||||||
|
"cardForeground": "80 20% 92%",
|
||||||
|
"popover": "90 10% 10%",
|
||||||
|
"popoverForeground": "80 20% 92%",
|
||||||
|
"primary": "84 82% 52%",
|
||||||
|
"primaryForeground": "90 12% 7%",
|
||||||
|
"secondary": "90 8% 15%",
|
||||||
|
"secondaryForeground": "80 20% 92%",
|
||||||
|
"muted": "90 8% 13%",
|
||||||
|
"mutedForeground": "82 15% 66%",
|
||||||
|
"accent": "84 82% 52%",
|
||||||
|
"accentForeground": "90 12% 7%",
|
||||||
|
"destructive": "0 84% 60%",
|
||||||
|
"destructiveForeground": "0 0% 100%",
|
||||||
|
"border": "80 20% 92%",
|
||||||
|
"input": "80 20% 92%",
|
||||||
|
"ring": "84 82% 52%"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
preview.png
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 89 KiB |
@ -1,51 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,173 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
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
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
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
@ -1,187 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
"git.dev.alexdunmow.com/block/core/plugin"
|
|
||||||
"git.dev.alexdunmow.com/block/core/templates"
|
|
||||||
)
|
|
||||||
|
|
||||||
// wrap adapts a templ-returning render function to 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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/blocks"
|
|
||||||
"git.dev.alexdunmow.com/block/core/plugin"
|
|
||||||
"git.dev.alexdunmow.com/block/core/templates"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Registration is the compile-time plugin registration for the 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() },
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
BIN
screenshots/01-home-light.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
screenshots/02-home-dark.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
screenshots/03-about-light.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
screenshots/04-about-dark.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
screenshots/05-blog-in-praise-of-a-hard-border-light.png
Normal file
|
After Width: | Height: | Size: 420 KiB |
BIN
screenshots/06-blog-in-praise-of-a-hard-border-dark.png
Normal file
|
After Width: | Height: | Size: 426 KiB |
209
seed/seed.json
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
{
|
||||||
|
"demo": true,
|
||||||
|
"settings": {
|
||||||
|
"merge": {
|
||||||
|
"companyName": "HARDSET",
|
||||||
|
"tagline": "A creative studio for people who ship."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data_tables": [
|
||||||
|
{
|
||||||
|
"key": "contact_submissions",
|
||||||
|
"name": "Project Enquiries",
|
||||||
|
"description": "Seeded contact-form submissions for the HARDSET demo. A submission becomes a row here and emails the site admins.",
|
||||||
|
"columns": [
|
||||||
|
{ "key": "name", "label": "Name", "type": "text", "required": true },
|
||||||
|
{ "key": "email", "label": "Email", "type": "email", "required": true },
|
||||||
|
{ "key": "project", "label": "Project type", "type": "text", "required": false },
|
||||||
|
{ "key": "message", "label": "Message", "type": "text", "required": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"slug": "/",
|
||||||
|
"title": "HARDSET",
|
||||||
|
"template_key": "landing",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"block_key": "brutalist:sticker_wall",
|
||||||
|
"title": "Disciplines",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 1,
|
||||||
|
"content": {
|
||||||
|
"heading": "What we do",
|
||||||
|
"items": [
|
||||||
|
{ "text": "Brand", "tone": "accent", "size": "1.6rem" },
|
||||||
|
{ "text": "Web", "tone": "ink", "size": "1.6rem" },
|
||||||
|
{ "text": "Product", "tone": "paper", "size": "1.6rem" },
|
||||||
|
{ "text": "Motion", "tone": "accent", "size": "1.6rem" },
|
||||||
|
{ "text": "Type", "tone": "ink", "size": "1.6rem" },
|
||||||
|
{ "text": "Print", "tone": "paper", "size": "1.6rem" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block_key": "feature-grid",
|
||||||
|
"title": "Services",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 2,
|
||||||
|
"content": {
|
||||||
|
"eyebrow": "Services",
|
||||||
|
"heading": "How we work",
|
||||||
|
"layout": "card",
|
||||||
|
"columns": 3,
|
||||||
|
"items": [
|
||||||
|
{ "icon": "lucide:box", "title": "Brand systems", "text": "Names, marks, and the rules that hold them together." },
|
||||||
|
{ "icon": "lucide:code", "title": "Websites", "text": "Fast, accessible sites built to last past the launch." },
|
||||||
|
{ "icon": "lucide:zap", "title": "Product design", "text": "Interfaces that read clearly and ship on schedule." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block_key": "brutalist:project_ledger",
|
||||||
|
"title": "Selected work",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 3,
|
||||||
|
"content": {
|
||||||
|
"rows": [
|
||||||
|
{ "no": "01", "year": "2024", "client": "Independent record label", "role": "Brand system" },
|
||||||
|
{ "no": "02", "year": "2024", "client": "Climbing gym chain", "role": "Website" },
|
||||||
|
{ "no": "03", "year": "2023", "client": "Furniture maker", "role": "Product + web" },
|
||||||
|
{ "no": "04", "year": "2023", "client": "Community radio station", "role": "Identity" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/about",
|
||||||
|
"title": "The Studio",
|
||||||
|
"template_key": "default",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "The Studio", "level": 1 } },
|
||||||
|
{ "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "<p>HARDSET is a small studio. We take on a few projects at a time and give each one the whole team. No account layer, no handoff. You talk to the people doing the work.</p><p>We like clear briefs and hard deadlines. We ship. Then we help you run it.</p>" } },
|
||||||
|
{
|
||||||
|
"block_key": "brutalist:spec_sheet",
|
||||||
|
"title": "Studio facts",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 4,
|
||||||
|
"content": {
|
||||||
|
"heading": "Studio",
|
||||||
|
"rows": [
|
||||||
|
{ "label": "Founded", "value": "2019" },
|
||||||
|
{ "label": "Team", "value": "Six" },
|
||||||
|
{ "label": "Based", "value": "Melbourne" },
|
||||||
|
{ "label": "Focus", "value": "Brand, web, product" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block_key": "brutalist:changelog",
|
||||||
|
"title": "Recent",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 5,
|
||||||
|
"content": {
|
||||||
|
"heading": "Lately",
|
||||||
|
"entries": [
|
||||||
|
{ "version": "Now", "date": "This quarter", "title": "Booking two new projects", "note": "<p>We have room for two more brand or web projects this quarter.</p>" },
|
||||||
|
{ "version": "New", "date": "Last month", "title": "Two more designers", "note": "<p>The studio grew to six. Same way of working, more hands.</p>" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 3, "content": { "style": "pull", "quote": "Make it obvious, then make it loud.", "attribution": "HARDSET" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/blog",
|
||||||
|
"title": "Journal",
|
||||||
|
"template_key": "blog-index",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "card", "title": "Post: Borders", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "In praise of a hard border", "text": "<p>Why a thick black line still does more work than a soft shadow.</p>", "link": "/blog/in-praise-of-a-hard-border", "linkLabel": "Read" } },
|
||||||
|
{ "block_key": "card", "title": "Post: Ship", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "Ship the ugly first draft", "text": "<p>How shipping early keeps a project honest.</p>", "link": "/blog/ship-the-ugly-first-draft", "linkLabel": "Read" } },
|
||||||
|
{ "block_key": "card", "title": "Post: Type", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Set it in one typeface", "text": "<p>A short case for doing more with less type.</p>", "link": "/blog/set-it-in-one-typeface", "linkLabel": "Read" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/blog/in-praise-of-a-hard-border",
|
||||||
|
"title": "In praise of a hard border",
|
||||||
|
"template_key": "article",
|
||||||
|
"parent_slug": "/blog",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "html": "<p>A soft shadow asks you to squint. A hard border does not. It draws the edge, holds the block, and tells you where one thing ends and the next begins. We reach for the line first because it is honest.</p><p>The border is the cheapest structure in design. One rule, one weight, and a page stops being a soup of floating cards. It prints. It reads on a bad screen in bright sun. It survives a screenshot pasted into a slide deck at quarter size.</p><p>We came to it the hard way. Early on we chased depth — blurs, gradients, shadows stacked three deep to fake a little lift. It looked fine in the file and fell apart in the wild. On a cheap monitor the shadows vanished and the whole layout went flat.</p><p>A border never vanishes. It is one pixel or it is four, and it is there in every rendering context you will ever ship into. When a client forwards a mockup to their printer, the printer sees the same edges we did.</p><p>There is a discipline in it too. A border forces you to decide where a thing begins. You cannot let a section bleed vaguely into the next and hope the eye sorts it out. You commit to the edge, and the commitment shows.</p><p>Weight is the only variable that matters. A hairline says these belong together, quietly. A four-pixel rule says stop here, this is a wall. We keep two weights on most projects and never more than three. Past that the borders start arguing with each other.</p><p>Colour is where the energy comes from. The border stays black. Then we add one loud accent — a single hot hue — and let it do all the shouting. Black holds the structure; the accent carries the mood. That split keeps a loud page from turning to noise.</p><p>Offset is the last trick. Take a bordered block and drop a hard shadow four pixels down and right — no blur, just a second solid shape. Now the block sits on the page like a physical object. It reads as printed, stamped, real. It is the whole brutalist move in one property.</p><p>None of this is nostalgia. We are not printing zines. We are building interfaces that have to survive resizing, dark mode, reduced motion, and a decade of browser churn. Hard edges survive all of it because there is nothing to degrade.</p><p>So we start with the line. Draw the edge, hold the block, pick one loud colour, offset the shadow. That is the whole trick, and it has never once let us down on a real screen in bright sun.</p>" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/blog/ship-the-ugly-first-draft",
|
||||||
|
"title": "Ship the ugly first draft",
|
||||||
|
"template_key": "article",
|
||||||
|
"parent_slug": "/blog",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "html": "<p>The first draft is meant to be wrong. Its only job is to exist. Once it is on a real screen, the real problems show up and the fake ones you were bracing for quietly disappear.</p><p>We ship early and we ship rough. A grey box where the photo goes, placeholder copy that says what it needs to say, a layout held together with the bluntest possible structure. Ugly, but real, and in front of someone who can react to it.</p><p>The point is to move the argument out of the abstract. Nobody can usefully review a description of a page. Everybody can react to a page. The ugly draft turns opinions into decisions because there is finally something concrete to point at.</p><p>It also keeps us honest. A polished mock hides its weak spots under nice type and a good photo. A rough one has nowhere to hide — if the structure is wrong, the rough draft shows it on day one, while it is still cheap to fix.</p><p>Polish comes last, after the shape is right. We would rather spend the back half of a project making a correct thing beautiful than the front half making a wrong thing pretty. Ship the ugly draft. Let it take the hits. Then make it good.</p>" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/blog/set-it-in-one-typeface",
|
||||||
|
"title": "Set it in one typeface",
|
||||||
|
"template_key": "article",
|
||||||
|
"parent_slug": "/blog",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "html": "<p>One family, used well, beats three used at random. Pick a face with real range, learn its weights, and let scale do the talking. Most projects need far less type than they think.</p><p>Range is the thing to look for. A grotesque with a heavy black, a workable regular, and a tight mono cousin can carry an entire site — headlines, body, meta, code — without ever reaching for a second family.</p><p>The discipline pays off in coherence. When every word on a site comes from one voice, the page reads as one thing. Mixing families is how sites end up feeling like a ransom note assembled from three different brands.</p><p>It pays off in engineering too. One family is fewer font files, fewer weights to load, less layout shift while they arrive. The performance budget thanks you, and so does anyone on a slow connection.</p><p>We set most of a project in a single grotesque. Big and tight for headlines, plain for body, mono for the small print. The result reads as one voice speaking at different volumes — which is exactly what a brand should sound like.</p>" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/contact",
|
||||||
|
"title": "Start a project",
|
||||||
|
"template_key": "contact",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"block_key": "contact-form",
|
||||||
|
"title": "Project enquiry",
|
||||||
|
"slot": "main",
|
||||||
|
"sort_order": 1,
|
||||||
|
"content": {
|
||||||
|
"heading": "Start a project",
|
||||||
|
"description": "Tell us what you are building and when you need it. We reply within two working days.",
|
||||||
|
"submitLabel": "Send it",
|
||||||
|
"successMessage": "Thanks. Your enquiry is in. We will reply within two working days.",
|
||||||
|
"fields": [
|
||||||
|
{ "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
|
||||||
|
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
|
||||||
|
{ "name": "project", "label": "Project type", "type": "text", "required": false, "width": "full" },
|
||||||
|
{ "name": "message", "label": "What are you building?", "type": "textarea", "required": true, "width": "full" }
|
||||||
|
],
|
||||||
|
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block_key": "contact-card",
|
||||||
|
"title": "Direct",
|
||||||
|
"slot": "aside",
|
||||||
|
"sort_order": 1,
|
||||||
|
"content": {
|
||||||
|
"heading": "Direct",
|
||||||
|
"columns": 1,
|
||||||
|
"channels": [
|
||||||
|
{ "icon": "lucide:mail", "label": "Email", "value": "studio@hardset.example", "link": "mailto:studio@hardset.example" },
|
||||||
|
{ "icon": "lucide:map-pin", "label": "Studio", "value": "Melbourne, AU" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "/login",
|
||||||
|
"title": "Sign in",
|
||||||
|
"template_key": "auth",
|
||||||
|
"blocks": [
|
||||||
|
{ "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } },
|
||||||
|
{ "block_key": "text", "title": "Foot", "slot": "footer", "sort_order": 1, "content": { "text": "<p>Clients sign in to review work and approve invoices.</p>" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"menu_items": [
|
||||||
|
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
|
||||||
|
{ "menu": "main", "label": "Studio", "page_slug": "/about", "sort_order": 2 },
|
||||||
|
{ "menu": "main", "label": "Journal", "page_slug": "/blog", "sort_order": 3 },
|
||||||
|
{ "menu": "main", "label": "Contact", "page_slug": "/contact", "sort_order": 4 }
|
||||||
|
]
|
||||||
|
}
|
||||||
19
seed/workflows.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"workflows": [
|
||||||
|
{
|
||||||
|
"key": "contact_notify_admins",
|
||||||
|
"name": "Project enquiry to admins",
|
||||||
|
"description": "On a contact-form submission, email the site admins.",
|
||||||
|
"enabled": true,
|
||||||
|
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"type": "email",
|
||||||
|
"name": "Email admins",
|
||||||
|
"template": "Form Submission Notification",
|
||||||
|
"recipients": { "mode": "admins", "scope": "all" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
188
template.templ
@ -1,188 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
@ -1,410 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1020
|
|
||||||
package main
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.dev.alexdunmow.com/block/core/templates/bn"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
22
templates/brutalist/article.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="article">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
<article>
|
||||||
|
{% if slots.post_hero %}{{ slots.post_hero|safe }}{% else %}<div class="bru-wrap-mid pt-12"><h1 class="bru-kicker" style="font-size:clamp(2rem,5vw,3.5rem)">{{ title }}</h1></div>{% endif %}
|
||||||
|
<div class="bru-wrap-mid py-10 md:py-14"><div class="prose max-w-none space-y-6">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="bru-meta">This post has no content yet.</p>{% endif %}</div></div>
|
||||||
|
{% if slots.author or slots.related %}
|
||||||
|
<div class="bru-wrap-mid pb-14 space-y-10">
|
||||||
|
{% if slots.author %}<div style="border-top:3px solid hsl(var(--border));padding-top:2.5rem">{{ slots.author|safe }}</div>{% endif %}
|
||||||
|
{% if slots.related %}<div>{{ slots.related|safe }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
templates/brutalist/auth.ninjatpl
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="auth">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<main class="flex-grow w-full flex flex-col items-center justify-center px-4 py-12">
|
||||||
|
<div class="w-full" style="max-width:30rem">
|
||||||
|
<a href="/" class="block text-center mb-8" aria-label="{{ site_settings.Title }} home">
|
||||||
|
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt }}" class="h-10 w-auto mx-auto">{% else %}<span class="bru-kicker" style="font-size:2rem">{{ site_settings.Title|default:"STUDIO" }}</span>{% endif %}
|
||||||
|
</a>
|
||||||
|
<div class="bru-slab bru-shadow p-6 md:p-8">
|
||||||
|
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="bru-meta">No form assigned to this page.</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if slots.footer %}<div class="mt-6 text-center bru-meta">{{ slots.footer|safe }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
templates/brutalist/blog-index.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="blog-index">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
<div class="bru-wrap py-12 md:py-16 space-y-12">
|
||||||
|
{% if title %}<div style="border-bottom:4px solid hsl(var(--border));padding-bottom:1.5rem"><h1 class="bru-kicker" style="font-size:clamp(2.5rem,7vw,5rem)">{{ title }}</h1>{% if page_meta.MetaDescription %}<p class="mt-3 text-lg text-muted-foreground">{{ page_meta.MetaDescription }}</p>{% endif %}</div>{% endif %}
|
||||||
|
{% if slots.featured %}<section>{{ slots.featured|safe }}</section>{% endif %}
|
||||||
|
{% if slots.main %}<section>{{ slots.main|safe }}</section>{% else %}<p class="bru-meta py-8">No posts published yet.</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
templates/brutalist/contact.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="contact">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
<div class="bru-wrap py-12 md:py-16">
|
||||||
|
{% if slots.aside %}
|
||||||
|
<div class="grid grid-cols-1 gap-12 lg:grid-cols-5">
|
||||||
|
<div class="lg:col-span-3 space-y-8">{% if slots.main %}{{ slots.main|safe }}{% endif %}</div>
|
||||||
|
<aside class="lg:col-span-2 space-y-8">{{ slots.aside|safe }}</aside>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="space-y-8">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="bru-meta py-8">No content blocks assigned to this page.</p>{% endif %}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
templates/brutalist/default.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="default">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
<div class="bru-wrap py-12 md:py-16">
|
||||||
|
{% if slots.main %}<div class="space-y-10">{{ slots.main|safe }}</div>{% else %}<p class="bru-meta py-16">No content blocks assigned to this page.</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
templates/brutalist/full-width.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="full-width">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="bru-wrap py-16"><p class="bru-meta">No content blocks assigned to this page.</p></div>{% endif %}
|
||||||
|
</main>
|
||||||
|
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
templates/brutalist/landing.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="landing">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||||
|
{% if slots.hero %}<section class="w-full" style="border-bottom:4px solid hsl(var(--border))">{{ slots.hero|safe }}</section>{% endif %}
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
{% if slots.main %}<div>{{ slots.main|safe }}</div>{% endif %}
|
||||||
|
</main>
|
||||||
|
{% if slots.cta %}<section class="w-full" style="border-top:4px solid hsl(var(--border))">{{ slots.cta|safe }}</section>{% endif %}
|
||||||
|
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
templates/email/brutalist.ninjatpl
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<!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>{{ site_name }}</title>
|
||||||
|
<style type="text/css">
|
||||||
|
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||||
|
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||||
|
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
|
||||||
|
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
|
||||||
|
@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="background-color: {{ colors.background|default:"#F2EFE8" }}; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; color: {{ colors.foreground|default:"#0A0A0A" }};">
|
||||||
|
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
|
||||||
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 48px 16px; background-color: {{ colors.background|default:"#F2EFE8" }};">
|
||||||
|
<table role="presentation" class="brutalist-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#F7F4ED" }}; border: 3px solid {{ colors.border|default:"#0A0A0A" }}; border-radius: 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="left" class="brutalist-email-pad" style="padding: 32px 40px; border-bottom: 3px solid {{ colors.border|default:"#0A0A0A" }};">
|
||||||
|
{% if site_name %}<h1 style="margin: 0; font-family: 'Space Grotesk', 'Inter Tight', Helvetica, Arial, sans-serif; font-weight: 700; font-size: 26px; letter-spacing: -0.02em; color: {{ colors.foreground|default:"#0A0A0A" }}; text-transform: uppercase;">{{ site_name }}</h1>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="brutalist-email-pad" style="padding: 40px 48px; color: {{ colors.foreground|default:"#0A0A0A" }}; font-size: 16px; line-height: 1.7;">{{ body|safe }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="brutalist-email-pad" style="padding: 24px 48px 32px; border-top: 3px solid {{ colors.border|default:"#0A0A0A" }}; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: {{ colors.mutedForeground|default:"#595959" }};">
|
||||||
|
{% if site_url %}<div style="margin-bottom: 8px;"><a href="{{ site_url }}" style="color: {{ colors.foreground|default:"#0A0A0A" }}; text-decoration: none; border-bottom: 1px solid {{ colors.border|default:"#0A0A0A" }};">{{ site_url }}</a></div>{% endif %}
|
||||||
|
{% if unsubscribe_url %}<div><a href="{{ unsubscribe_url }}" style="color: {{ colors.foreground|default:"#0A0A0A" }}; text-decoration: none; border-bottom: 1px solid {{ colors.border|default:"#0A0A0A" }};">UNSUBSCRIBE</a></div>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
templates/overrides/brutalist/announcement-bar.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{# brutalist announcement-bar override — bold bordered strip, dismissable. Fields: message, variant, linkText, linkUrl, dismissable. #}
|
||||||
|
{% set annstyle = "background:hsl(var(--accent));color:hsl(var(--accent-foreground))" %}
|
||||||
|
{% if variant == "alert" %}{% set annstyle = "background:hsl(var(--destructive));color:hsl(var(--destructive-foreground))" %}{% elif variant == "info" %}{% set annstyle = "background:hsl(var(--foreground));color:hsl(var(--background))" %}{% endif %}
|
||||||
|
<div data-bn-announcement data-bn-annkey="{{ variant|default:"promo" }}:{{ message|slugify }}" class="bn-announcement w-full {{ class }}" style="{{ annstyle }};border-bottom:3px solid hsl(var(--border))">
|
||||||
|
<div class="bru-wrap py-2 relative flex items-center justify-center gap-2">
|
||||||
|
<p class="bru-meta" style="color:inherit;letter-spacing:0.08em">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
|
||||||
|
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1"><svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#x"></use></svg></button>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if dismissable %}<script>
|
||||||
|
(function(){
|
||||||
|
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
|
||||||
|
bars.forEach(function(bar){
|
||||||
|
bar.setAttribute('data-bn-ann-wired','1');
|
||||||
|
var key='bnann:'+bar.getAttribute('data-bn-annkey');
|
||||||
|
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
|
||||||
|
var btn=bar.querySelector('[data-bn-ann-dismiss]');
|
||||||
|
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>{% endif %}
|
||||||
16
templates/overrides/brutalist/audio-embed.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{# brutalist audio-embed override — bordered player card. Fields: url, media, artwork, title, artist, download. #}
|
||||||
|
<figure class="bn-audio my-8 bru-slab bru-shadow overflow-hidden{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden bg-muted" style="border:2px solid hsl(var(--border))">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="bru-kicker truncate" style="font-size:1rem;text-transform:none">{{ title }}</p>{% endif %}{% if artist %}<p class="bru-meta truncate">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="bru-btn bru-btn-solid" style="padding:0.4rem 0.9rem;box-shadow:2px 2px 0 0 hsl(var(--border))" aria-label="Play{% if title %}: {{ title }}{% endif %}"><svg width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg>Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="bru-meta inline-flex items-center gap-1 text-foreground">Download</a>{% endif %}</div></div></div></figure><script>
|
||||||
|
(function(){
|
||||||
|
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
|
||||||
|
document.addEventListener('click',function(e){
|
||||||
|
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
|
||||||
|
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
|
||||||
|
e.preventDefault();
|
||||||
|
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
|
||||||
|
if(!src)return;
|
||||||
|
var slot=btn.closest('[data-bn-audio-slot]');
|
||||||
|
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
|
||||||
|
btn.remove();slot.insertBefore(a,slot.firstChild);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
61
templates/overrides/brutalist/auth-form.ninjatpl
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{# brutalist auth-form override — bordered login/register card. HTMX endpoints + input names + tab panels are load-bearing (public_auth.go); kept exact. Fields: default_tab, show_social, captchaEnabled; context.isPublicLoggedIn, context.publicUsername, context.site.google_oauth_client_id/facebook_app_id. NOTE: the proof-of-work captcha widget is server-generated and cannot be emitted from a codeless template — do NOT enable captchaEnabled on an override-rendered auth-form (see WO limitation). #}
|
||||||
|
{% if context.isPublicLoggedIn %}
|
||||||
|
<div class="bru-slab bru-shadow p-6 text-center" style="max-width:28rem;margin:0 auto">
|
||||||
|
<p class="bru-meta">Logged in as <strong class="text-foreground">{{ context.publicUsername }}</strong></p>
|
||||||
|
<a href="/account" class="bru-btn bru-btn-ghost mt-4">My Account</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% set defaultTab = default_tab|default:"login" %}
|
||||||
|
<style>
|
||||||
|
.bru-auth-tab{color:hsl(var(--muted-foreground));border-bottom:3px solid transparent;margin-bottom:-3px;background:transparent}
|
||||||
|
.bru-auth-tab[data-active="1"]{color:hsl(var(--foreground));border-bottom-color:hsl(var(--accent))}
|
||||||
|
</style>
|
||||||
|
<div class="auth-form" style="max-width:28rem;margin:0 auto">
|
||||||
|
<div id="auth-message"></div>
|
||||||
|
<div class="flex mb-6" style="border-bottom:3px solid hsl(var(--border))">
|
||||||
|
<button type="button" data-auth-tab="login" class="bru-auth-tab flex-1 py-2.5 bru-meta" data-active="{% if defaultTab == "register" %}0{% else %}1{% endif %}">Login</button>
|
||||||
|
<button type="button" data-auth-tab="register" class="bru-auth-tab flex-1 py-2.5 bru-meta" data-active="{% if defaultTab == "register" %}1{% else %}0{% endif %}">Register</button>
|
||||||
|
</div>
|
||||||
|
<div id="login-panel"{% if defaultTab == "register" %} class="hidden"{% endif %}>
|
||||||
|
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML" class="space-y-4">
|
||||||
|
<div><label class="bru-label">Email</label><input type="email" name="email" required class="bru-field"></div>
|
||||||
|
<div><label class="bru-label">Password</label><input type="password" name="password" required class="bru-field"></div>
|
||||||
|
{% if captchaEnabled %}<p class="bru-meta text-destructive">Captcha is enabled but cannot render in this theme. Disable it on the auth-form block.</p>{% endif %}
|
||||||
|
<button type="submit" class="bru-btn bru-btn-solid w-full" hx-disabled-elt="this">Login</button>
|
||||||
|
<p class="text-center bru-meta"><a href="/forgot-password" class="text-accent underline underline-offset-4">Forgot password?</a></p>
|
||||||
|
</form>
|
||||||
|
{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}
|
||||||
|
<div class="mt-5"><div class="relative my-4"><hr class="bru-rule-thin"><span class="bru-meta" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:hsl(var(--background));padding:0 .6rem">or</span></div>
|
||||||
|
{% if context.site.google_oauth_client_id %}<a href="/api/auth/google/start" class="bru-btn bru-btn-ghost w-full mb-2">Continue with Google</a>{% endif %}
|
||||||
|
{% if context.site.facebook_app_id %}<a href="/api/auth/facebook/start" class="bru-btn bru-btn-ghost w-full">Continue with Facebook</a>{% endif %}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div id="register-panel"{% if defaultTab != "register" %} class="hidden"{% endif %}>
|
||||||
|
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML" class="space-y-4">
|
||||||
|
<div><label class="bru-label">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="bru-field"></div>
|
||||||
|
<div><label class="bru-label">Email</label><input type="email" name="email" required class="bru-field"></div>
|
||||||
|
<div><label class="bru-label">Password</label><input type="password" name="password" required minlength="8" class="bru-field"></div>
|
||||||
|
{% if captchaEnabled %}<p class="bru-meta text-destructive">Captcha is enabled but cannot render in this theme. Disable it on the auth-form block.</p>{% endif %}
|
||||||
|
<button type="submit" class="bru-btn bru-btn-solid w-full" hx-disabled-elt="this">Create Account</button>
|
||||||
|
</form>
|
||||||
|
{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}
|
||||||
|
<div class="mt-5"><div class="relative my-4"><hr class="bru-rule-thin"><span class="bru-meta" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:hsl(var(--background));padding:0 .6rem">or</span></div>
|
||||||
|
{% if context.site.google_oauth_client_id %}<a href="/api/auth/google/start" class="bru-btn bru-btn-ghost w-full mb-2">Continue with Google</a>{% endif %}
|
||||||
|
{% if context.site.facebook_app_id %}<a href="/api/auth/facebook/start" class="bru-btn bru-btn-ghost w-full">Continue with Facebook</a>{% endif %}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var tabs=document.querySelectorAll('[data-auth-tab]');
|
||||||
|
if(!tabs.length)return;
|
||||||
|
function activate(name){
|
||||||
|
var lp=document.getElementById('login-panel'),rp=document.getElementById('register-panel');
|
||||||
|
if(lp)lp.classList.toggle('hidden',name!=='login');
|
||||||
|
if(rp)rp.classList.toggle('hidden',name!=='register');
|
||||||
|
tabs.forEach(function(t){t.setAttribute('data-active',t.getAttribute('data-auth-tab')===name?'1':'0');});
|
||||||
|
}
|
||||||
|
tabs.forEach(function(t){t.addEventListener('click',function(){activate(t.getAttribute('data-auth-tab'));});});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
33
templates/overrides/brutalist/auth-status.ninjatpl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{# brutalist auth-status override — header login state. Logout/resend HTMX endpoints load-bearing. Context: context.isPublicLoggedIn, context.publicDisplayName, context.publicUsername, context.publicEmailVerified. #}
|
||||||
|
{% if not context.isPublicLoggedIn %}
|
||||||
|
<div class="auth-status flex items-center gap-2">
|
||||||
|
<a href="/login" class="bru-meta px-3 py-1.5 text-foreground hover:text-accent transition-colors">Login</a>
|
||||||
|
<a href="/login?tab=register" class="bru-btn bru-btn-solid" style="padding:0.4rem 0.9rem">Sign Up</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% set displayName = context.publicDisplayName|default:context.publicUsername %}
|
||||||
|
{% if not context.publicEmailVerified %}<div class="fixed top-16 inset-x-0 z-40 text-center bru-meta" style="background:hsl(var(--accent));color:hsl(var(--accent-foreground));border-bottom:3px solid hsl(var(--border));padding:0.5rem 1rem">Please verify your email address. <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="underline">Resend verification email</button></div>{% endif %}
|
||||||
|
<div class="auth-status relative">
|
||||||
|
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="flex items-center gap-2 bru-meta px-3 py-1.5 text-foreground hover:text-accent transition-colors">
|
||||||
|
<span class="inline-flex items-center justify-center w-7 h-7 text-xs" style="background:hsl(var(--accent));color:hsl(var(--accent-foreground));border:2px solid hsl(var(--border));font-family:var(--font-heading,sans-serif);font-weight:700">{{ displayName|first }}</span>
|
||||||
|
<span>{{ displayName }}</span>
|
||||||
|
<svg width="12" height="12" class="w-3 h-3" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
|
||||||
|
</button>
|
||||||
|
<div id="auth-dropdown-menu" class="hidden absolute right-0 top-full mt-2 w-48 bru-slab bru-shadow py-1 z-50">
|
||||||
|
<a href="/account" class="block px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground">My Profile</a>
|
||||||
|
<a href="/account/reviews" class="block px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground">My Reviews</a>
|
||||||
|
<hr class="my-1 bru-rule-thin">
|
||||||
|
<button hx-post="/api/auth/logout" hx-swap="none" class="block w-full text-left px-4 py-2 text-sm text-destructive hover:bg-accent">Logout</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var menu = document.getElementById('auth-dropdown-menu');
|
||||||
|
var btn = document.querySelector('.auth-status > button');
|
||||||
|
if (!menu || !btn) return;
|
||||||
|
if (!btn.contains(e.target) && !menu.contains(e.target)) { menu.classList.add('hidden'); }
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
21
templates/overrides/brutalist/author-bio-hero.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{# brutalist author-bio-hero override — author-page header from the `authors` provider. Fields: showAvatar, author.*, showPostCount, showSocial. #}
|
||||||
|
{% if author %}
|
||||||
|
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}" style="border-bottom:4px solid hsl(var(--border))">
|
||||||
|
<div class="bru-wrap-narrow flex flex-col items-center text-center">
|
||||||
|
{% if showAvatar and author.avatar_url %}<div class="mb-5 h-24 w-24 overflow-hidden bg-muted bru-shadow" style="border:3px solid hsl(var(--border))">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||||
|
<h1 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,2.5rem)">{{ author.display_name }}</h1>
|
||||||
|
{% if author.job_title or author.company %}<p class="bru-meta mt-2 text-muted-foreground">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
|
||||||
|
{% if author.bio %}<p class="mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
|
||||||
|
<div class="bru-meta mt-5 flex flex-wrap items-center justify-center gap-4 text-muted-foreground">
|
||||||
|
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
|
||||||
|
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if showSocial %}<div class="mt-5 flex items-center justify-center gap-3">
|
||||||
|
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-accent" title="Website"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
|
||||||
|
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-accent" title="Twitter"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
|
||||||
|
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-accent" title="LinkedIn"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
|
||||||
|
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-accent" title="GitHub"><svg width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
|
||||||
|
</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
13
templates/overrides/brutalist/author-bio.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{# brutalist author-bio override — byline slab sourced from the `post` provider's author_* fields. Fields: heading, showAvatar, post.author_*. #}
|
||||||
|
{% if post.author_name %}
|
||||||
|
<aside class="bru-slab bru-shadow p-6{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if heading %}<p class="bru-meta mb-4 text-muted-foreground">{{ heading }}</p>{% endif %}
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden bg-muted" style="border:3px solid hsl(var(--border))">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h3 class="bru-kicker text-base" style="text-transform:none">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-accent">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
|
||||||
|
{% if post.author_bio %}<p class="mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
{% endif %}
|
||||||
23
templates/overrides/brutalist/blog-hero.ninjatpl
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{# brutalist blog-hero override — blog-index masthead + featured posts from the `posts` provider. Fields: title, subtitle, posts[], showExcerpt. #}
|
||||||
|
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="bru-wrap">
|
||||||
|
<div style="max-width:44rem">
|
||||||
|
{% if title %}<h1 class="bru-kicker" style="font-size:clamp(2rem,5vw,3.5rem)">{{ title }}</h1>{% endif %}
|
||||||
|
{% if subtitle %}<p class="mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if posts %}
|
||||||
|
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||||
|
{% for post in posts %}
|
||||||
|
<article class="group bru-slab bru-shadow bru-hover flex flex-col overflow-hidden{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch{% endif %}">
|
||||||
|
{% if post.featured_image_url %}<a href="{{ post.url }}" class="block overflow-hidden bg-muted{% if forloop.First %} md:w-1/2{% endif %}" style="border-bottom:3px solid hsl(var(--border))">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
||||||
|
<div class="flex flex-col justify-center gap-2 p-5 md:p-6{% if forloop.First %} md:w-1/2{% endif %}">
|
||||||
|
<h2 class="bru-kicker {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} leading-snug" style="text-transform:none"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h2>
|
||||||
|
{% if showExcerpt and post.excerpt %}<p class="text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
|
||||||
|
<div class="bru-meta mt-2 flex flex-wrap items-center gap-x-3">{% if post.author_name %}<span class="text-foreground">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||