diff --git a/RECOMMENDED_FONTS.md b/RECOMMENDED_FONTS.md index 4ddf310..32f61ce 100644 --- a/RECOMMENDED_FONTS.md +++ b/RECOMMENDED_FONTS.md @@ -1,33 +1,32 @@ -# Recommended Fonts — Brutalist +# Recommended fonts — Brutalist -`fonts.json` ships empty (`[]`) per the wave-1 policy in -`/home/alex/src/blockninja/themes/docs/FONTS.md`. No woff2 files are bundled in -this build pass; the theme uses CSS variable fallback stacks until an admin -assigns fonts in the typography panel. +Fonts are admin-controlled. Assign them in Admin → Appearance → Typography; the +theme consumes them only via `var(--font-heading|body|mono, )`, never a +hardcoded family. -All three roles below are available in the Google Fonts tab of the typography -picker. Open the typography panel, pick from the **Google Fonts** tab, and -assign: +## Bundled (ship with this theme, SIL OFL 1.1) -| Role | Source | Family | Notes | -|---|---|---|---| -| Heading | `google:Space Grotesk` | Space Grotesk | Display / wordmark face. The whole point of the theme is that it lives at 200pt+. Bold (700) is what the templates default to. | -| Body | `google:Inter` | Inter | 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. | +These latin-subset woff2 files are bundled in `assets/fonts/` and appear in the +picker as `source=template` rows (see `assets/fonts/LICENSES.md`): -## 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 -flows through CSS custom properties with the following fallback stacks: +## Suggested assignment -```css -var(--font-heading, "Space Grotesk", "Inter Tight", "Helvetica Neue", Helvetica, Arial, sans-serif) -var(--font-body, "Inter", "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif) -var(--font-mono, "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace) -``` +- **Heading:** Space Grotesk (700). Alternative: Archivo Black for maximum weight. +- **Body:** Inter (400). +- **Mono:** IBM Plex Mono (400). Alternative from the Google tab: JetBrains Mono. -## 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 -Brutalist. When that lands, replace this doc with a `LICENSES.md` and add the -woff2 family to `fonts.json` per the schema in `docs/FONTS.md`. +- Heading: `"Space Grotesk", "Inter Tight", "Helvetica Neue", Helvetica, Arial, sans-serif` +- Body: `"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif` +- 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. diff --git a/RECOMMENDED_ICONS.md b/RECOMMENDED_ICONS.md new file mode 100644 index 0000000..7801aae --- /dev/null +++ b/RECOMMENDED_ICONS.md @@ -0,0 +1,21 @@ +# Recommended icons — Brutalist + +This theme renders icons through the sprite pipeline +(``). 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. diff --git a/assets/fonts/LICENSES.md b/assets/fonts/LICENSES.md new file mode 100644 index 0000000..703cf6c --- /dev/null +++ b/assets/fonts/LICENSES.md @@ -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, )`. diff --git a/assets/fonts/archivo-black-400.woff2 b/assets/fonts/archivo-black-400.woff2 new file mode 100644 index 0000000..a88b0b5 Binary files /dev/null and b/assets/fonts/archivo-black-400.woff2 differ diff --git a/assets/fonts/ibm-plex-mono-400.woff2 b/assets/fonts/ibm-plex-mono-400.woff2 new file mode 100644 index 0000000..0804aaf Binary files /dev/null and b/assets/fonts/ibm-plex-mono-400.woff2 differ diff --git a/assets/fonts/ibm-plex-mono-500.woff2 b/assets/fonts/ibm-plex-mono-500.woff2 new file mode 100644 index 0000000..090f82f Binary files /dev/null and b/assets/fonts/ibm-plex-mono-500.woff2 differ diff --git a/assets/fonts/inter-400.woff2 b/assets/fonts/inter-400.woff2 new file mode 100644 index 0000000..f15b025 Binary files /dev/null and b/assets/fonts/inter-400.woff2 differ diff --git a/assets/fonts/inter-500.woff2 b/assets/fonts/inter-500.woff2 new file mode 100644 index 0000000..54f0a59 Binary files /dev/null and b/assets/fonts/inter-500.woff2 differ diff --git a/assets/fonts/inter-700.woff2 b/assets/fonts/inter-700.woff2 new file mode 100644 index 0000000..a68fb10 Binary files /dev/null and b/assets/fonts/inter-700.woff2 differ diff --git a/assets/fonts/space-grotesk-500.woff2 b/assets/fonts/space-grotesk-500.woff2 new file mode 100644 index 0000000..0db251f Binary files /dev/null and b/assets/fonts/space-grotesk-500.woff2 differ diff --git a/assets/fonts/space-grotesk-700.woff2 b/assets/fonts/space-grotesk-700.woff2 new file mode 100644 index 0000000..44604a0 Binary files /dev/null and b/assets/fonts/space-grotesk-700.woff2 differ diff --git a/blocks/blocks.yaml b/blocks/blocks.yaml index aaa886a..97c1b4b 100644 --- a/blocks/blocks.yaml +++ b/blocks/blocks.yaml @@ -1,48 +1,31 @@ -# Brutalist theme block definitions (codeless .bnp, WO-WZ-021). -# Keys are fully qualified (brutalist:) because the codeless loader -# registers definitions verbatim — there is no PluginBlockRegistry prefixing -# in the codeless path (unlike the wasm/templ path). The qualified keys match -# the hardcoded data-block attributes and the master_pages.json references. +# Brutalist theme persona block definitions (codeless .bnp, WO-WZ-021 / WO-TF-012). +# Keys are fully qualified (brutalist:) 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:masthead - title: Studio Masthead - description: Oversized studio wordmark with mono index counter top-right - category: layout - schema: masthead.schema.json - template: masthead.ninjatpl - key: brutalist:project_ledger title: Project Ledger - description: "Ordered project list on a 12-col grid: number / year / client / role / arrow" + 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:concrete_hero - title: Concrete Hero - description: 200pt+ display headline with optional eyebrow and full-bleed image - category: layout - schema: concrete_hero.schema.json - template: concrete_hero.ninjatpl - - key: brutalist:meta_strip - title: Metadata Strip - description: "Mono uppercase strip: CLIENT / YEAR / DISCIPLINE / LOCATION" + - key: brutalist:sticker_wall + title: Sticker Wall + description: "Wall of hard-bordered candy stickers — labels, disciplines, or tags with offset shadows" category: content - schema: meta_strip.schema.json - template: meta_strip.ninjatpl - - key: brutalist:caption_image - title: Captioned Image - description: Image with 11px mono caption in the left gutter + 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: caption_image.schema.json - template: caption_image.ninjatpl - - key: brutalist:pull_quote - title: Pull Quote - description: Massive quote spanning 8 columns with hairline rules above and below + 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: pull_quote.schema.json - template: pull_quote.ninjatpl - - key: brutalist:colophon - title: Colophon Footer - description: Three-row mono footer with copyright stamp - category: layout - schema: colophon.schema.json - template: colophon.ninjatpl + schema: spec_sheet.schema.json + template: spec_sheet.ninjatpl diff --git a/blocks/caption_image.ninjatpl b/blocks/caption_image.ninjatpl deleted file mode 100644 index aa1587d..0000000 --- a/blocks/caption_image.ninjatpl +++ /dev/null @@ -1 +0,0 @@ -
{% if image %}{{ caption }}{% endif %}{% if caption or figureNumber %}
{% if figureNumber %}{{ figureNumber }}{% endif %}{% if caption %}{{ caption }}{% endif %}
{% endif %}
diff --git a/blocks/caption_image.schema.json b/blocks/caption_image.schema.json deleted file mode 100644 index 91486ba..0000000 --- a/blocks/caption_image.schema.json +++ /dev/null @@ -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" - } - } -} diff --git a/blocks/changelog.ninjatpl b/blocks/changelog.ninjatpl new file mode 100644 index 0000000..ca3637c --- /dev/null +++ b/blocks/changelog.ninjatpl @@ -0,0 +1,8 @@ +
+
+{% if heading %}

{{ heading }}

{% endif %} +
+{% for entry in entries %}
{{ entry.version }}{% if entry.date %}
{{ entry.date }}
{% endif %}
{% if entry.title %}

{{ entry.title }}

{% endif %}{% if entry.note %}
{{ entry.note|safe }}
{% endif %}
{% endfor %} +
+
+
diff --git a/blocks/changelog.schema.json b/blocks/changelog.schema.json new file mode 100644 index 0000000..07de1c2 --- /dev/null +++ b/blocks/changelog.schema.json @@ -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"] + } + } + } +} diff --git a/blocks/colophon.ninjatpl b/blocks/colophon.ninjatpl deleted file mode 100644 index 45615df..0000000 --- a/blocks/colophon.ninjatpl +++ /dev/null @@ -1 +0,0 @@ -
{% if address and showAddress != "false" and showAddress != false %}
{{ address }}
{% endif %}
{% if email %}{{ email }}{% endif %}{% for link in social %}{{ link.label }}{% endfor %}
© {% now "2006" %}
diff --git a/blocks/colophon.schema.json b/blocks/colophon.schema.json deleted file mode 100644 index 5937774..0000000 --- a/blocks/colophon.schema.json +++ /dev/null @@ -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"] - } - } - } -} diff --git a/blocks/concrete_hero.ninjatpl b/blocks/concrete_hero.ninjatpl deleted file mode 100644 index 1a49649..0000000 --- a/blocks/concrete_hero.ninjatpl +++ /dev/null @@ -1 +0,0 @@ -
{% if media %}{% endif %}
{% if eyebrow %}

{{ eyebrow }}

{% endif %}

{{ headline }}

diff --git a/blocks/concrete_hero.schema.json b/blocks/concrete_hero.schema.json deleted file mode 100644 index 0c75339..0000000 --- a/blocks/concrete_hero.schema.json +++ /dev/null @@ -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"] -} diff --git a/blocks/masthead.ninjatpl b/blocks/masthead.ninjatpl deleted file mode 100644 index 57d4512..0000000 --- a/blocks/masthead.ninjatpl +++ /dev/null @@ -1,5 +0,0 @@ -
- {{ indexNumber|default:"01 / 14" }} -

{{ studioName|default:"STUDIO" }}

- {% if tagline %}

{{ tagline }}

{% endif %} -
diff --git a/blocks/masthead.schema.json b/blocks/masthead.schema.json deleted file mode 100644 index df42321..0000000 --- a/blocks/masthead.schema.json +++ /dev/null @@ -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"] -} diff --git a/blocks/meta_strip.ninjatpl b/blocks/meta_strip.ninjatpl deleted file mode 100644 index cfea4c6..0000000 --- a/blocks/meta_strip.ninjatpl +++ /dev/null @@ -1 +0,0 @@ -
{% for item in items %}
{{ item.label }}
{{ item.value }}
{% endfor %}
diff --git a/blocks/pull_quote.ninjatpl b/blocks/pull_quote.ninjatpl deleted file mode 100644 index 2aab9f6..0000000 --- a/blocks/pull_quote.ninjatpl +++ /dev/null @@ -1 +0,0 @@ -
{{ quote|safe }}
{% if attribution %}— {{ attribution }}{% endif %}
diff --git a/blocks/pull_quote.schema.json b/blocks/pull_quote.schema.json deleted file mode 100644 index 337050e..0000000 --- a/blocks/pull_quote.schema.json +++ /dev/null @@ -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"] -} diff --git a/blocks/spec_sheet.ninjatpl b/blocks/spec_sheet.ninjatpl new file mode 100644 index 0000000..ddce901 --- /dev/null +++ b/blocks/spec_sheet.ninjatpl @@ -0,0 +1,6 @@ +
+
+{% if heading %}

{{ heading }}

{% endif %} +
{% for row in rows %}
{{ row.label }}
{{ row.value }}
{% endfor %}
+
+
diff --git a/blocks/meta_strip.schema.json b/blocks/spec_sheet.schema.json similarity index 56% rename from blocks/meta_strip.schema.json rename to blocks/spec_sheet.schema.json index 5be18b4..d04d240 100644 --- a/blocks/meta_strip.schema.json +++ b/blocks/spec_sheet.schema.json @@ -1,13 +1,19 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Metadata Strip", - "description": "Mono uppercase strip: CLIENT / YEAR / DISCIPLINE / LOCATION", + "title": "Spec Sheet", + "description": "Mono label/value spec table with hairline rows", "type": "object", "properties": { - "items": { + "heading": { + "type": "string", + "title": "Heading", + "description": "Optional section heading", + "x-editor": "text" + }, + "rows": { "type": "array", - "title": "Items", - "description": "Label/value pairs displayed inline", + "title": "Rows", + "description": "Spec label/value pairs in display order", "default": [], "x-editor": "collection", "items": { @@ -16,13 +22,13 @@ "label": { "type": "string", "title": "Label", - "description": "Short uppercase label", + "description": "Spec label", "x-editor": "text" }, "value": { "type": "string", "title": "Value", - "description": "Value text shown beside the label", + "description": "Spec value", "x-editor": "text" } }, diff --git a/blocks/sticker_wall.ninjatpl b/blocks/sticker_wall.ninjatpl new file mode 100644 index 0000000..f7bbb0e --- /dev/null +++ b/blocks/sticker_wall.ninjatpl @@ -0,0 +1,8 @@ +
+
+{% if heading %}

{{ heading }}

{% endif %} +
+{% for item in items %}{{ item.text }}{% endfor %} +
+
+
diff --git a/blocks/sticker_wall.schema.json b/blocks/sticker_wall.schema.json new file mode 100644 index 0000000..1d1eb38 --- /dev/null +++ b/blocks/sticker_wall.schema.json @@ -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"] + } + } + } +} diff --git a/fonts.json b/fonts.json index fe51488..a729406 100644 --- a/fonts.json +++ b/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" } + ] + } +] diff --git a/manifest.yaml b/manifest.yaml index e7d4c67..fcfce4a 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -13,29 +13,96 @@ page_templates: - system: brutalist key: default title: Default - description: "Standard masthead + 12-col body + footer" + description: "Standard masthead + hard-ruled body + colophon footer" slots: [header, main, footer] - - system: brutalist - key: landing - title: Index Sheet - description: "Oversized headline + project ledger" - slots: [hero, ledger, footer] - - system: brutalist - key: article - title: Case Study - description: "Long-form project page with metadata strip" - slots: [header, meta, 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: button } + - { 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 @@ -401,3 +468,182 @@ css: ); } + /* ============================================================ + 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: 4rem 0; } + @media (min-width: 768px) { .bru-section { padding: 6rem 0; } } + .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)); } + diff --git a/master_pages.json b/master_pages.json index 0b5db75..b4f6d14 100644 --- a/master_pages.json +++ b/master_pages.json @@ -1,33 +1,32 @@ [ { "key": "brutalist:default-master", - "title": "Brutalist Default Master", - "page_templates": ["default", "article"], + "title": "Brutalist Default", + "page_templates": ["default", "full-width", "contact", "blog-index", "article"], "blocks": [ - { "block_key": "navbar", "title": "Masthead Nav", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 }, - { "block_key": "brutalist:masthead", "title": "Studio Masthead", "content": { "studioName": "STUDIO" }, "slot": "header", "sort_order": 1 }, + { "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": "brutalist:colophon", "title": "Colophon Footer", "content": { "showAddress": true }, "slot": "footer", "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:index-master", - "title": "Brutalist Index Master", + "key": "brutalist:landing-master", + "title": "Brutalist Index Sheet", "page_templates": ["landing"], "blocks": [ - { "block_key": "brutalist:masthead", "title": "Studio Masthead", "content": { "studioName": "STUDIO" }, "slot": "hero", "sort_order": 0 }, - { "block_key": "slot", "title": "Ledger Slot", "content": { "slotName": "ledger" }, "slot": "ledger", "sort_order": 0 }, - { "block_key": "brutalist:colophon", "title": "Colophon Footer", "content": { "showAddress": true }, "slot": "footer", "sort_order": 0 } + { "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:fullbleed-master", - "title": "Brutalist Full Bleed Master", - "page_templates": ["full-width"], + "key": "brutalist:auth-master", + "title": "Brutalist Access", + "page_templates": ["auth"], "blocks": [ - { "block_key": "navbar", "title": "Masthead Nav", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 }, - { "block_key": "slot", "title": "Full Bleed Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }, - { "block_key": "brutalist:colophon", "title": "Colophon Footer", "content": { "showAddress": false }, "slot": "footer", "sort_order": 0 } + { "block_key": "slot", "title": "Form", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 } ] } ] diff --git a/plugin.mod b/plugin.mod index 8007dd3..d58a931 100644 --- a/plugin.mod +++ b/plugin.mod @@ -6,4 +6,5 @@ version = "0.2.0" description = "Concrete, oversized type, hard 12-col grid theme for design studios, architecture firms and art galleries." kind = "theme" categories = ["templates"] -tags = ["brutalist", "raw", "mono", "agency", "architecture", "gallery", "editorial", "minimal"] +tags = ["brutalist", "neo-brutalist", "studio", "agency", "bold", "mono", "editorial", "candy"] +required_icon_packs = ["lucide", "simple-icons"] diff --git a/presets.json b/presets.json index 0409baf..c95d79a 100644 --- a/presets.json +++ b/presets.json @@ -2,7 +2,7 @@ { "id": "concrete-red", "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": { "mode": "both", "lightColors": { @@ -33,28 +33,28 @@ "cardForeground": "40 14% 93%", "popover": "0 0% 9%", "popoverForeground": "40 14% 93%", - "primary": "4 86% 54%", + "primary": "4 86% 56%", "primaryForeground": "0 0% 6%", "secondary": "0 0% 14%", "secondaryForeground": "40 14% 93%", "muted": "0 0% 12%", "mutedForeground": "40 6% 65%", - "accent": "4 86% 54%", + "accent": "4 86% 56%", "accentForeground": "0 0% 6%", "destructive": "0 84% 60%", "destructiveForeground": "0 0% 100%", "border": "40 14% 93%", "input": "40 14% 93%", - "ring": "4 86% 54%" + "ring": "4 86% 56%" } } }, { "id": "hazard-yellow", "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": { - "mode": "dark", + "mode": "both", "lightColors": { "background": "40 14% 93%", "foreground": "0 0% 4%", @@ -83,19 +83,119 @@ "cardForeground": "48 100% 88%", "popover": "0 0% 9%", "popoverForeground": "48 100% 88%", - "primary": "48 100% 50%", + "primary": "48 100% 52%", "primaryForeground": "0 0% 6%", "secondary": "0 0% 14%", "secondaryForeground": "48 100% 88%", "muted": "0 0% 12%", "mutedForeground": "48 20% 65%", - "accent": "48 100% 50%", + "accent": "48 100% 52%", "accentForeground": "0 0% 6%", "destructive": "0 84% 55%", "destructiveForeground": "0 0% 100%", "border": "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", + "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", + "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%" } } } diff --git a/seed/seed.json b/seed/seed.json new file mode 100644 index 0000000..1433af4 --- /dev/null +++ b/seed/seed.json @@ -0,0 +1,212 @@ +{ + "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": "

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.

We like clear briefs and hard deadlines. We ship. Then we help you run it.

" } }, + { + "block_key": "brutalist:spec_sheet", + "title": "Studio facts", + "slot": "main", + "sort_order": 3, + "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": 4, + "content": { + "heading": "Lately", + "entries": [ + { "version": "Now", "date": "This quarter", "title": "Booking two new projects", "note": "

We have room for two more brand or web projects this quarter.

" }, + { "version": "New", "date": "Last month", "title": "Two more designers", "note": "

The studio grew to six. Same way of working, more hands.

" } + ] + } + }, + { "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 5, "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": "

Why a thick black line still does more work than a soft shadow.

", "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": "

How shipping early keeps a project honest.

", "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": "

A short case for doing more with less type.

", "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": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "In praise of a hard border", "level": 1 } }, + { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "

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. It is honest. It prints. It reads on a bad screen in bright sun. When we want energy we add one loud colour and a hard offset. That is the whole trick.

" } } + ] + }, + { + "slug": "/blog/ship-the-ugly-first-draft", + "title": "Ship the ugly first draft", + "template_key": "article", + "parent_slug": "/blog", + "blocks": [ + { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Ship the ugly first draft", "level": 1 } }, + { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "

The first draft is meant to be wrong. Its job is to exist. Once it is on a real screen, the real problems show up and the fake ones fall away.

We ship early and often. It keeps the brief honest and the feedback specific. Polish comes last, after the shape is right.

" } } + ] + }, + { + "slug": "/blog/set-it-in-one-typeface", + "title": "Set it in one typeface", + "template_key": "article", + "parent_slug": "/blog", + "blocks": [ + { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Set it in one typeface", "level": 1 } }, + { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "

One family, used well, beats three used at random. Pick a face with range, learn its weights, and let scale do the talking.

We set most of a project in a single grotesque. Big for headlines, tight for meta, plain for body. The result reads as one voice.

" } } + ] + }, + { + "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": "

Clients sign in to review work and approve invoices.

" } } + ] + } + ], + "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 } + ] +} diff --git a/seed/workflows.json b/seed/workflows.json new file mode 100644 index 0000000..2967cde --- /dev/null +++ b/seed/workflows.json @@ -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" } + } + ] + } + ] +} diff --git a/templates/brutalist/article.ninjatpl b/templates/brutalist/article.ninjatpl index e0fac74..e287e1c 100644 --- a/templates/brutalist/article.ninjatpl +++ b/templates/brutalist/article.ninjatpl @@ -1,22 +1,22 @@ - + {{ head_html|safe }} {{ admin_banner_html|safe }} -
-
{{ slots.header|safe }}
-
-
-
{{ slots.meta|safe }}
-
+ {% if slots.header %}
{{ slots.header|safe }}
{% endif %}
-
- {% if slots.main %}{{ slots.main|safe }}{% endif %} +
+ {% if slots.post_hero %}{{ slots.post_hero|safe }}{% else %}

{{ title }}

{% endif %} +
{% if slots.main %}{{ slots.main|safe }}{% else %}

This post has no content yet.

{% endif %}
+ {% if slots.author or slots.related %} +
+ {% if slots.author %}
{{ slots.author|safe }}
{% endif %} + {% if slots.related %}
{{ slots.related|safe }}
{% endif %} +
+ {% endif %}
-
-
{{ slots.footer|safe }}
-
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} {{ body_end_html|safe }} diff --git a/templates/brutalist/auth.ninjatpl b/templates/brutalist/auth.ninjatpl new file mode 100644 index 0000000..26b2a67 --- /dev/null +++ b/templates/brutalist/auth.ninjatpl @@ -0,0 +1,19 @@ + + +{{ head_html|safe }} + + {{ admin_banner_html|safe }} +
+
+ + {% if site_settings.Logo %}{{ site_settings.LogoAlt }}{% else %}{{ site_settings.Title|default:"STUDIO" }}{% endif %} + +
+ {% if slots.main %}{{ slots.main|safe }}{% else %}

No form assigned to this page.

{% endif %} +
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} +
+
+ {{ body_end_html|safe }} + + diff --git a/templates/brutalist/blog-index.ninjatpl b/templates/brutalist/blog-index.ninjatpl new file mode 100644 index 0000000..2434b7f --- /dev/null +++ b/templates/brutalist/blog-index.ninjatpl @@ -0,0 +1,17 @@ + + +{{ head_html|safe }} + + {{ admin_banner_html|safe }} + {% if slots.header %}
{{ slots.header|safe }}
{% endif %} +
+
+ {% if title %}

{{ title }}

{% if page_meta.MetaDescription %}

{{ page_meta.MetaDescription }}

{% endif %}
{% endif %} + {% if slots.featured %}
{{ slots.featured|safe }}
{% endif %} + {% if slots.main %}
{{ slots.main|safe }}
{% else %}

No posts published yet.

{% endif %} +
+
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} + {{ body_end_html|safe }} + + diff --git a/templates/brutalist/contact.ninjatpl b/templates/brutalist/contact.ninjatpl new file mode 100644 index 0000000..2328821 --- /dev/null +++ b/templates/brutalist/contact.ninjatpl @@ -0,0 +1,22 @@ + + +{{ head_html|safe }} + + {{ admin_banner_html|safe }} + {% if slots.header %}
{{ slots.header|safe }}
{% endif %} +
+
+ {% if slots.aside %} +
+
{% if slots.main %}{{ slots.main|safe }}{% endif %}
+ +
+ {% else %} +
{% if slots.main %}{{ slots.main|safe }}{% else %}

No content blocks assigned to this page.

{% endif %}
+ {% endif %} +
+
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} + {{ body_end_html|safe }} + + diff --git a/templates/brutalist/default.ninjatpl b/templates/brutalist/default.ninjatpl index 737b84f..2fd5871 100644 --- a/templates/brutalist/default.ninjatpl +++ b/templates/brutalist/default.ninjatpl @@ -1,19 +1,15 @@ - + {{ head_html|safe }} {{ admin_banner_html|safe }} -
-
{{ slots.header|safe }}
-
+ {% if slots.header %}
{{ slots.header|safe }}
{% endif %}
-
- {% if slots.main %}{{ slots.main|safe }}{% else %}

NO CONTENT BLOCKS ASSIGNED TO THIS PAGE.

{% endif %} +
+ {% if slots.main %}
{{ slots.main|safe }}
{% else %}

No content blocks assigned to this page.

{% endif %}
-
-
{{ slots.footer|safe }}
-
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} {{ body_end_html|safe }} diff --git a/templates/brutalist/full-width.ninjatpl b/templates/brutalist/full-width.ninjatpl index c88c6e6..aad974c 100644 --- a/templates/brutalist/full-width.ninjatpl +++ b/templates/brutalist/full-width.ninjatpl @@ -1,17 +1,13 @@ - + {{ head_html|safe }} {{ admin_banner_html|safe }} -
-
{{ slots.header|safe }}
-
+ {% if slots.header %}
{{ slots.header|safe }}
{% endif %}
- {% if slots.main %}{{ slots.main|safe }}{% endif %} + {% if slots.main %}{{ slots.main|safe }}{% else %}

No content blocks assigned to this page.

{% endif %}
-
-
{{ slots.footer|safe }}
-
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} {{ body_end_html|safe }} diff --git a/templates/brutalist/landing.ninjatpl b/templates/brutalist/landing.ninjatpl index 89e334c..c9e83b8 100644 --- a/templates/brutalist/landing.ninjatpl +++ b/templates/brutalist/landing.ninjatpl @@ -1,19 +1,15 @@ - + {{ head_html|safe }} {{ admin_banner_html|safe }} -
-
{{ slots.hero|safe }}
-
+ {% if slots.header %}
{{ slots.header|safe }}
{% endif %} + {% if slots.hero %}
{{ slots.hero|safe }}
{% endif %}
-
- {% if slots.ledger %}{{ slots.ledger|safe }}{% endif %} -
+ {% if slots.main %}
{{ slots.main|safe }}
{% endif %}
-
-
{{ slots.footer|safe }}
-
+ {% if slots.cta %}
{{ slots.cta|safe }}
{% endif %} + {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} {{ body_end_html|safe }} diff --git a/templates/email/brutalist.ninjatpl b/templates/email/brutalist.ninjatpl index 18fa1f6..8c2f4f8 100644 --- a/templates/email/brutalist.ninjatpl +++ b/templates/email/brutalist.ninjatpl @@ -7,32 +7,32 @@ {{ site_name }} + 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; } + } + - + {% if preview_text %}
{{ preview_text }}
{% endif %}
- + - - diff --git a/templates/overrides/brutalist/announcement-bar.ninjatpl b/templates/overrides/brutalist/announcement-bar.ninjatpl new file mode 100644 index 0000000..061909c --- /dev/null +++ b/templates/overrides/brutalist/announcement-bar.ninjatpl @@ -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 %} +
+
+

{{ message }}{% if linkText and linkUrl %} {{ linkText }}{% endif %}

+{% if dismissable %}{% endif %} +
+
+{% if dismissable %}{% endif %} diff --git a/templates/overrides/brutalist/audio-embed.ninjatpl b/templates/overrides/brutalist/audio-embed.ninjatpl new file mode 100644 index 0000000..c79e9e2 --- /dev/null +++ b/templates/overrides/brutalist/audio-embed.ninjatpl @@ -0,0 +1,16 @@ +{# brutalist audio-embed override — bordered player card. Fields: url, media, artwork, title, artist, download. #} +
{% if artwork %}
{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}
{% endif %}
{% if title %}

{{ title }}

{% endif %}{% if artist %}

{{ artist }}

{% endif %}
{% if download %}Download{% endif %}
diff --git a/templates/overrides/brutalist/auth-form.ninjatpl b/templates/overrides/brutalist/auth-form.ninjatpl new file mode 100644 index 0000000..1a41c8c --- /dev/null +++ b/templates/overrides/brutalist/auth-form.ninjatpl @@ -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 %} +
+

Logged in as {{ context.publicUsername }}

+ My Account +
+{% else %} +{% set defaultTab = default_tab|default:"login" %} + +
+
+
+ + +
+ + +
+ +{% endif %} diff --git a/templates/overrides/brutalist/auth-status.ninjatpl b/templates/overrides/brutalist/auth-status.ninjatpl new file mode 100644 index 0000000..119b941 --- /dev/null +++ b/templates/overrides/brutalist/auth-status.ninjatpl @@ -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 %} +
+ Login + Sign Up +
+{% else %} +{% set displayName = context.publicDisplayName|default:context.publicUsername %} +{% if not context.publicEmailVerified %}
Please verify your email address.
{% endif %} +
+ + + +
+{% endif %} diff --git a/templates/overrides/brutalist/author-bio-hero.ninjatpl b/templates/overrides/brutalist/author-bio-hero.ninjatpl new file mode 100644 index 0000000..d677501 --- /dev/null +++ b/templates/overrides/brutalist/author-bio-hero.ninjatpl @@ -0,0 +1,21 @@ +{# brutalist author-bio-hero override — author-page header from the `authors` provider. Fields: showAvatar, author.*, showPostCount, showSocial. #} +{% if author %} +
+
+{% if showAvatar and author.avatar_url %}
{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}
{% endif %} +

{{ author.display_name }}

+{% if author.job_title or author.company %}

{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}

{% endif %} +{% if author.bio %}

{{ author.bio }}

{% endif %} +
+{% if showPostCount %}{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}{% endif %} +{% if author.location %}{{ author.location }}{% endif %} +
+{% if showSocial %}
+{% if author.website_url %}{% endif %} +{% if author.twitter_handle %}{% endif %} +{% if author.linkedin_url %}{% endif %} +{% if author.github_url %}{% endif %} +
{% endif %} +
+
+{% endif %} diff --git a/templates/overrides/brutalist/author-bio.ninjatpl b/templates/overrides/brutalist/author-bio.ninjatpl new file mode 100644 index 0000000..04836fc --- /dev/null +++ b/templates/overrides/brutalist/author-bio.ninjatpl @@ -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 %} + +{% endif %} diff --git a/templates/overrides/brutalist/blog-hero.ninjatpl b/templates/overrides/brutalist/blog-hero.ninjatpl new file mode 100644 index 0000000..1ca186b --- /dev/null +++ b/templates/overrides/brutalist/blog-hero.ninjatpl @@ -0,0 +1,23 @@ +{# brutalist blog-hero override — blog-index masthead + featured posts from the `posts` provider. Fields: title, subtitle, posts[], showExcerpt. #} +
+
+
+{% if title %}

{{ title }}

{% endif %} +{% if subtitle %}

{{ subtitle }}

{% endif %} +
+{% if posts %} +
+{% for post in posts %} + +{% endfor %} +
+{% endif %} +
+
diff --git a/templates/overrides/brutalist/breadcrumbs.ninjatpl b/templates/overrides/brutalist/breadcrumbs.ninjatpl new file mode 100644 index 0000000..40dfc11 --- /dev/null +++ b/templates/overrides/brutalist/breadcrumbs.ninjatpl @@ -0,0 +1,13 @@ +{# brutalist breadcrumbs override — trail from the `breadcrumbs` provider (page ancestry). Mono uppercase trail; keeps the arrows-template chevron verbatim. Fields: template, showHome, homeLabel, breadcrumbs[], separator, showCurrent, item.*. #} +{% set arrows = template == "arrows" %} + diff --git a/templates/overrides/brutalist/button.ninjatpl b/templates/overrides/brutalist/button.ninjatpl index 041af3d..a9769e7 100644 --- a/templates/overrides/brutalist/button.ninjatpl +++ b/templates/overrides/brutalist/button.ninjatpl @@ -1 +1,2 @@ -{{ text }} +{# brutalist button override — square, hard border, offset shadow, hover snap. Fields: label, link, style, size, icon, iconPosition, align, newTab. #} +{% with align=align|default:"left" %}{% endwith %} diff --git a/templates/overrides/brutalist/card.ninjatpl b/templates/overrides/brutalist/card.ninjatpl new file mode 100644 index 0000000..9957636 --- /dev/null +++ b/templates/overrides/brutalist/card.ninjatpl @@ -0,0 +1,2 @@ +{# brutalist card override — slab with hard offset shadow + hover snap. Fields: title, text, media, link, linkLabel, layout. #} +{% with layout=layout|default:"vertical" %}{% endwith %} diff --git a/templates/overrides/brutalist/category-list.ninjatpl b/templates/overrides/brutalist/category-list.ninjatpl new file mode 100644 index 0000000..7d8e50a --- /dev/null +++ b/templates/overrides/brutalist/category-list.ninjatpl @@ -0,0 +1,18 @@ +{# brutalist category-list override — from the `categories` provider. list or pills. Fields: heading, style, categories[], showCount. #} +{% set pills = style == "pills" %} +
+{% if heading %}

{{ heading }}

{% endif %} +{% if pills %} +
+{% for cat in categories %} +{{ cat.name }}{% if showCount %}{{ cat.post_count }}{% endif %} +{% endfor %} +
+{% else %} + +{% endif %} +
diff --git a/templates/overrides/brutalist/contact-card.ninjatpl b/templates/overrides/brutalist/contact-card.ninjatpl new file mode 100644 index 0000000..0dcf15d --- /dev/null +++ b/templates/overrides/brutalist/contact-card.ninjatpl @@ -0,0 +1,18 @@ +{# brutalist contact-card override — bordered channel slabs, accent icon square. Fields: heading, description, columns, channels[] (icon, label, value, link). Keeps builtin icon split + literal conditional column classes. #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if description %}

{{ description }}

{% endif %} +
+ {% for channel in channels %} +
+ {% if channel.icon %}{% endif %} +
+ {% if channel.label %}
{{ channel.label }}
{% endif %} + {% if channel.value %}
{% if channel.link %}{{ channel.value }}{% else %}{{ channel.value }}{% endif %}
{% endif %} +
+
+ {% endfor %} +
+
+
diff --git a/templates/overrides/brutalist/contact-form.ninjatpl b/templates/overrides/brutalist/contact-form.ninjatpl new file mode 100644 index 0000000..0bd2082 --- /dev/null +++ b/templates/overrides/brutalist/contact-form.ninjatpl @@ -0,0 +1,40 @@ +{# brutalist contact-form override — bordered form, mono labels. Submit + honeypot wiring is load-bearing; kept verbatim. Fields: heading, description, successMessage, submitLabel, fields[.name,.label,.type,.required,.placeholder,.width,.options], plus `form` context. #} +{% with bid=_block_id|default:context.blockId %} +
+
+ {% if form.submitted %} +
+
+

{{ form.message|default:successMessage|default:"Thanks — your message has been sent." }}

+
+ {% else %} + {% if heading %}

{{ heading }}

{% endif %} + {% if description %}

{{ description }}

{% endif %} + {% if form.errors._form %}
{{ form.errors._form }}
{% endif %} + {% with formAction="/api/blocks/"|add:bid|add:"/submit" formTarget="#contact-form-"|add:bid %} + {% form action=formAction hx_target=formTarget hx_swap="outerHTML" class="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2" %} + {% for field in fields %} +
+ {% if field.label %}{% endif %} + {% if field.type == "textarea" %} + + {% elif field.type == "select" %} + + {% else %} + + {% endif %} +
+ {% endfor %} + +
+ +
+ {% endform %} + {% endwith %} + {% endif %} +
+
+{% endwith %} diff --git a/templates/overrides/brutalist/countdown.ninjatpl b/templates/overrides/brutalist/countdown.ninjatpl new file mode 100644 index 0000000..6b59345 --- /dev/null +++ b/templates/overrides/brutalist/countdown.ninjatpl @@ -0,0 +1,49 @@ +{# brutalist countdown override — mono digit slabs, timer script + data hooks kept verbatim. Fields: heading, intro, target, variant, labelDays/Hours/Minutes/Seconds, expiredMessage, expiredUrl, expiredLabel. #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+
+
--{{ labelDays|default:"Days" }}
+
--{{ labelHours|default:"Hours" }}
+
--{{ labelMinutes|default:"Minutes" }}
+
--{{ labelSeconds|default:"Seconds" }}
+
+ +
+ +
+
+ diff --git a/templates/overrides/brutalist/cta.ninjatpl b/templates/overrides/brutalist/cta.ninjatpl new file mode 100644 index 0000000..416c54b --- /dev/null +++ b/templates/overrides/brutalist/cta.ninjatpl @@ -0,0 +1,13 @@ +{# brutalist cta override — bordered slab with hard offset. Fields: heading, text, primaryUrl, primaryLabel, secondaryUrl, secondaryLabel, background. #} +
+
+
+{% if heading %}

{{ heading }}

{% endif %} +{% if text %}

{{ text }}

{% endif %} +{% if (primaryUrl and primaryLabel) or (secondaryUrl and secondaryLabel) %}
+{% if primaryUrl and primaryLabel %}{{ primaryLabel }}{% endif %} +{% if secondaryUrl and secondaryLabel %}{{ secondaryLabel }}{% endif %} +
{% endif %} +
+
+
diff --git a/templates/overrides/brutalist/divider.ninjatpl b/templates/overrides/brutalist/divider.ninjatpl new file mode 100644 index 0000000..a6db714 --- /dev/null +++ b/templates/overrides/brutalist/divider.ninjatpl @@ -0,0 +1,2 @@ +{# brutalist divider override — hard rule or icon ornament. Fields: variant, weight, width, icon. #} +{% with variant=variant|default:"line" weight=weight|default:"thick" width=width|default:"full" %}{% if variant == "ornament" %}{% else %}{% endif %}{% endwith %} diff --git a/templates/overrides/brutalist/event-list.ninjatpl b/templates/overrides/brutalist/event-list.ninjatpl new file mode 100644 index 0000000..bd4fd04 --- /dev/null +++ b/templates/overrides/brutalist/event-list.ninjatpl @@ -0,0 +1,41 @@ +{# brutalist event-list override — bordered event slabs (cards) or hard-ruled rows (list). Fields: heading, intro, layout, hidePast, events[] (past, media, datetime, start, end, timezone, title, venue, description, link, linkLabel). Keeps builtin hidePast guards. #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} + {% if layout == "cards" %} +
+ {% for event in events %}{% if not (hidePast and event.past) %} +
+ {% if event.media %}
{% img event.media alt=event.title class="h-full w-full object-cover" %}
{% endif %} +
+
{{ event.start }}{% if event.end %} – {{ event.end }}{% endif %}{% if event.timezone %} {{ event.timezone }}{% endif %}
+

{{ event.title }}

+ {% if event.venue %}

{{ event.venue }}

{% endif %} + {% if event.description %}

{{ event.description }}

{% endif %} + {% if event.link %}{{ event.linkLabel|default:"Details" }} →{% endif %} +
+
+ {% endif %}{% endfor %} +
+ {% else %} +
    + {% for event in events %}{% if not (hidePast and event.past) %} +
  • +
    +
    {{ event.start }}
    + {% if event.end %}
    to {{ event.end }}
    {% endif %} + {% if event.timezone %}
    {{ event.timezone }}
    {% endif %} +
    +
    +

    {{ event.title }}

    + {% if event.venue %}

    {{ event.venue }}

    {% endif %} + {% if event.description %}

    {{ event.description }}

    {% endif %} +
    + {% if event.link %}{{ event.linkLabel|default:"Details" }}{% endif %} +
  • + {% endif %}{% endfor %} +
+ {% endif %} +
+
diff --git a/templates/overrides/brutalist/faq.ninjatpl b/templates/overrides/brutalist/faq.ninjatpl new file mode 100644 index 0000000..d0c80fd --- /dev/null +++ b/templates/overrides/brutalist/faq.ninjatpl @@ -0,0 +1,18 @@ +{# brutalist faq override — bordered accordion, native
mechanism kept. Fields: heading, intro, variant, items[] (question, answer). #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for item in items %} +
+ + {{ item.question }} + + +
{{ item.answer|safe }}
+
+ {% endfor %} +
+
+
diff --git a/templates/overrides/brutalist/feature-grid.ninjatpl b/templates/overrides/brutalist/feature-grid.ninjatpl new file mode 100644 index 0000000..b6281a4 --- /dev/null +++ b/templates/overrides/brutalist/feature-grid.ninjatpl @@ -0,0 +1,18 @@ +{# brutalist feature-grid override — bordered slabs with hard offset shadow. Fields: eyebrow, heading, intro, columns, layout, items[] (icon, title, text, linkUrl, linkLabel). Keeps builtin icon split + literal conditional column classes. #} +
+
+ {% if eyebrow %}{{ eyebrow }}{% endif %} + {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for item in items %} +
+ {% if item.icon %}
{% endif %} +

{% if item.linkUrl %}{{ item.title }}{% else %}{{ item.title }}{% endif %}

+ {% if item.text %}

{{ item.text }}

{% endif %} + {% if item.linkUrl and item.linkLabel %}{{ item.linkLabel }} →{% endif %} +
+ {% endfor %} +
+
+
diff --git a/templates/overrides/brutalist/featured-posts.ninjatpl b/templates/overrides/brutalist/featured-posts.ninjatpl new file mode 100644 index 0000000..6899f1b --- /dev/null +++ b/templates/overrides/brutalist/featured-posts.ninjatpl @@ -0,0 +1,22 @@ +{# brutalist featured-posts override — grid of bordered slab cards from the `posts` provider. Fields: heading, columns, posts[], showFeaturedImage, showExcerpt, showAuthor, showDate. #} +
+
+{% if heading %}

{{ heading }}

{% endif %} +
+{% for post in posts %} +
+{% if showFeaturedImage and post.featured_image_url %}{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}{% endif %} +
+

{{ post.title }}

+{% if showExcerpt and post.excerpt %}

{{ post.excerpt }}

{% endif %} +
+{% if showAuthor and post.author_name %}{{ post.author_name }}{% endif %} +{% if showDate and post.published_at %}{{ post.published_at|date:"M j, Y" }}{% endif %} +{% if post.reading_time %}{{ post.reading_time }} min read{% endif %} +
+
+
+{% endfor %} +
+
+
diff --git a/templates/overrides/brutalist/footer.ninjatpl b/templates/overrides/brutalist/footer.ninjatpl new file mode 100644 index 0000000..41f1788 --- /dev/null +++ b/templates/overrides/brutalist/footer.ninjatpl @@ -0,0 +1,28 @@ +{# brutalist footer override — colophon. Thick top rule, mono uppercase columns, copyright stamp. Fields: layout, social_items[], showNewsletter, newsletter*, columns[], legal_items[], copyright, companyName, logo, tagline, address, email, phone, fullWidth, backgroundColor. #} +{% set fl = layout|default:"columns" %} +{% macro sociallinks(cls) %}{% if social_items %}{% endif %}{% endmacro %} +{% macro newsletter() %}{% if showNewsletter %}

{{ newsletterTitle|default:"Join the list" }}

{% if newsletterDescription %}

{{ newsletterDescription }}

{% endif %}
{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}
{% endif %}{% endmacro %} +{% macro legalbar() %}

© {% now "2006" %} {% if copyright %}{{ copyright }}{% else %}{{ companyName }}{% endif %}

{% if legal_items %}{% endif %}
{% endmacro %} +
+
+{% if fl == "minimal" %} +
+
{% if logo %}{{ companyName }}{% elif companyName %}{{ companyName }}{% endif %}{% if tagline %}{% endif %}
+{{ sociallinks("") }} +
+{{ legalbar() }} +{% else %} +
+
+{% if logo %}{{ companyName }}{% elif companyName %}{{ companyName }}{% endif %} +{% if tagline %}

{{ tagline }}

{% endif %} +{% if address or email or phone %}
{% if address %}
{{ address }}
{% endif %}{% if email %}{% endif %}{% if phone %}{% endif %}
{% endif %} +{{ sociallinks("mt-5") }} +
+{% for col in columns %}

{{ col.header }}

{% endfor %} +{% if showNewsletter %}
{{ newsletter() }}
{% endif %} +
+{{ legalbar() }} +{% endif %} +
+
diff --git a/templates/overrides/brutalist/gallery.ninjatpl b/templates/overrides/brutalist/gallery.ninjatpl new file mode 100644 index 0000000..566cbd8 --- /dev/null +++ b/templates/overrides/brutalist/gallery.ninjatpl @@ -0,0 +1,52 @@ +{# brutalist gallery override — hard-bordered tiles + dependency-free lightbox. Fields: images[.src,.alt,.caption], layout, columns, gap, lightbox. #} +{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %}{% if lightbox %}{% endif %}{% endwith %} diff --git a/templates/overrides/brutalist/heading.ninjatpl b/templates/overrides/brutalist/heading.ninjatpl index 4088b13..4287b4b 100644 --- a/templates/overrides/brutalist/heading.ninjatpl +++ b/templates/overrides/brutalist/heading.ninjatpl @@ -1 +1,7 @@ -{% if level|integer == 1 %}

{{ text }}

{% elif level|integer == 3 %}

{{ text }}

{% elif level|integer == 4 %}

{{ text }}

{% elif level|integer == 5 %}
{{ text }}
{% elif level|integer == 6 %}
{{ text }}
{% else %}

{{ text }}

{% endif %} +{# brutalist heading override — oversized uppercase display type. Fields: level, text, textClass. #} +{% if level == 1 %}

{{ text }}

+{% elif level == 3 %}

{{ text }}

+{% elif level == 4 %}

{{ text }}

+{% elif level == 5 %}
{{ text }}
+{% elif level == 6 %}
{{ text }}
+{% else %}

{{ text }}

{% endif %} diff --git a/templates/overrides/brutalist/hero.ninjatpl b/templates/overrides/brutalist/hero.ninjatpl new file mode 100644 index 0000000..9e856c7 --- /dev/null +++ b/templates/overrides/brutalist/hero.ninjatpl @@ -0,0 +1,24 @@ +{# brutalist hero override — oversized display headline (the studio masthead identity). Styled-text fields accept {text} object OR bare string. Fields: eyebrow, headline, subheadline, description, ctaPrimary{.text,.url}, ctaSecondary{.text,.url}, template, backgroundImage. {% img %} takes no nested {% if %} so the bg is one unconditional call. #} +{% set eyebrowText = eyebrow.text|default:eyebrow %} +{% set headlineText = headline.text|default:headline %} +{% set subText = subheadline.text|default:subheadline %} +{% set descText = description.text|default:description %} +{% set pText = ctaPrimary.text %} +{% set pUrl = ctaPrimary.url %} +{% set sText = ctaSecondary.text %} +{% set sUrl = ctaSecondary.url %} +{% set variant = template|default:"centered" %} +{% set hasImage = backgroundImage %} +
+{% if hasImage %}
{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}
{% endif %} +
+
+{% if eyebrowText %}{{ eyebrowText }}{% endif %} +{% if headlineText %}

{{ headlineText }}

{% endif %} +{% if subText %}

{{ subText }}

{% endif %} +{% if descText %}

{{ descText }}

{% endif %} +{% if pText or sText %}
{% if pText and pUrl %}{{ pText }}{% endif %}{% if sText and sUrl %}{{ sText }}{% endif %}
{% endif %} +{% if trustLogos %}

Trusted by

{% for logo in trustLogos %}{% if logo.url %}{% img logo.image alt=logo.alt class="h-8 w-auto" %}{% else %}{% img logo.image alt=logo.alt class="h-8 w-auto opacity-70" %}{% endif %}{% endfor %}
{% endif %} +
+
+
diff --git a/templates/overrides/brutalist/hours-location.ninjatpl b/templates/overrides/brutalist/hours-location.ninjatpl new file mode 100644 index 0000000..4e7fe85 --- /dev/null +++ b/templates/overrides/brutalist/hours-location.ninjatpl @@ -0,0 +1,31 @@ +{# brutalist hours-location override — bordered hours table + lazy map. Fields: heading, hours[] (day, closed, hours), specialHoursNote, address, phone, directionsUrl, mapEmbedUrl, mapButtonLabel. Keeps map button's inline onclick verbatim. Builtin uses fixed lucide icons (no icon split field). #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} +
+
+ {% if hours %} +
+ + {% for row in hours %} + + + + + {% endfor %} + +
{{ row.day }}{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}
+ {% endif %} + {% if specialHoursNote %}

{{ specialHoursNote }}

{% endif %} + {% if address %}
Address
{{ address }}
{% endif %} + {% if phone %}{% endif %} + {% if directionsUrl %}Get directions{% endif %} + + {% if mapEmbedUrl %} +
+ +
+ {% endif %} + + + diff --git a/templates/overrides/brutalist/image.ninjatpl b/templates/overrides/brutalist/image.ninjatpl index 5e3b029..6e75b61 100644 --- a/templates/overrides/brutalist/image.ninjatpl +++ b/templates/overrides/brutalist/image.ninjatpl @@ -1 +1,2 @@ -
{% if src %}{{ alt }}{% elif url %}{{ alt }}{% endif %}{% if caption %}
{{ caption }}
{% endif %}
+{# brutalist image override — hard-bordered figure, mono caption. Fields: src, alt, size, align, aspectRatio, objectFit, caption. Rounding is killed page-wide. #} +{% with size=size|default:"full" align=align|default:"center" aspectRatio=aspectRatio|default:"auto" objectFit=objectFit|default:"cover" %}
{% if objectFit == "contain" %}{% img src alt=alt class="w-full h-full object-contain" layout=size %}{% elif objectFit == "fill" %}{% img src alt=alt class="w-full h-full object-fill" layout=size %}{% else %}{% img src alt=alt class="w-full h-full object-cover" layout=size %}{% endif %}
{% if caption %}
{{ caption }}
{% endif %}
{% endwith %} diff --git a/templates/overrides/brutalist/logo-strip.ninjatpl b/templates/overrides/brutalist/logo-strip.ninjatpl new file mode 100644 index 0000000..ceecdb9 --- /dev/null +++ b/templates/overrides/brutalist/logo-strip.ninjatpl @@ -0,0 +1,16 @@ +{# brutalist logo-strip override — mono heading + optional marquee, keyframe kept verbatim. Fields: heading, marquee, grayscale, logos[] (url, image, alt). #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} +
+
+ {% for logo in logos %} + {% if logo.url %}{% endif %} + {% img logo.image alt=logo.alt class="h-8 w-auto object-contain" %} + {% if logo.url %}{% endif %} + {% endfor %} +
+
+
+
+{% if marquee %}{% endif %} diff --git a/templates/overrides/brutalist/menu-list.ninjatpl b/templates/overrides/brutalist/menu-list.ninjatpl new file mode 100644 index 0000000..b60a03c --- /dev/null +++ b/templates/overrides/brutalist/menu-list.ninjatpl @@ -0,0 +1,30 @@ +{# brutalist menu-list override — bordered menu sections, mono price. Fields: heading, intro, columns, currency, footnote, sections[] (title, note, items[] {name, badges[] {label}, description, price}). Builtin has no icon field so no icon split here. #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for section in sections %} + + {% endfor %} +
+ {% if footnote %}

{{ footnote }}

{% endif %} +
+
diff --git a/templates/overrides/brutalist/navbar.ninjatpl b/templates/overrides/brutalist/navbar.ninjatpl new file mode 100644 index 0000000..6902819 --- /dev/null +++ b/templates/overrides/brutalist/navbar.ninjatpl @@ -0,0 +1,64 @@ +{# brutalist navbar override — thick-ruled bar, uppercase wordmark, mono links, off-canvas drawer. One navbar per page (fixed drawer ids). Fields: companyName, logo, logoType, logoText.text, alt, menu.items[], cta.text|ctaLabel, cta.url|ctaUrl, stickyBehavior, sticky, showUtilityBar, utilityBar*, class. #} +{% set sb = stickyBehavior %}{% if not sb %}{% if sticky %}{% set sb = "sticky" %}{% else %}{% set sb = "none" %}{% endif %}{% endif %} +{% set cta_text = cta.text|default:ctaLabel %} +{% set cta_url = cta.url|default:ctaUrl %} +{% macro brand() %}{% if logoType == "text" and logoText.text %}{{ logoText.text }}{% elif logo %}{{ alt|default:companyName }}{% else %}{{ companyName|default:"STUDIO" }}{% endif %}{% endmacro %} +{% macro navlink(item) %}{{ item.label }}{% endmacro %} +{% macro dropdown(item) %}
{% endmacro %} +{% macro ctabtn(extra) %}{% if cta_text %}{{ cta_text }}{% endif %}{% endmacro %} + + + diff --git a/templates/overrides/brutalist/newsletter-signup.ninjatpl b/templates/overrides/brutalist/newsletter-signup.ninjatpl new file mode 100644 index 0000000..c91c4df --- /dev/null +++ b/templates/overrides/brutalist/newsletter-signup.ninjatpl @@ -0,0 +1,10 @@ +{# brutalist newsletter-signup override — the {% signup_form %} tag carries the untouchable subscribe wiring; this template only supplies brutalist chrome. Fields: layout, title, description, listSlug, source, submitText, successMessage, placeholder. #} +
+
+
+{% if title %}

{{ title }}

{% endif %} +{% if description %}

{{ description }}

{% endif %} +
{% signup_form list=listSlug source=source submit=submitText success_message=successMessage placeholder=placeholder %}
+
+
+
diff --git a/templates/overrides/brutalist/page-suggestions.ninjatpl b/templates/overrides/brutalist/page-suggestions.ninjatpl new file mode 100644 index 0000000..2f00748 --- /dev/null +++ b/templates/overrides/brutalist/page-suggestions.ninjatpl @@ -0,0 +1,20 @@ +{# brutalist page-suggestions override — themed 404. Fields: title, emptyMessage, showDescription. NOTE: the builtin computes `suggestions` via a server-side SearchSimilarPages DB query keyed on the requested path; that data is NOT exposed to codeless templates, so the dynamic suggestion list cannot be reproduced here (WO limitation). The `{% for %}` below renders defensively if the host ever supplies `suggestions`; otherwise the themed 404 chrome + a way home is shown. #} +
+
+

404

+

{{ title|default:"Page not found" }}

+ {% if suggestions %} + + {% elif emptyMessage %} +

{{ emptyMessage }}

+ {% endif %} + +
+
diff --git a/templates/overrides/brutalist/password-reset-form.ninjatpl b/templates/overrides/brutalist/password-reset-form.ninjatpl new file mode 100644 index 0000000..85e7292 --- /dev/null +++ b/templates/overrides/brutalist/password-reset-form.ninjatpl @@ -0,0 +1,13 @@ +{# brutalist password-reset-form override — request-reset form. HTMX endpoint + input name load-bearing; kept exact. #} +
+
+
+

Reset Password

+

Enter your email address and we will send you a link to reset your password.

+
+
+ +
+

Back to login

+
+
diff --git a/templates/overrides/brutalist/popular-posts.ninjatpl b/templates/overrides/brutalist/popular-posts.ninjatpl new file mode 100644 index 0000000..47492eb --- /dev/null +++ b/templates/overrides/brutalist/popular-posts.ninjatpl @@ -0,0 +1,16 @@ +{# brutalist popular-posts override — compact ranked list from the `posts` provider. Fields: heading, posts[], showRank, showThumbnail. #} +
+{% if heading %}

{{ heading }}

{% endif %} +
    +{% for post in posts %} +
  1. +{% if showRank %}{{ forloop.Counter }}{% endif %} +{% if showThumbnail and post.featured_image_url %}{% img post.featured_image_url alt=post.title class="h-full w-full object-cover" %}{% endif %} +
    +

    {{ post.title }}

    +{% if post.published_at %}

    {{ post.published_at|date:"M j, Y" }}

    {% endif %} +
    +
  2. +{% endfor %} +
+
diff --git a/templates/overrides/brutalist/post-hero.ninjatpl b/templates/overrides/brutalist/post-hero.ninjatpl new file mode 100644 index 0000000..246de7a --- /dev/null +++ b/templates/overrides/brutalist/post-hero.ninjatpl @@ -0,0 +1,20 @@ +{# brutalist post-hero override — current post from the `post` provider. Featured image = overlaid banner; otherwise clean text hero. Fields: align, post.*, showExcerpt, showMeta. #} +{% set center = align != "left" %} +{% if post.featured_image_url %} +
+
{% img post.featured_image_url alt=post.featured_image_alt class="h-full w-full object-cover" layout="hero" %}
+
+

{{ post.title }}

+{% if showExcerpt and post.excerpt %}

{{ post.excerpt }}

{% endif %} +{% if showMeta %}
{% if post.author_name %}{{ post.author_name }}{% endif %}{% if post.published_at %}{{ post.published_at|date:"F j, Y" }}{% endif %}{% if post.reading_time %}{{ post.reading_time }} min read{% endif %}
{% endif %} +
+
+{% else %} +
+
+

{{ post.title }}

+{% if showExcerpt and post.excerpt %}

{{ post.excerpt }}

{% endif %} +{% if showMeta %}
{% if post.author_name %}{{ post.author_name }}{% endif %}{% if post.published_at %}{{ post.published_at|date:"F j, Y" }}{% endif %}{% if post.reading_time %}{{ post.reading_time }} min read{% endif %}
{% endif %} +
+
+{% endif %} diff --git a/templates/overrides/brutalist/post-metadata.ninjatpl b/templates/overrides/brutalist/post-metadata.ninjatpl new file mode 100644 index 0000000..7da5003 --- /dev/null +++ b/templates/overrides/brutalist/post-metadata.ninjatpl @@ -0,0 +1,9 @@ +{# brutalist post-metadata override — author/date/reading-time row from the `post` provider. Fields: showAuthor, showAvatar, showDate, showReadingTime, post.*, post.categories[]. #} +{% if post %} +
+{% if showAuthor and post.author_name %}{% if showAvatar and post.author_avatar %}{% img post.author_avatar alt=post.author_name class="h-7 w-7 object-cover" %}{% endif %}{{ post.author_name }}{% endif %} +{% if showDate and post.published_at %}{% endif %} +{% if showReadingTime and post.reading_time %}{{ post.reading_time }} min read{% endif %} +{% if post.categories %}{% for cat in post.categories %}{{ cat.name }}{% endfor %}{% endif %} +
+{% endif %} diff --git a/templates/overrides/brutalist/pricing.ninjatpl b/templates/overrides/brutalist/pricing.ninjatpl new file mode 100644 index 0000000..552541a --- /dev/null +++ b/templates/overrides/brutalist/pricing.ninjatpl @@ -0,0 +1,25 @@ +{# brutalist pricing override — bordered tier slabs, highlighted tier gets accent band. Fields: heading, intro, currency, billingNote, tiers[] (highlighted, badge, name, price, period, description, features[], ctaUrl, ctaLabel). #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for tier in tiers %} +
+ {% if tier.badge %}{{ tier.badge }}{% endif %} +

{{ tier.name }}

+

+ {% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }} + {% if tier.period %}{{ tier.period }}{% endif %} +

+ {% if tier.description %}

{{ tier.description }}

{% endif %} + {% if tier.features %}
    + {% for feature in tier.features %}
  • {{ feature }}
  • {% endfor %} +
{% endif %} + {% if tier.ctaUrl and tier.ctaLabel %}{{ tier.ctaLabel }}{% endif %} +
+ {% endfor %} +
+ {% if billingNote %}

{{ billingNote }}

{% endif %} +
+
diff --git a/templates/overrides/brutalist/quote.ninjatpl b/templates/overrides/brutalist/quote.ninjatpl new file mode 100644 index 0000000..c66a61a --- /dev/null +++ b/templates/overrides/brutalist/quote.ninjatpl @@ -0,0 +1,2 @@ +{# brutalist quote override — thick accent rule or bordered slab. Fields: quote, cite, attribution, role, style, align. #} +{% with style=style|default:"block" align=align|default:"left" %}
{{ quote|safe }}{% if attribution or role or cite %}
{% if attribution %}{{ attribution }}{% endif %}{% if role %}{{ role }}{% endif %}{% if cite %}Source{% endif %}
{% endif %}
{% endwith %} diff --git a/templates/overrides/brutalist/related-posts.ninjatpl b/templates/overrides/brutalist/related-posts.ninjatpl new file mode 100644 index 0000000..9379443 --- /dev/null +++ b/templates/overrides/brutalist/related-posts.ninjatpl @@ -0,0 +1,19 @@ +{# brutalist related-posts override — grid from the `posts` provider, thick top rule. Fields: heading, columns, posts[]. #} +{% if posts %} +
+
+

{{ heading|default:"Related posts" }}

+
+{% for post in posts %} + +{% endfor %} +
+
+
+{% endif %} diff --git a/templates/overrides/brutalist/rich-section.ninjatpl b/templates/overrides/brutalist/rich-section.ninjatpl new file mode 100644 index 0000000..c99f54b --- /dev/null +++ b/templates/overrides/brutalist/rich-section.ninjatpl @@ -0,0 +1,14 @@ +{# brutalist rich-section override — split text/media with bordered media frame. Fields: background, mediaPosition, eyebrow, heading, body, ctaUrl, ctaLabel, media, mediaAlt. #} +
+
+
+ {% if eyebrow %}{{ eyebrow }}{% endif %} + {% if heading %}

{{ heading }}

{% endif %} + {% if body %}
{{ body|safe }}
{% endif %} + {% if ctaUrl and ctaLabel %}{{ ctaLabel }} →{% endif %} +
+
+ {% if media %}
{% img media alt=mediaAlt class="w-full h-auto object-cover" %}
{% endif %} +
+
+
diff --git a/templates/overrides/brutalist/rich-text.ninjatpl b/templates/overrides/brutalist/rich-text.ninjatpl new file mode 100644 index 0000000..87f2ac2 --- /dev/null +++ b/templates/overrides/brutalist/rich-text.ninjatpl @@ -0,0 +1,2 @@ +{# brutalist rich-text override — imported HTML content. Field: html. #} +{% if html %}
{{ html|safe }}
{% endif %} diff --git a/templates/overrides/brutalist/sidebar-nav.ninjatpl b/templates/overrides/brutalist/sidebar-nav.ninjatpl new file mode 100644 index 0000000..7793514 --- /dev/null +++ b/templates/overrides/brutalist/sidebar-nav.ninjatpl @@ -0,0 +1,13 @@ +{# brutalist sidebar-nav override — vertical menu from the `menus` provider. Variants default | compact. highlight-current script kept verbatim (toggles text-muted-foreground → text-foreground/bg-accent on data-bn-sidebar-link). Fields: template, showBorder, title, menu.items[], highlightCurrent. #} +{% set compact = template == "compact" %} + +{% if highlightCurrent %}{% endif %} diff --git a/templates/overrides/brutalist/social-links.ninjatpl b/templates/overrides/brutalist/social-links.ninjatpl new file mode 100644 index 0000000..f646151 --- /dev/null +++ b/templates/overrides/brutalist/social-links.ninjatpl @@ -0,0 +1,14 @@ +{# brutalist social-links override — bordered square chips (widget, not a full section). Fields: heading, style, links[] (url, label, network). Keeps builtin icon split on network + literal conditional style classes. #} + diff --git a/templates/overrides/brutalist/stats.ninjatpl b/templates/overrides/brutalist/stats.ninjatpl new file mode 100644 index 0000000..a4932b0 --- /dev/null +++ b/templates/overrides/brutalist/stats.ninjatpl @@ -0,0 +1,49 @@ +{# brutalist stats override — bordered stat tiles with hard offset, count-up script kept verbatim. Fields: heading, intro, columns, countUp, items[] (prefix, value, suffix, label). #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for item in items %} +
+
{% if item.prefix %}{{ item.prefix }}{% endif %}{{ item.value }}{% if item.suffix %}{{ item.suffix }}{% endif %}
+ {% if item.label %}
{{ item.label }}
{% endif %} +
+ {% endfor %} +
+
+
+{% if countUp %}{% endif %} diff --git a/templates/overrides/brutalist/team.ninjatpl b/templates/overrides/brutalist/team.ninjatpl new file mode 100644 index 0000000..145b651 --- /dev/null +++ b/templates/overrides/brutalist/team.ninjatpl @@ -0,0 +1,22 @@ +{# brutalist team override — bordered member slabs. Fields: heading, intro, layout, columns, members[] (photo, name, role, bio, socials[] {url, icon}). Keeps builtin icon split + literal conditional column classes. #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for member in members %} +
+ {% if member.photo %}{% img member.photo alt=member.name class="h-full w-full object-cover" %}{% endif %} +
+ {% if member.name %}

{{ member.name }}

{% endif %} + {% if member.role %}

{{ member.role }}

{% endif %} + {% if member.bio %}

{{ member.bio }}

{% endif %} + {% if member.socials %}
+ {% for social in member.socials %}{% if social.url %}{% endif %}{% endfor %} +
{% endif %} +
+
+ {% endfor %} +
+
+
diff --git a/templates/overrides/brutalist/testimonial.ninjatpl b/templates/overrides/brutalist/testimonial.ninjatpl new file mode 100644 index 0000000..1ab2bf7 --- /dev/null +++ b/templates/overrides/brutalist/testimonial.ninjatpl @@ -0,0 +1,27 @@ +{# brutalist testimonial override — bordered quote slabs with hard offset. Fields: heading, layout, quotes[] (rating, quote, avatar, name, role). Keeps builtin's 5 literal rating-star comparisons. #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} +
+ {% for q in quotes %} +
+ {% if q.rating %}
+ + + + + +
{% endif %} +
{% if q.quote %}“{{ q.quote }}”{% endif %}
+
+ {% if q.avatar %}{% img q.avatar alt=q.name class="h-full w-full object-cover" %}{% endif %} + + {% if q.name %}{{ q.name }}{% endif %} + {% if q.role %}{{ q.role }}{% endif %} + +
+
+ {% endfor %} +
+
+
diff --git a/templates/overrides/brutalist/text.ninjatpl b/templates/overrides/brutalist/text.ninjatpl index a5b0597..883be08 100644 --- a/templates/overrides/brutalist/text.ninjatpl +++ b/templates/overrides/brutalist/text.ninjatpl @@ -1 +1,2 @@ -
{{ text|safe }}
+{# brutalist text override — body copy, generous measure. Fields: text, proseClass. #} +
{{ text|safe }}
diff --git a/templates/overrides/brutalist/timeline.ninjatpl b/templates/overrides/brutalist/timeline.ninjatpl new file mode 100644 index 0000000..ca00d11 --- /dev/null +++ b/templates/overrides/brutalist/timeline.ninjatpl @@ -0,0 +1,22 @@ +{# brutalist timeline override — square step markers + hard rule. Fields: heading, intro, orientation, marker, steps[] (label, title, text, media). Keeps forloop.counter, marker enum branch, and literal conditional orientation classes. #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
    + {% for step in steps %} +
  1. + + {% if marker == "numbered" %}{{ forloop.counter }}{% elif marker == "dot" %}{% else %}{% endif %} + +
    + {% if step.label %}

    {{ step.label }}

    {% endif %} + {% if step.title %}

    {{ step.title }}

    {% endif %} + {% if step.text %}

    {{ step.text }}

    {% endif %} + {% if step.media %}
    {% img step.media alt=step.title class="w-full h-auto object-cover" %}
    {% endif %} +
    +
  2. + {% endfor %} +
+
+
diff --git a/templates/overrides/brutalist/utility-bar.ninjatpl b/templates/overrides/brutalist/utility-bar.ninjatpl new file mode 100644 index 0000000..56c39e5 --- /dev/null +++ b/templates/overrides/brutalist/utility-bar.ninjatpl @@ -0,0 +1,10 @@ +{# brutalist utility-bar override — mono uppercase meta strip. Fields: leftItems[], rightItems[] (.icon,.label,.link), variant, showOnMobile. #} +{% set ubstyle = "background:hsl(var(--background));color:hsl(var(--muted-foreground));border-bottom:1px solid hsl(var(--border))" %} +{% if variant == "primary" %}{% set ubstyle = "background:hsl(var(--accent));color:hsl(var(--accent-foreground))" %}{% elif variant == "dark" %}{% set ubstyle = "background:hsl(var(--foreground));color:hsl(var(--background))" %}{% endif %} +{% macro ubitem(it) %}{% if it.link %}{% if it.icon %}::{{ it.icon }}:sm::{% endif %}{% if it.label %}{{ it.label }}{% endif %}{% else %}{% if it.icon %}::{{ it.icon }}:sm::{% endif %}{% if it.label %}{{ it.label }}{% endif %}{% endif %}{% endmacro %} +
+
+
{% for it in leftItems %}{{ ubitem(it) }}{% endfor %}
+
{% for it in rightItems %}{{ ubitem(it) }}{% endfor %}
+
+
diff --git a/templates/overrides/brutalist/video-embed.ninjatpl b/templates/overrides/brutalist/video-embed.ninjatpl new file mode 100644 index 0000000..a15670f --- /dev/null +++ b/templates/overrides/brutalist/video-embed.ninjatpl @@ -0,0 +1,23 @@ +{# brutalist video-embed override — hard-bordered click-to-play facade. Fields: url, media, poster, title, caption, aspect. #} +{% with aspect=aspect|default:"16:9" %}
{% if caption %}
{{ caption }}
{% endif %}
{% endwith %}