feat(brutalist): Wave A override contract — 49 builtin overrides, 7 templates, seed
WO-TF-012. Neo-brutalist creative-studio theme brought up to the Wave A OVERRIDE-CONTRACT (frozen against cms a0f07e2ae). - Override all 49 mandatory builtin keys in templates/overrides/brutalist/ (chrome, marketing, primitives, commerce, blog family, auth surface, 404), each carrying the brutalist chrome (chunky borders, hard offset shadows, hover snap, mono uppercase meta, candy accents) via a raw-CSS "bru-*" kit shipped in manifest.yaml. Load-bearing wiring (HTMX auth endpoints, signup form, contact-form submit + honeypot, facade scripts, lightbox) preserved verbatim. - 7 page templates: default, full-width, landing, article, blog-index, contact, auth (fixed a bug where templates omitted the `dark` class so dark presets never activated). - Blocks rework: removed 6 blocks that duplicated builtins (masthead, concrete_hero, meta_strip, caption_image, pull_quote, colophon) — now overrides; kept project_ledger; added persona blocks sticker_wall, changelog, spec_sheet (4 total). - 4 dual-mode presets (all mode "both", 19 tokens light + dark): Concrete & Cadmium, Hazard, Riso, Acid. - Bundled 8 latin-subset OFL woff2 (Space Grotesk, Archivo Black, Inter, IBM Plex Mono) in assets/fonts/ + LICENSES.md; fonts.json populated; all font-family via var(--font-*). RECOMMENDED_FONTS/ICONS updated; required_icon_packs declared. - Email-safe themed wrapper (tables, inline styles) cleaned up. - master_pages.json reworked to builtin navbar/hero/cta/footer. - Demo seed (seed/seed.json + workflows.json, demo:true): home, about, blog (3 posts), contact (form + contact_submissions data table + row_inserted → email-admins workflow), login — studio voice. Known codeless limitation: page-suggestions renders themed 404 chrome but the similar-pages list (server-side DB query, no template provider) can't be reproduced; auth-form captcha widget is server-generated and can't be emitted (seed leaves captchaEnabled=false). Verified: make exit 0; check-safety exit 0. Visual quality UNVERIFIED until Wave B. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
979516b5e9
commit
6c03f6da6c
@ -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, <fallback>)`, 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.
|
||||
|
||||
21
RECOMMENDED_ICONS.md
Normal file
21
RECOMMENDED_ICONS.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Recommended icons — Brutalist
|
||||
|
||||
This theme renders icons through the sprite pipeline
|
||||
(`<svg><use href="/icons/<pack>.svg#<name>"></use></svg>`). Two packs are used by
|
||||
the theme's block overrides and are declared in `plugin.mod`
|
||||
(`required_icon_packs = ["lucide", "simple-icons"]`).
|
||||
|
||||
Until the loader auto-installs declared packs, install them manually before
|
||||
activation:
|
||||
|
||||
1. Open `/admin/icons`.
|
||||
2. Install **Lucide** (bundled — usually already present). Used for UI chrome:
|
||||
`menu`, `x`, `chevron-down`, `chevron-left`, `chevron-right`, `arrow-right`,
|
||||
`play`, `download`, `check`, `star`, `navigation`, `map-pin`, `flag`, `globe`,
|
||||
`asterisk`.
|
||||
3. Install **Simple Icons** (bundled). Used by the author-bio-hero social row:
|
||||
`x`, `linkedin`, `github`.
|
||||
|
||||
`lucide` is the safe default for any icon-picker field. Utility-bar and footer
|
||||
social icons render whatever `pack:name` string the admin assigns, so they work
|
||||
with any installed pack.
|
||||
16
assets/fonts/LICENSES.md
Normal file
16
assets/fonts/LICENSES.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Bundled font licenses
|
||||
|
||||
All bundled woff2 files in this directory are latin-subset builds redistributed
|
||||
under the SIL Open Font License 1.1 (OFL). No modifications were made beyond
|
||||
subsetting to the latin range.
|
||||
|
||||
| Family | Files | License | Source |
|
||||
|--------|-------|---------|--------|
|
||||
| Space Grotesk | space-grotesk-500.woff2, space-grotesk-700.woff2 | SIL OFL 1.1 | Florian Karsten / Google Fonts |
|
||||
| Archivo Black | archivo-black-400.woff2 | SIL OFL 1.1 | Omnibus-Type / Google Fonts |
|
||||
| Inter | inter-400.woff2, inter-500.woff2, inter-700.woff2 | SIL OFL 1.1 | Rasmus Andersson / Google Fonts |
|
||||
| IBM Plex Mono | ibm-plex-mono-400.woff2, ibm-plex-mono-500.woff2 | SIL OFL 1.1 | IBM / Google Fonts |
|
||||
|
||||
Full OFL text: https://openfontlicense.org/. Font-family is still admin-controlled;
|
||||
these bundled families appear as `source=template` rows in the fonts picker and
|
||||
are consumed only via `var(--font-heading|body|mono, <fallback>)`.
|
||||
BIN
assets/fonts/archivo-black-400.woff2
Normal file
BIN
assets/fonts/archivo-black-400.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/ibm-plex-mono-400.woff2
Normal file
BIN
assets/fonts/ibm-plex-mono-400.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/ibm-plex-mono-500.woff2
Normal file
BIN
assets/fonts/ibm-plex-mono-500.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/inter-400.woff2
Normal file
BIN
assets/fonts/inter-400.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/inter-500.woff2
Normal file
BIN
assets/fonts/inter-500.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/inter-700.woff2
Normal file
BIN
assets/fonts/inter-700.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/space-grotesk-500.woff2
Normal file
BIN
assets/fonts/space-grotesk-500.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/space-grotesk-700.woff2
Normal file
BIN
assets/fonts/space-grotesk-700.woff2
Normal file
Binary file not shown.
@ -1,48 +1,31 @@
|
||||
# Brutalist theme block definitions (codeless .bnp, WO-WZ-021).
|
||||
# Keys are fully qualified (brutalist:<key>) 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:<key>) because the codeless loader registers
|
||||
# definitions verbatim. Blocks that duplicated builtins (masthead, concrete_hero,
|
||||
# meta_strip, caption_image, pull_quote, colophon) were removed — they are now
|
||||
# template overrides of navbar/hero/utility-bar/image/quote/footer. Only true
|
||||
# persona furniture remains here.
|
||||
blocks:
|
||||
- key: brutalist: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
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<figure class="brutalist-figure" data-block="brutalist:caption_image" style="margin: 2rem 0;">{% if image %}<img src="{{ image }}" alt="{{ caption }}" style="display: block; width: 100%; height: auto;">{% endif %}{% if caption or figureNumber %}<figcaption>{% if figureNumber %}<span style="margin-right: 0.5rem;">{{ figureNumber }}</span>{% endif %}{% if caption %}<span>{{ caption }}</span>{% endif %}</figcaption>{% endif %}</figure>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
blocks/changelog.ninjatpl
Normal file
8
blocks/changelog.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
||||
<section class="bru-section bg-background text-foreground" data-block="brutalist:changelog">
|
||||
<div class="bru-wrap-mid">
|
||||
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
|
||||
<div>
|
||||
{% for entry in entries %}<div class="bru-changelog-row"><div><span class="bru-tag">{{ entry.version }}</span>{% if entry.date %}<div class="bru-meta mt-2">{{ entry.date }}</div>{% endif %}</div><div class="text-foreground">{% if entry.title %}<h3 class="bru-kicker" style="font-size:1.15rem;text-transform:none">{{ entry.title }}</h3>{% endif %}{% if entry.note %}<div class="mt-1 text-muted-foreground">{{ entry.note|safe }}</div>{% endif %}</div></div>{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
51
blocks/changelog.schema.json
Normal file
51
blocks/changelog.schema.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Changelog",
|
||||
"description": "Versioned changelog rows",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heading": {
|
||||
"type": "string",
|
||||
"title": "Heading",
|
||||
"description": "Optional section heading",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"title": "Entries",
|
||||
"description": "Changelog entries, newest first",
|
||||
"default": [],
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"title": "Version",
|
||||
"description": "Version tag e.g. v1.2",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"date": {
|
||||
"type": "string",
|
||||
"title": "Date",
|
||||
"description": "Release date",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Title",
|
||||
"description": "Optional short title for the release",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"note": {
|
||||
"type": "string",
|
||||
"title": "Note",
|
||||
"description": "What changed",
|
||||
"x-editor": "richtext"
|
||||
}
|
||||
},
|
||||
"required": ["version"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
<section class="brutalist-colophon" data-block="brutalist:colophon">{% if address and showAddress != "false" and showAddress != false %}<address style="font-style: normal; white-space: pre-line;">{{ address }}</address>{% endif %}<div style="display: flex; flex-direction: column; gap: 0.25rem;">{% if email %}<a href="mailto:{{ email }}" style="color: inherit; text-decoration: none;">{{ email }}</a>{% endif %}{% for link in social %}<a href="{{ link.url }}" target="_blank" rel="noopener" style="color: inherit; text-decoration: none;">{{ link.label }}</a>{% endfor %}</div><div class="stamp">© {% now "2006" %}</div></section>
|
||||
@ -1,53 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Colophon Footer",
|
||||
"description": "Three-row mono footer with address, email, and social links",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"title": "Address",
|
||||
"description": "Studio postal address",
|
||||
"x-editor": "textarea"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"title": "Contact Email",
|
||||
"description": "Contact email rendered in mono",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"showAddress": {
|
||||
"type": "string",
|
||||
"title": "Show Address",
|
||||
"description": "Toggle the address row on or off",
|
||||
"x-editor": "select",
|
||||
"enum": ["true", "false"],
|
||||
"default": "true"
|
||||
},
|
||||
"social": {
|
||||
"type": "array",
|
||||
"title": "Social Links",
|
||||
"description": "Optional list of social links",
|
||||
"default": [],
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"title": "Label",
|
||||
"description": "Display label",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"title": "URL",
|
||||
"description": "Target URL",
|
||||
"x-editor": "link"
|
||||
}
|
||||
},
|
||||
"required": ["label", "url"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
<section class="brutalist-hero" data-block="brutalist:concrete_hero" data-has-media="{% if media %}true{% else %}false{% endif %}">{% if media %}<div class="media-bg" style="background-image: url(&#39;{{ media }}&#39;);" role="presentation"></div>{% endif %}<div class="inner">{% if eyebrow %}<p class="eyebrow">{{ eyebrow }}</p>{% endif %}<h1 class="headline">{{ headline }}</h1></div></section>
|
||||
@ -1,27 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Concrete Hero",
|
||||
"description": "200pt+ display headline with optional eyebrow and full-bleed image",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"headline": {
|
||||
"type": "string",
|
||||
"title": "Headline",
|
||||
"description": "Oversized display headline",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"eyebrow": {
|
||||
"type": "string",
|
||||
"title": "Eyebrow",
|
||||
"description": "Mono eyebrow line shown above the headline",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"media": {
|
||||
"type": "string",
|
||||
"title": "Background Media",
|
||||
"description": "Optional full-bleed background image",
|
||||
"x-editor": "media"
|
||||
}
|
||||
},
|
||||
"required": ["headline"]
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<section class="brutalist-masthead" data-block="brutalist:masthead">
|
||||
<span class="index-counter brutalist-mono">{{ indexNumber|default:"01 / 14" }}</span>
|
||||
<h1 class="studio-name">{{ studioName|default:"STUDIO" }}</h1>
|
||||
{% if tagline %}<p class="tagline">{{ tagline }}</p>{% endif %}
|
||||
</section>
|
||||
@ -1,29 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Studio Masthead",
|
||||
"description": "Oversized studio wordmark with mono index counter top-right",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"studioName": {
|
||||
"type": "string",
|
||||
"title": "Studio Name",
|
||||
"description": "Wordmark text rendered at display size",
|
||||
"x-editor": "text",
|
||||
"default": "STUDIO"
|
||||
},
|
||||
"tagline": {
|
||||
"type": "string",
|
||||
"title": "Tagline",
|
||||
"description": "Short tagline rendered below the wordmark",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"indexNumber": {
|
||||
"type": "string",
|
||||
"title": "Index Counter",
|
||||
"description": "Mono index counter top-right, e.g. 01 / 14",
|
||||
"x-editor": "text",
|
||||
"default": "01 / 14"
|
||||
}
|
||||
},
|
||||
"required": ["studioName"]
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
<dl class="brutalist-meta-strip" data-block="brutalist:meta_strip">{% for item in items %}<div style="display: flex; flex-direction: column; gap: 0.25rem;"><dt>{{ item.label }}</dt><dd>{{ item.value }}</dd></div>{% endfor %}</dl>
|
||||
@ -1 +0,0 @@
|
||||
<section class="brutalist-pullquote brutalist-grid-12 brutalist-col-span-8" data-block="brutalist:pull_quote"><blockquote>{{ quote|safe }}</blockquote>{% if attribution %}<cite>— {{ attribution }}</cite>{% endif %}</section>
|
||||
@ -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"]
|
||||
}
|
||||
6
blocks/spec_sheet.ninjatpl
Normal file
6
blocks/spec_sheet.ninjatpl
Normal file
@ -0,0 +1,6 @@
|
||||
<section class="bru-section bg-background text-foreground" data-block="brutalist:spec_sheet">
|
||||
<div class="bru-wrap-mid">
|
||||
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
|
||||
<dl>{% for row in rows %}<div class="bru-spec-row"><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div>{% endfor %}</dl>
|
||||
</div>
|
||||
</section>
|
||||
@ -1,13 +1,19 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
8
blocks/sticker_wall.ninjatpl
Normal file
8
blocks/sticker_wall.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
||||
<section class="bru-section bg-background text-foreground" data-block="brutalist:sticker_wall">
|
||||
<div class="bru-wrap">
|
||||
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{% for item in items %}<span class="bru-sticker" style="font-size:{{ item.size|default:"1.1rem" }};{% if item.tone == "ink" %}background:hsl(var(--foreground));color:hsl(var(--background)){% elif item.tone == "paper" %}background:hsl(var(--background));color:hsl(var(--foreground)){% else %}background:hsl(var(--accent));color:hsl(var(--accent-foreground)){% endif %}">{{ item.text }}</span>{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
47
blocks/sticker_wall.schema.json
Normal file
47
blocks/sticker_wall.schema.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Sticker Wall",
|
||||
"description": "Wall of hard-bordered candy stickers",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heading": {
|
||||
"type": "string",
|
||||
"title": "Heading",
|
||||
"description": "Optional section heading",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"title": "Stickers",
|
||||
"description": "Sticker labels in display order",
|
||||
"default": [],
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"title": "Label",
|
||||
"description": "Sticker text",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"tone": {
|
||||
"type": "string",
|
||||
"title": "Tone",
|
||||
"description": "Sticker colour",
|
||||
"default": "accent",
|
||||
"enum": ["accent", "ink", "paper"],
|
||||
"x-editor": "select"
|
||||
},
|
||||
"size": {
|
||||
"type": "string",
|
||||
"title": "Size",
|
||||
"description": "Optional font-size (e.g. 1.5rem)",
|
||||
"x-editor": "text"
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
fonts.json
35
fonts.json
@ -1 +1,34 @@
|
||||
[]
|
||||
[
|
||||
{
|
||||
"name": "Space Grotesk",
|
||||
"family": "Space Grotesk",
|
||||
"variants": [
|
||||
{ "weight": "500", "style": "normal", "file": "fonts/space-grotesk-500.woff2" },
|
||||
{ "weight": "700", "style": "normal", "file": "fonts/space-grotesk-700.woff2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Archivo Black",
|
||||
"family": "Archivo Black",
|
||||
"variants": [
|
||||
{ "weight": "400", "style": "normal", "file": "fonts/archivo-black-400.woff2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Inter",
|
||||
"family": "Inter",
|
||||
"variants": [
|
||||
{ "weight": "400", "style": "normal", "file": "fonts/inter-400.woff2" },
|
||||
{ "weight": "500", "style": "normal", "file": "fonts/inter-500.woff2" },
|
||||
{ "weight": "700", "style": "normal", "file": "fonts/inter-700.woff2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IBM Plex Mono",
|
||||
"family": "IBM Plex Mono",
|
||||
"variants": [
|
||||
{ "weight": "400", "style": "normal", "file": "fonts/ibm-plex-mono-400.woff2" },
|
||||
{ "weight": "500", "style": "normal", "file": "fonts/ibm-plex-mono-500.woff2" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
270
manifest.yaml
270
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)); }
|
||||
|
||||
|
||||
@ -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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -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"]
|
||||
|
||||
118
presets.json
118
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%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
212
seed/seed.json
Normal file
212
seed/seed.json
Normal file
@ -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": "<p>HARDSET is a small studio. We take on a few projects at a time and give each one the whole team. No account layer, no handoff. You talk to the people doing the work.</p><p>We like clear briefs and hard deadlines. We ship. Then we help you run it.</p>" } },
|
||||
{
|
||||
"block_key": "brutalist:spec_sheet",
|
||||
"title": "Studio facts",
|
||||
"slot": "main",
|
||||
"sort_order": 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": "<p>We have room for two more brand or web projects this quarter.</p>" },
|
||||
{ "version": "New", "date": "Last month", "title": "Two more designers", "note": "<p>The studio grew to six. Same way of working, more hands.</p>" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 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": "<p>Why a thick black line still does more work than a soft shadow.</p>", "link": "/blog/in-praise-of-a-hard-border", "linkLabel": "Read" } },
|
||||
{ "block_key": "card", "title": "Post: Ship", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "Ship the ugly first draft", "text": "<p>How shipping early keeps a project honest.</p>", "link": "/blog/ship-the-ugly-first-draft", "linkLabel": "Read" } },
|
||||
{ "block_key": "card", "title": "Post: Type", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Set it in one typeface", "text": "<p>A short case for doing more with less type.</p>", "link": "/blog/set-it-in-one-typeface", "linkLabel": "Read" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/blog/in-praise-of-a-hard-border",
|
||||
"title": "In praise of a hard border",
|
||||
"template_key": "article",
|
||||
"parent_slug": "/blog",
|
||||
"blocks": [
|
||||
{ "block_key": "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": "<p>A soft shadow asks you to squint. A hard border does not. It draws the edge, holds the block, and tells you where one thing ends and the next begins.</p><p>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.</p>" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "<p>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.</p><p>We ship early and often. It keeps the brief honest and the feedback specific. Polish comes last, after the shape is right.</p>" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "<p>One family, used well, beats three used at random. Pick a face with range, learn its weights, and let scale do the talking.</p><p>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.</p>" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/contact",
|
||||
"title": "Start a project",
|
||||
"template_key": "contact",
|
||||
"blocks": [
|
||||
{
|
||||
"block_key": "contact-form",
|
||||
"title": "Project enquiry",
|
||||
"slot": "main",
|
||||
"sort_order": 1,
|
||||
"content": {
|
||||
"heading": "Start a project",
|
||||
"description": "Tell us what you are building and when you need it. We reply within two working days.",
|
||||
"submitLabel": "Send it",
|
||||
"successMessage": "Thanks. Your enquiry is in. We will reply within two working days.",
|
||||
"fields": [
|
||||
{ "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
|
||||
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
|
||||
{ "name": "project", "label": "Project type", "type": "text", "required": false, "width": "full" },
|
||||
{ "name": "message", "label": "What are you building?", "type": "textarea", "required": true, "width": "full" }
|
||||
],
|
||||
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"block_key": "contact-card",
|
||||
"title": "Direct",
|
||||
"slot": "aside",
|
||||
"sort_order": 1,
|
||||
"content": {
|
||||
"heading": "Direct",
|
||||
"columns": 1,
|
||||
"channels": [
|
||||
{ "icon": "lucide:mail", "label": "Email", "value": "studio@hardset.example", "link": "mailto:studio@hardset.example" },
|
||||
{ "icon": "lucide:map-pin", "label": "Studio", "value": "Melbourne, AU" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/login",
|
||||
"title": "Sign in",
|
||||
"template_key": "auth",
|
||||
"blocks": [
|
||||
{ "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } },
|
||||
{ "block_key": "text", "title": "Foot", "slot": "footer", "sort_order": 1, "content": { "text": "<p>Clients sign in to review work and approve invoices.</p>" } }
|
||||
]
|
||||
}
|
||||
],
|
||||
"menu_items": [
|
||||
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
|
||||
{ "menu": "main", "label": "Studio", "page_slug": "/about", "sort_order": 2 },
|
||||
{ "menu": "main", "label": "Journal", "page_slug": "/blog", "sort_order": 3 },
|
||||
{ "menu": "main", "label": "Contact", "page_slug": "/contact", "sort_order": 4 }
|
||||
]
|
||||
}
|
||||
19
seed/workflows.json
Normal file
19
seed/workflows.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"workflows": [
|
||||
{
|
||||
"key": "contact_notify_admins",
|
||||
"name": "Project enquiry to admins",
|
||||
"description": "On a contact-form submission, email the site admins.",
|
||||
"enabled": true,
|
||||
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
|
||||
"steps": [
|
||||
{
|
||||
"type": "email",
|
||||
"name": "Email admins",
|
||||
"template": "Form Submission Notification",
|
||||
"recipients": { "mode": "admins", "scope": "all" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,22 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="article">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.header|safe }}</div>
|
||||
</header>
|
||||
<section class="w-full">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 1rem 1.5rem;">{{ slots.meta|safe }}</div>
|
||||
</section>
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="flex-grow w-full">
|
||||
<article class="brutalist-shell" style="max-width: 60rem; margin: 0 auto; padding: 2rem 1.5rem 4rem;">
|
||||
{% if slots.main %}{{ slots.main|safe }}{% endif %}
|
||||
<article>
|
||||
{% if slots.post_hero %}{{ slots.post_hero|safe }}{% else %}<div class="bru-wrap-mid pt-12"><h1 class="bru-kicker" style="font-size:clamp(2rem,5vw,3.5rem)">{{ title }}</h1></div>{% endif %}
|
||||
<div class="bru-wrap-mid py-10 md:py-14"><div class="prose max-w-none space-y-6">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="bru-meta">This post has no content yet.</p>{% endif %}</div></div>
|
||||
{% if slots.author or slots.related %}
|
||||
<div class="bru-wrap-mid pb-14 space-y-10">
|
||||
{% if slots.author %}<div style="border-top:3px solid hsl(var(--border));padding-top:2.5rem">{{ slots.author|safe }}</div>{% endif %}
|
||||
{% if slots.related %}<div>{{ slots.related|safe }}</div>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.footer|safe }}</div>
|
||||
</footer>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
19
templates/brutalist/auth.ninjatpl
Normal file
19
templates/brutalist/auth.ninjatpl
Normal file
@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="auth">
|
||||
{{ admin_banner_html|safe }}
|
||||
<main class="flex-grow w-full flex flex-col items-center justify-center px-4 py-12">
|
||||
<div class="w-full" style="max-width:30rem">
|
||||
<a href="/" class="block text-center mb-8" aria-label="{{ site_settings.Title }} home">
|
||||
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt }}" class="h-10 w-auto mx-auto">{% else %}<span class="bru-kicker" style="font-size:2rem">{{ site_settings.Title|default:"STUDIO" }}</span>{% endif %}
|
||||
</a>
|
||||
<div class="bru-slab bru-shadow p-6 md:p-8">
|
||||
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="bru-meta">No form assigned to this page.</p>{% endif %}
|
||||
</div>
|
||||
{% if slots.footer %}<div class="mt-6 text-center bru-meta">{{ slots.footer|safe }}</div>{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
17
templates/brutalist/blog-index.ninjatpl
Normal file
17
templates/brutalist/blog-index.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="blog-index">
|
||||
{{ admin_banner_html|safe }}
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="flex-grow w-full">
|
||||
<div class="bru-wrap py-12 md:py-16 space-y-12">
|
||||
{% if title %}<div style="border-bottom:4px solid hsl(var(--border));padding-bottom:1.5rem"><h1 class="bru-kicker" style="font-size:clamp(2.5rem,7vw,5rem)">{{ title }}</h1>{% if page_meta.MetaDescription %}<p class="mt-3 text-lg text-muted-foreground">{{ page_meta.MetaDescription }}</p>{% endif %}</div>{% endif %}
|
||||
{% if slots.featured %}<section>{{ slots.featured|safe }}</section>{% endif %}
|
||||
{% if slots.main %}<section>{{ slots.main|safe }}</section>{% else %}<p class="bru-meta py-8">No posts published yet.</p>{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
22
templates/brutalist/contact.ninjatpl
Normal file
22
templates/brutalist/contact.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="contact">
|
||||
{{ admin_banner_html|safe }}
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="flex-grow w-full">
|
||||
<div class="bru-wrap py-12 md:py-16">
|
||||
{% if slots.aside %}
|
||||
<div class="grid grid-cols-1 gap-12 lg:grid-cols-5">
|
||||
<div class="lg:col-span-3 space-y-8">{% if slots.main %}{{ slots.main|safe }}{% endif %}</div>
|
||||
<aside class="lg:col-span-2 space-y-8">{{ slots.aside|safe }}</aside>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="space-y-8">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="bru-meta py-8">No content blocks assigned to this page.</p>{% endif %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
@ -1,19 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="default">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.header|safe }}</div>
|
||||
</header>
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="flex-grow w-full">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 2rem 1.5rem 4rem;">
|
||||
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="brutalist-mono" style="color: hsl(var(--muted-foreground)); padding: 4rem 0;">NO CONTENT BLOCKS ASSIGNED TO THIS PAGE.</p>{% endif %}
|
||||
<div class="bru-wrap py-12 md:py-16">
|
||||
{% if slots.main %}<div class="space-y-10">{{ slots.main|safe }}</div>{% else %}<p class="bru-meta py-16">No content blocks assigned to this page.</p>{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.footer|safe }}</div>
|
||||
</footer>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,17 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="full-width">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.header|safe }}</div>
|
||||
</header>
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="flex-grow w-full">
|
||||
{% if slots.main %}{{ slots.main|safe }}{% endif %}
|
||||
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="bru-wrap py-16"><p class="bru-meta">No content blocks assigned to this page.</p></div>{% endif %}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.footer|safe }}</div>
|
||||
</footer>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,19 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="landing">
|
||||
{{ admin_banner_html|safe }}
|
||||
<section class="w-full">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.hero|safe }}</div>
|
||||
</section>
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
{% if slots.hero %}<section class="w-full" style="border-bottom:4px solid hsl(var(--border))">{{ slots.hero|safe }}</section>{% endif %}
|
||||
<main class="flex-grow w-full">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 2rem 1.5rem;">
|
||||
{% if slots.ledger %}{{ slots.ledger|safe }}{% endif %}
|
||||
</div>
|
||||
{% if slots.main %}<div>{{ slots.main|safe }}</div>{% endif %}
|
||||
</main>
|
||||
<footer class="w-full mt-auto">
|
||||
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.footer|safe }}</div>
|
||||
</footer>
|
||||
{% if slots.cta %}<section class="w-full" style="border-top:4px solid hsl(var(--border))">{{ slots.cta|safe }}</section>{% endif %}
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -7,32 +7,32 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>{{ site_name }}</title>
|
||||
<style type="text/css">
|
||||
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
|
||||
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
|
||||
@media only screen and (max-width: 620px) {
|
||||
.brutalist-email-container { width: 100% !important; max-width: 100% !important; }
|
||||
.brutalist-email-pad { padding-left: 24px !important; padding-right: 24px !important; }
|
||||
}
|
||||
</style>
|
||||
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
|
||||
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
|
||||
@media only screen and (max-width: 620px) {
|
||||
.brutalist-email-container { width: 100% !important; max-width: 100% !important; }
|
||||
.brutalist-email-pad { padding-left: 24px !important; padding-right: 24px !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: {{ colors.background|default:"#F2EFE8" }}; margin: 0; padding: 0; font-family: &#39;Inter&#39;, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Helvetica, Arial, sans-serif; color: {{ colors.foreground|default:"#0A0A0A" }};">
|
||||
<body style="background-color: {{ colors.background|default:"#F2EFE8" }}; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; color: {{ colors.foreground|default:"#0A0A0A" }};">
|
||||
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 48px 16px; background-color: {{ colors.background|default:"#F2EFE8" }};">
|
||||
<table role="presentation" class="brutalist-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#F7F4ED" }}; border: 1px solid {{ colors.border|default:"#0A0A0A" }}; border-radius: 0;">
|
||||
<table role="presentation" class="brutalist-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#F7F4ED" }}; border: 3px solid {{ colors.border|default:"#0A0A0A" }}; border-radius: 0;">
|
||||
<tr>
|
||||
<td align="left" class="brutalist-email-pad" style="padding: 32px 40px; border-bottom: 1px solid {{ colors.border|default:"#0A0A0A" }};">
|
||||
{% if site_name %}<h1 style="margin: 0; font-family: &#39;Space Grotesk&#39;, &#39;Inter Tight&#39;, Helvetica, Arial, sans-serif; font-weight: 700; font-size: 26px; letter-spacing: -0.02em; color: {{ colors.foreground|default:"#0A0A0A" }}; text-transform: uppercase;">{{ site_name }}</h1>{% endif %}
|
||||
<td align="left" class="brutalist-email-pad" style="padding: 32px 40px; border-bottom: 3px solid {{ colors.border|default:"#0A0A0A" }};">
|
||||
{% if site_name %}<h1 style="margin: 0; font-family: 'Space Grotesk', 'Inter Tight', Helvetica, Arial, sans-serif; font-weight: 700; font-size: 26px; letter-spacing: -0.02em; color: {{ colors.foreground|default:"#0A0A0A" }}; text-transform: uppercase;">{{ site_name }}</h1>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="brutalist-email-pad" style="padding: 40px 48px; color: {{ colors.foreground|default:"#0A0A0A" }}; font-size: 16px; line-height: 1.7;">{{ body|safe }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="brutalist-email-pad" style="padding: 24px 48px 32px; border-top: 1px solid {{ colors.border|default:"#0A0A0A" }}; font-family: &#39;JetBrains Mono&#39;, ui-monospace, monospace; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: {{ colors.mutedForeground|default:"#595959" }};">
|
||||
<td class="brutalist-email-pad" style="padding: 24px 48px 32px; border-top: 3px solid {{ colors.border|default:"#0A0A0A" }}; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: {{ colors.mutedForeground|default:"#595959" }};">
|
||||
{% if site_url %}<div style="margin-bottom: 8px;"><a href="{{ site_url }}" style="color: {{ colors.foreground|default:"#0A0A0A" }}; text-decoration: none; border-bottom: 1px solid {{ colors.border|default:"#0A0A0A" }};">{{ site_url }}</a></div>{% endif %}
|
||||
{% if unsubscribe_url %}<div><a href="{{ unsubscribe_url }}" style="color: {{ colors.foreground|default:"#0A0A0A" }}; text-decoration: none; border-bottom: 1px solid {{ colors.border|default:"#0A0A0A" }};">UNSUBSCRIBE</a></div>{% endif %}
|
||||
</td>
|
||||
|
||||
21
templates/overrides/brutalist/announcement-bar.ninjatpl
Normal file
21
templates/overrides/brutalist/announcement-bar.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
||||
{# brutalist announcement-bar override — bold bordered strip, dismissable. Fields: message, variant, linkText, linkUrl, dismissable. #}
|
||||
{% set annstyle = "background:hsl(var(--accent));color:hsl(var(--accent-foreground))" %}
|
||||
{% if variant == "alert" %}{% set annstyle = "background:hsl(var(--destructive));color:hsl(var(--destructive-foreground))" %}{% elif variant == "info" %}{% set annstyle = "background:hsl(var(--foreground));color:hsl(var(--background))" %}{% endif %}
|
||||
<div data-bn-announcement data-bn-annkey="{{ variant|default:"promo" }}:{{ message|slugify }}" class="bn-announcement w-full {{ class }}" style="{{ annstyle }};border-bottom:3px solid hsl(var(--border))">
|
||||
<div class="bru-wrap py-2 relative flex items-center justify-center gap-2">
|
||||
<p class="bru-meta" style="color:inherit;letter-spacing:0.08em">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
|
||||
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#x"></use></svg></button>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if dismissable %}<script>
|
||||
(function(){
|
||||
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
|
||||
bars.forEach(function(bar){
|
||||
bar.setAttribute('data-bn-ann-wired','1');
|
||||
var key='bnann:'+bar.getAttribute('data-bn-annkey');
|
||||
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
|
||||
var btn=bar.querySelector('[data-bn-ann-dismiss]');
|
||||
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
|
||||
});
|
||||
})();
|
||||
</script>{% endif %}
|
||||
16
templates/overrides/brutalist/audio-embed.ninjatpl
Normal file
16
templates/overrides/brutalist/audio-embed.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
||||
{# brutalist audio-embed override — bordered player card. Fields: url, media, artwork, title, artist, download. #}
|
||||
<figure class="bn-audio my-8 bru-slab bru-shadow overflow-hidden{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden bg-muted" style="border:2px solid hsl(var(--border))">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="bru-kicker truncate" style="font-size:1rem;text-transform:none">{{ title }}</p>{% endif %}{% if artist %}<p class="bru-meta truncate">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="bru-btn bru-btn-solid" style="padding:0.4rem 0.9rem;box-shadow:2px 2px 0 0 hsl(var(--border))" aria-label="Play{% if title %}: {{ title }}{% endif %}"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg>Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="bru-meta inline-flex items-center gap-1 text-foreground">Download</a>{% endif %}</div></div></div></figure><script>
|
||||
(function(){
|
||||
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
|
||||
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
|
||||
e.preventDefault();
|
||||
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
|
||||
if(!src)return;
|
||||
var slot=btn.closest('[data-bn-audio-slot]');
|
||||
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
|
||||
btn.remove();slot.insertBefore(a,slot.firstChild);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
61
templates/overrides/brutalist/auth-form.ninjatpl
Normal file
61
templates/overrides/brutalist/auth-form.ninjatpl
Normal file
@ -0,0 +1,61 @@
|
||||
{# brutalist auth-form override — bordered login/register card. HTMX endpoints + input names + tab panels are load-bearing (public_auth.go); kept exact. Fields: default_tab, show_social, captchaEnabled; context.isPublicLoggedIn, context.publicUsername, context.site.google_oauth_client_id/facebook_app_id. NOTE: the proof-of-work captcha widget is server-generated and cannot be emitted from a codeless template — do NOT enable captchaEnabled on an override-rendered auth-form (see WO limitation). #}
|
||||
{% if context.isPublicLoggedIn %}
|
||||
<div class="bru-slab bru-shadow p-6 text-center" style="max-width:28rem;margin:0 auto">
|
||||
<p class="bru-meta">Logged in as <strong class="text-foreground">{{ context.publicUsername }}</strong></p>
|
||||
<a href="/account" class="bru-btn bru-btn-ghost mt-4">My Account</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% set defaultTab = default_tab|default:"login" %}
|
||||
<style>
|
||||
.bru-auth-tab{color:hsl(var(--muted-foreground));border-bottom:3px solid transparent;margin-bottom:-3px;background:transparent}
|
||||
.bru-auth-tab[data-active="1"]{color:hsl(var(--foreground));border-bottom-color:hsl(var(--accent))}
|
||||
</style>
|
||||
<div class="auth-form" style="max-width:28rem;margin:0 auto">
|
||||
<div id="auth-message"></div>
|
||||
<div class="flex mb-6" style="border-bottom:3px solid hsl(var(--border))">
|
||||
<button type="button" data-auth-tab="login" class="bru-auth-tab flex-1 py-2.5 bru-meta" data-active="{% if defaultTab == "register" %}0{% else %}1{% endif %}">Login</button>
|
||||
<button type="button" data-auth-tab="register" class="bru-auth-tab flex-1 py-2.5 bru-meta" data-active="{% if defaultTab == "register" %}1{% else %}0{% endif %}">Register</button>
|
||||
</div>
|
||||
<div id="login-panel"{% if defaultTab == "register" %} class="hidden"{% endif %}>
|
||||
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML" class="space-y-4">
|
||||
<div><label class="bru-label">Email</label><input type="email" name="email" required class="bru-field"></div>
|
||||
<div><label class="bru-label">Password</label><input type="password" name="password" required class="bru-field"></div>
|
||||
{% if captchaEnabled %}<p class="bru-meta text-destructive">Captcha is enabled but cannot render in this theme. Disable it on the auth-form block.</p>{% endif %}
|
||||
<button type="submit" class="bru-btn bru-btn-solid w-full" hx-disabled-elt="this">Login</button>
|
||||
<p class="text-center bru-meta"><a href="/forgot-password" class="text-accent underline underline-offset-4">Forgot password?</a></p>
|
||||
</form>
|
||||
{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}
|
||||
<div class="mt-5"><div class="relative my-4"><hr class="bru-rule-thin"><span class="bru-meta" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:hsl(var(--background));padding:0 .6rem">or</span></div>
|
||||
{% if context.site.google_oauth_client_id %}<a href="/api/auth/google/start" class="bru-btn bru-btn-ghost w-full mb-2">Continue with Google</a>{% endif %}
|
||||
{% if context.site.facebook_app_id %}<a href="/api/auth/facebook/start" class="bru-btn bru-btn-ghost w-full">Continue with Facebook</a>{% endif %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="register-panel"{% if defaultTab != "register" %} class="hidden"{% endif %}>
|
||||
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML" class="space-y-4">
|
||||
<div><label class="bru-label">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="bru-field"></div>
|
||||
<div><label class="bru-label">Email</label><input type="email" name="email" required class="bru-field"></div>
|
||||
<div><label class="bru-label">Password</label><input type="password" name="password" required minlength="8" class="bru-field"></div>
|
||||
{% if captchaEnabled %}<p class="bru-meta text-destructive">Captcha is enabled but cannot render in this theme. Disable it on the auth-form block.</p>{% endif %}
|
||||
<button type="submit" class="bru-btn bru-btn-solid w-full" hx-disabled-elt="this">Create Account</button>
|
||||
</form>
|
||||
{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}
|
||||
<div class="mt-5"><div class="relative my-4"><hr class="bru-rule-thin"><span class="bru-meta" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:hsl(var(--background));padding:0 .6rem">or</span></div>
|
||||
{% if context.site.google_oauth_client_id %}<a href="/api/auth/google/start" class="bru-btn bru-btn-ghost w-full mb-2">Continue with Google</a>{% endif %}
|
||||
{% if context.site.facebook_app_id %}<a href="/api/auth/facebook/start" class="bru-btn bru-btn-ghost w-full">Continue with Facebook</a>{% endif %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var tabs=document.querySelectorAll('[data-auth-tab]');
|
||||
if(!tabs.length)return;
|
||||
function activate(name){
|
||||
var lp=document.getElementById('login-panel'),rp=document.getElementById('register-panel');
|
||||
if(lp)lp.classList.toggle('hidden',name!=='login');
|
||||
if(rp)rp.classList.toggle('hidden',name!=='register');
|
||||
tabs.forEach(function(t){t.setAttribute('data-active',t.getAttribute('data-auth-tab')===name?'1':'0');});
|
||||
}
|
||||
tabs.forEach(function(t){t.addEventListener('click',function(){activate(t.getAttribute('data-auth-tab'));});});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
33
templates/overrides/brutalist/auth-status.ninjatpl
Normal file
33
templates/overrides/brutalist/auth-status.ninjatpl
Normal file
@ -0,0 +1,33 @@
|
||||
{# brutalist auth-status override — header login state. Logout/resend HTMX endpoints load-bearing. Context: context.isPublicLoggedIn, context.publicDisplayName, context.publicUsername, context.publicEmailVerified. #}
|
||||
{% if not context.isPublicLoggedIn %}
|
||||
<div class="auth-status flex items-center gap-2">
|
||||
<a href="/login" class="bru-meta px-3 py-1.5 text-foreground hover:text-accent transition-colors">Login</a>
|
||||
<a href="/login?tab=register" class="bru-btn bru-btn-solid" style="padding:0.4rem 0.9rem">Sign Up</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% set displayName = context.publicDisplayName|default:context.publicUsername %}
|
||||
{% if not context.publicEmailVerified %}<div class="fixed top-16 inset-x-0 z-40 text-center bru-meta" style="background:hsl(var(--accent));color:hsl(var(--accent-foreground));border-bottom:3px solid hsl(var(--border));padding:0.5rem 1rem">Please verify your email address. <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="underline">Resend verification email</button></div>{% endif %}
|
||||
<div class="auth-status relative">
|
||||
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="flex items-center gap-2 bru-meta px-3 py-1.5 text-foreground hover:text-accent transition-colors">
|
||||
<span class="inline-flex items-center justify-center w-7 h-7 text-xs" style="background:hsl(var(--accent));color:hsl(var(--accent-foreground));border:2px solid hsl(var(--border));font-family:var(--font-heading,sans-serif);font-weight:700">{{ displayName|first }}</span>
|
||||
<span>{{ displayName }}</span>
|
||||
<svg class="w-3 h-3" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
|
||||
</button>
|
||||
<div id="auth-dropdown-menu" class="hidden absolute right-0 top-full mt-2 w-48 bru-slab bru-shadow py-1 z-50">
|
||||
<a href="/account" class="block px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground">My Profile</a>
|
||||
<a href="/account/reviews" class="block px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground">My Reviews</a>
|
||||
<hr class="my-1 bru-rule-thin">
|
||||
<button hx-post="/api/auth/logout" hx-swap="none" class="block w-full text-left px-4 py-2 text-sm text-destructive hover:bg-accent">Logout</button>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
document.addEventListener('click', function(e) {
|
||||
var menu = document.getElementById('auth-dropdown-menu');
|
||||
var btn = document.querySelector('.auth-status > button');
|
||||
if (!menu || !btn) return;
|
||||
if (!btn.contains(e.target) && !menu.contains(e.target)) { menu.classList.add('hidden'); }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
{% endif %}
|
||||
21
templates/overrides/brutalist/author-bio-hero.ninjatpl
Normal file
21
templates/overrides/brutalist/author-bio-hero.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
||||
{# brutalist author-bio-hero override — author-page header from the `authors` provider. Fields: showAvatar, author.*, showPostCount, showSocial. #}
|
||||
{% if author %}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}" style="border-bottom:4px solid hsl(var(--border))">
|
||||
<div class="bru-wrap-narrow flex flex-col items-center text-center">
|
||||
{% if showAvatar and author.avatar_url %}<div class="mb-5 h-24 w-24 overflow-hidden bg-muted bru-shadow" style="border:3px solid hsl(var(--border))">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||
<h1 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,2.5rem)">{{ author.display_name }}</h1>
|
||||
{% if author.job_title or author.company %}<p class="bru-meta mt-2 text-muted-foreground">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
|
||||
{% if author.bio %}<p class="mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
|
||||
<div class="bru-meta mt-5 flex flex-wrap items-center justify-center gap-4 text-muted-foreground">
|
||||
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
|
||||
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
|
||||
</div>
|
||||
{% if showSocial %}<div class="mt-5 flex items-center justify-center gap-3">
|
||||
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-accent" title="Website"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
|
||||
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-accent" title="Twitter"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
|
||||
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-accent" title="LinkedIn"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
|
||||
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-accent" title="GitHub"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
|
||||
</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
13
templates/overrides/brutalist/author-bio.ninjatpl
Normal file
13
templates/overrides/brutalist/author-bio.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
{# brutalist author-bio override — byline slab sourced from the `post` provider's author_* fields. Fields: heading, showAvatar, post.author_*. #}
|
||||
{% if post.author_name %}
|
||||
<aside class="bru-slab bru-shadow p-6{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<p class="bru-meta mb-4 text-muted-foreground">{{ heading }}</p>{% endif %}
|
||||
<div class="flex items-start gap-4">
|
||||
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden bg-muted" style="border:3px solid hsl(var(--border))">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||
<div class="min-w-0">
|
||||
<h3 class="bru-kicker text-base" style="text-transform:none">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-accent">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
|
||||
{% if post.author_bio %}<p class="mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
{% endif %}
|
||||
23
templates/overrides/brutalist/blog-hero.ninjatpl
Normal file
23
templates/overrides/brutalist/blog-hero.ninjatpl
Normal file
@ -0,0 +1,23 @@
|
||||
{# brutalist blog-hero override — blog-index masthead + featured posts from the `posts` provider. Fields: title, subtitle, posts[], showExcerpt. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap">
|
||||
<div style="max-width:44rem">
|
||||
{% if title %}<h1 class="bru-kicker" style="font-size:clamp(2rem,5vw,3.5rem)">{{ title }}</h1>{% endif %}
|
||||
{% if subtitle %}<p class="mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
|
||||
</div>
|
||||
{% if posts %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
{% for post in posts %}
|
||||
<article class="group bru-slab bru-shadow bru-hover flex flex-col overflow-hidden{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch{% endif %}">
|
||||
{% if post.featured_image_url %}<a href="{{ post.url }}" class="block overflow-hidden bg-muted{% if forloop.First %} md:w-1/2{% endif %}" style="border-bottom:3px solid hsl(var(--border))">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
||||
<div class="flex flex-col justify-center gap-2 p-5 md:p-6{% if forloop.First %} md:w-1/2{% endif %}">
|
||||
<h2 class="bru-kicker {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} leading-snug" style="text-transform:none"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h2>
|
||||
{% if showExcerpt and post.excerpt %}<p class="text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
|
||||
<div class="bru-meta mt-2 flex flex-wrap items-center gap-x-3">{% if post.author_name %}<span class="text-foreground">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
13
templates/overrides/brutalist/breadcrumbs.ninjatpl
Normal file
13
templates/overrides/brutalist/breadcrumbs.ninjatpl
Normal file
@ -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" %}
|
||||
<nav aria-label="Breadcrumb" class="py-3{% if class %} {{ class }}{% endif %}">
|
||||
<ol class="bru-meta flex flex-wrap items-center gap-1">
|
||||
{% if showHome %}<li class="flex items-center"><a href="/" class="text-muted-foreground transition-colors hover:text-accent">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
|
||||
{% for item in breadcrumbs %}
|
||||
<li class="flex items-center">
|
||||
{% if showHome or not forloop.First %}{% if arrows %}<svg class="mx-2 h-4 w-4 text-muted-foreground" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"></use></svg>{% else %}<span class="mx-2 text-muted-foreground">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
|
||||
{% if item.is_current %}{% if showCurrent %}<span class="text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-muted-foreground transition-colors hover:text-accent">{{ item.label }}</a>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
@ -1 +1,2 @@
|
||||
<a href="{% if href %}{{ href }}{% elif url %}{{ url }}{% else %}#{% endif %}" class="brutalist-btn" data-brutalist-btn style="display: inline-flex; align-items: center; padding: 0.75rem 1.5rem; font-family: var(--font-mono, 'JetBrains Mono', ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; background-color: transparent; color: hsl(var(--foreground)); border: 1px solid hsl(var(--border)); border-radius: 0; text-decoration: none;">{{ text }}</a>
|
||||
{# brutalist button override — square, hard border, offset shadow, hover snap. Fields: label, link, style, size, icon, iconPosition, align, newTab. #}
|
||||
{% with align=align|default:"left" %}<div class="flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}"><a href="{{ link|default:"#" }}"{% if newTab %} target="_blank" rel="noopener noreferrer"{% endif %} class="bru-btn {% if style == "secondary" or style == "outline" or style == "ghost" or style == "link" %}bru-btn-ghost{% else %}bru-btn-solid{% endif %}{% if size == "sm" %} text-[0.7rem] px-3 py-2{% elif size == "lg" %} text-sm px-6 py-3.5{% endif %}">{% if icon and iconPosition != "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</a></div>{% endwith %}
|
||||
|
||||
2
templates/overrides/brutalist/card.ninjatpl
Normal file
2
templates/overrides/brutalist/card.ninjatpl
Normal file
@ -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" %}<article class="bru-slab bru-shadow bru-hover overflow-hidden {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">{% if link %}<a href="{{ link }}" class="contents">{% endif %}{% if media %}<div class="overflow-hidden bg-muted {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}" style="border-bottom:3px solid hsl(var(--border))">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}<div class="flex flex-col gap-2 p-5 md:p-6 {% if layout == "horizontal" %}justify-center{% endif %}">{% if title %}<h3 class="bru-kicker" style="font-size:1.15rem;text-transform:none">{{ title }}</h3>{% endif %}{% if text %}<div class="text-sm leading-relaxed text-muted-foreground">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="bru-meta mt-2 inline-flex items-center gap-1 text-accent">{{ linkLabel }} →</span>{% endif %}</div>{% if link %}</a>{% endif %}</article>{% endwith %}
|
||||
18
templates/overrides/brutalist/category-list.ninjatpl
Normal file
18
templates/overrides/brutalist/category-list.ninjatpl
Normal file
@ -0,0 +1,18 @@
|
||||
{# brutalist category-list override — from the `categories` provider. list or pills. Fields: heading, style, categories[], showCount. #}
|
||||
{% set pills = style == "pills" %}
|
||||
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<h2 class="bru-meta mb-4 text-foreground">{{ heading }}</h2>{% endif %}
|
||||
{% if pills %}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for cat in categories %}
|
||||
<a href="{{ cat.url }}" class="bru-tag inline-flex items-center gap-1.5">{{ cat.name }}{% if showCount %}<span class="bru-index" style="font-size:0.85rem">{{ cat.post_count }}</span>{% endif %}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<ul class="space-y-2">
|
||||
{% for cat in categories %}
|
||||
<li><a href="{{ cat.url }}" class="bru-slab bru-shadow-sm bru-hover flex items-center justify-between px-3 py-2 text-sm text-foreground transition-colors hover:text-accent"><span>{{ cat.name }}</span>{% if showCount %}<span class="bru-index" style="font-size:0.85rem">{{ cat.post_count }}</span>{% endif %}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</section>
|
||||
18
templates/overrides/brutalist/contact-card.ninjatpl
Normal file
18
templates/overrides/brutalist/contact-card.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap-mid">
|
||||
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if description %}<p class="mt-3 text-lg text-muted-foreground" style="max-width:44rem">{{ description }}</p>{% endif %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-4 {% if columns == 3 %}sm:grid-cols-2 lg:grid-cols-3{% elif columns == 1 %}{% else %}sm:grid-cols-2{% endif %}">
|
||||
{% for channel in channels %}
|
||||
<div class="bru-slab bru-shadow-sm bru-hover flex items-start gap-4 p-5">
|
||||
{% if channel.icon %}<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center bg-accent text-accent-foreground" style="border:3px solid hsl(var(--border))"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ channel.icon|split:":"|first }}.svg#{{ channel.icon|split:":"|last }}"></use></svg></span>{% endif %}
|
||||
<div class="min-w-0">
|
||||
{% if channel.label %}<div class="bru-meta text-muted-foreground">{{ channel.label }}</div>{% endif %}
|
||||
{% if channel.value %}<div class="mt-0.5 break-words bru-kicker" style="font-size:1rem;text-transform:none">{% if channel.link %}<a href="{{ channel.link }}" class="transition-colors hover:text-accent">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
40
templates/overrides/brutalist/contact-form.ninjatpl
Normal file
40
templates/overrides/brutalist/contact-form.ninjatpl
Normal file
@ -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 %}
|
||||
<section id="contact-form-{{ bid }}" class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap-narrow">
|
||||
{% if form.submitted %}
|
||||
<div class="bru-slab bru-shadow p-8 text-center">
|
||||
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center text-accent-foreground" style="background:hsl(var(--accent));border:3px solid hsl(var(--border))"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg></div>
|
||||
<p class="bru-kicker" style="font-size:1.2rem;text-transform:none">{{ form.message|default:successMessage|default:"Thanks — your message has been sent." }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if description %}<p class="mt-3 text-lg text-muted-foreground">{{ description }}</p>{% endif %}
|
||||
{% if form.errors._form %}<div class="mt-6 px-4 py-3 text-sm" style="border:3px solid hsl(var(--destructive));background:hsl(var(--destructive)/0.1);color:hsl(var(--destructive))">{{ form.errors._form }}</div>{% 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 %}
|
||||
<div class="{% if field.width == "half" %}sm:col-span-1{% else %}sm:col-span-2{% endif %}">
|
||||
{% if field.label %}<label for="cf-{{ bid }}-{{ field.name }}" class="bru-label">{{ field.label }}{% if field.required %} <span class="text-accent">*</span>{% endif %}</label>{% endif %}
|
||||
{% if field.type == "textarea" %}
|
||||
<textarea id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" rows="5"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="bru-field"></textarea>
|
||||
{% elif field.type == "select" %}
|
||||
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="bru-field">
|
||||
{% if field.placeholder %}<option value="">{{ field.placeholder }}</option>{% endif %}
|
||||
{% for opt in field.options %}<option value="{{ opt.value }}">{{ opt.label|default:opt.value }}</option>{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<input id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" type="{{ field.type|default:"text" }}"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="bru-field">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="hidden" aria-hidden="true"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
|
||||
<div class="sm:col-span-2">
|
||||
<button type="submit" class="bru-btn bru-btn-solid">{{ submitLabel|default:"Send message" }}</button>
|
||||
</div>
|
||||
{% endform %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endwith %}
|
||||
49
templates/overrides/brutalist/countdown.ninjatpl
Normal file
49
templates/overrides/brutalist/countdown.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap-mid text-center">
|
||||
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
|
||||
<div class="mt-10" data-countdown>
|
||||
<div class="flex items-start justify-center gap-3 sm:gap-6" data-countdown-timer data-target="{{ target }}" role="timer" aria-label="Time remaining">
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] bru-slab bru-shadow-sm px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono)" data-unit="days">--</span><span class="bru-meta mt-2 text-muted-foreground">{{ labelDays|default:"Days" }}</span></div>
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] bru-slab bru-shadow-sm px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono)" data-unit="hours">--</span><span class="bru-meta mt-2 text-muted-foreground">{{ labelHours|default:"Hours" }}</span></div>
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] bru-slab bru-shadow-sm px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono)" data-unit="minutes">--</span><span class="bru-meta mt-2 text-muted-foreground">{{ labelMinutes|default:"Minutes" }}</span></div>
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] bru-slab bru-shadow-sm px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono)" data-unit="seconds">--</span><span class="bru-meta mt-2 text-muted-foreground">{{ labelSeconds|default:"Seconds" }}</span></div>
|
||||
</div>
|
||||
<div class="mt-8 hidden" data-countdown-expired>
|
||||
<p class="bru-kicker" style="font-size:1.35rem">{{ expiredMessage|default:"This has started." }}</p>
|
||||
{% if expiredUrl and expiredLabel %}<a href="{{ expiredUrl }}" class="bru-btn bru-btn-solid mt-4">{{ expiredLabel }} →</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<noscript><p class="mt-6 text-muted-foreground">Counting down to <time datetime="{{ target }}">{{ target }}</time>.</p></noscript>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function(){
|
||||
var timers = document.querySelectorAll('[data-countdown-timer]');
|
||||
timers.forEach(function(el){
|
||||
var target = new Date(el.getAttribute('data-target')).getTime();
|
||||
if (isNaN(target)) return;
|
||||
var wrap = el.closest('[data-countdown]');
|
||||
var expired = wrap ? wrap.querySelector('[data-countdown-expired]') : null;
|
||||
function pad(n){ return (n < 10 ? '0' : '') + n; }
|
||||
function set(unit, value){ var node = el.querySelector('[data-unit="' + unit + '"]'); if (node) node.textContent = value; }
|
||||
var interval;
|
||||
function tick(){
|
||||
var diff = target - Date.now();
|
||||
if (diff <= 0){
|
||||
el.style.display = 'none';
|
||||
if (expired) expired.classList.remove('hidden');
|
||||
if (interval) clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
set('days', Math.floor(diff / 86400000));
|
||||
set('hours', pad(Math.floor(diff % 86400000 / 3600000)));
|
||||
set('minutes', pad(Math.floor(diff % 3600000 / 60000)));
|
||||
set('seconds', pad(Math.floor(diff % 60000 / 1000)));
|
||||
}
|
||||
tick();
|
||||
interval = setInterval(tick, 1000);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
13
templates/overrides/brutalist/cta.ninjatpl
Normal file
13
templates/overrides/brutalist/cta.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
{# brutalist cta override — bordered slab with hard offset. Fields: heading, text, primaryUrl, primaryLabel, secondaryUrl, secondaryLabel, background. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap">
|
||||
<div class="bru-slab bru-shadow p-8 md:p-14 text-center"{% if background == "band" %} style="background:hsl(var(--accent));color:hsl(var(--accent-foreground))"{% endif %}>
|
||||
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem);{% if background == "band" %}color:hsl(var(--accent-foreground)){% endif %}">{{ heading }}</h2>{% endif %}
|
||||
{% if text %}<p class="mt-4 text-lg mx-auto" style="max-width:44rem;{% if background == "band" %}color:hsl(var(--accent-foreground));opacity:0.85{% else %}color:hsl(var(--muted-foreground)){% endif %}">{{ text }}</p>{% endif %}
|
||||
{% if (primaryUrl and primaryLabel) or (secondaryUrl and secondaryLabel) %}<div class="mt-8 flex flex-wrap items-center justify-center gap-4">
|
||||
{% if primaryUrl and primaryLabel %}<a href="{{ primaryUrl }}" class="bru-btn {% if background == "band" %}bru-btn-ghost{% else %}bru-btn-solid{% endif %}">{{ primaryLabel }}</a>{% endif %}
|
||||
{% if secondaryUrl and secondaryLabel %}<a href="{{ secondaryUrl }}" class="bru-btn bru-btn-ghost">{{ secondaryLabel }}</a>{% endif %}
|
||||
</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
2
templates/overrides/brutalist/divider.ninjatpl
Normal file
2
templates/overrides/brutalist/divider.ninjatpl
Normal file
@ -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" %}<div class="mx-auto my-10 flex items-center gap-4 {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator"><span class="flex-1" style="border-top:3px solid hsl(var(--border))"></span><svg class="h-5 w-5 text-accent" aria-hidden="true"><use href="/icons/{{ icon|default:"lucide:asterisk"|split:":"|first }}.svg#{{ icon|default:"lucide:asterisk"|split:":"|last }}"></use></svg><span class="flex-1" style="border-top:3px solid hsl(var(--border))"></span></div>{% else %}<hr class="mx-auto my-10 {% if weight == "thin" %}bru-rule-thin{% else %}bru-rule{% endif %} {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator">{% endif %}{% endwith %}
|
||||
41
templates/overrides/brutalist/event-list.ninjatpl
Normal file
41
templates/overrides/brutalist/event-list.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap">
|
||||
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mt-3 text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
|
||||
{% if layout == "cards" %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{% for event in events %}{% if not (hidePast and event.past) %}
|
||||
<article class="bru-slab bru-shadow bru-hover flex flex-col overflow-hidden{% if event.past %} opacity-60{% endif %}">
|
||||
{% if event.media %}<div class="aspect-video overflow-hidden bg-muted" style="border-bottom:3px solid hsl(var(--border))">{% img event.media alt=event.title class="h-full w-full object-cover" %}</div>{% endif %}
|
||||
<div class="flex flex-1 flex-col gap-2 p-5">
|
||||
<div class="bru-meta text-accent"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} – {{ event.end }}{% endif %}</time>{% if event.timezone %} <span class="text-muted-foreground">{{ event.timezone }}</span>{% endif %}</div>
|
||||
<h3 class="bru-kicker" style="font-size:1.15rem;text-transform:none">{{ event.title }}</h3>
|
||||
{% if event.venue %}<p class="text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
|
||||
{% if event.description %}<p class="text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
|
||||
{% if event.link %}<a href="{{ event.link }}" class="bru-meta mt-auto inline-flex items-center gap-1 pt-2 text-accent">{{ event.linkLabel|default:"Details" }} →</a>{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<ul class="mt-10" style="border-top:3px solid hsl(var(--border));border-bottom:3px solid hsl(var(--border))">
|
||||
{% for event in events %}{% if not (hidePast and event.past) %}
|
||||
<li class="flex flex-col gap-3 py-5 sm:flex-row sm:items-center sm:gap-6{% if event.past %} opacity-60{% endif %}" style="border-bottom:1px solid hsl(var(--border))">
|
||||
<div class="shrink-0 sm:w-52">
|
||||
<div class="bru-meta text-foreground"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time></div>
|
||||
{% if event.end %}<div class="bru-meta text-muted-foreground">to {{ event.end }}</div>{% endif %}
|
||||
{% if event.timezone %}<div class="bru-meta text-muted-foreground">{{ event.timezone }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="bru-kicker" style="font-size:1.05rem;text-transform:none">{{ event.title }}</h3>
|
||||
{% if event.venue %}<p class="text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
|
||||
{% if event.description %}<p class="mt-1 text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
|
||||
</div>
|
||||
{% if event.link %}<a href="{{ event.link }}" class="bru-btn bru-btn-ghost shrink-0" style="padding:0.4rem 0.9rem">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
|
||||
</li>
|
||||
{% endif %}{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
18
templates/overrides/brutalist/faq.ninjatpl
Normal file
18
templates/overrides/brutalist/faq.ninjatpl
Normal file
@ -0,0 +1,18 @@
|
||||
{# brutalist faq override — bordered accordion, native <details> mechanism kept. Fields: heading, intro, variant, items[] (question, answer). #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap-narrow">
|
||||
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 text-center text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
|
||||
<div class="mt-10 space-y-4">
|
||||
{% for item in items %}
|
||||
<details class="group bru-slab bru-shadow-sm px-5"{% if variant == "open-list" %} open{% endif %}>
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between gap-4 py-4 text-lg font-medium marker:content-none [&::-webkit-details-marker]:hidden">
|
||||
<span class="bru-kicker" style="font-size:1.05rem;text-transform:none">{{ item.question }}</span>
|
||||
<svg class="h-5 w-5 shrink-0 text-accent transition-transform duration-200 group-open:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
|
||||
</summary>
|
||||
<div class="pb-4 text-muted-foreground [&_a]:text-accent [&_a]:underline">{{ item.answer|safe }}</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
18
templates/overrides/brutalist/feature-grid.ninjatpl
Normal file
18
templates/overrides/brutalist/feature-grid.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap">
|
||||
{% if eyebrow %}<span class="bru-tag mb-4">{{ eyebrow }}</span>{% endif %}
|
||||
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mt-3 text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
|
||||
<div class="mt-12 grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
|
||||
{% for item in items %}
|
||||
<div class="bru-slab bru-shadow bru-hover flex flex-col gap-2 p-6{% if layout == "centered" %} text-center items-center{% endif %}">
|
||||
{% if item.icon %}<div class="mb-2 inline-flex h-12 w-12 items-center justify-center bg-accent text-accent-foreground" style="border:3px solid hsl(var(--border))"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg></div>{% endif %}
|
||||
<h3 class="bru-kicker" style="font-size:1.15rem;text-transform:none">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" class="transition-colors hover:text-accent">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>
|
||||
{% if item.text %}<p class="text-muted-foreground">{{ item.text }}</p>{% endif %}
|
||||
{% if item.linkUrl and item.linkLabel %}<a href="{{ item.linkUrl }}" class="bru-meta mt-2 inline-flex items-center gap-1 text-accent">{{ item.linkLabel }} →</a>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
22
templates/overrides/brutalist/featured-posts.ninjatpl
Normal file
22
templates/overrides/brutalist/featured-posts.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap">
|
||||
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
|
||||
{% for post in posts %}
|
||||
<article class="group bru-slab bru-shadow bru-hover flex flex-col overflow-hidden">
|
||||
{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-muted" style="border-bottom:3px solid hsl(var(--border))">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
||||
<div class="flex flex-1 flex-col p-5">
|
||||
<h3 class="bru-kicker text-lg leading-snug" style="text-transform:none"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
|
||||
{% if showExcerpt and post.excerpt %}<p class="mt-2 flex-1 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
|
||||
<div class="bru-meta mt-4 flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
{% if showAuthor and post.author_name %}<span class="text-foreground">{{ post.author_name }}</span>{% endif %}
|
||||
{% if showDate and post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}
|
||||
{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
28
templates/overrides/brutalist/footer.ninjatpl
Normal file
28
templates/overrides/brutalist/footer.ninjatpl
Normal file
@ -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 %}<div class="flex items-center gap-3 {{ cls }}">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="text-muted-foreground hover:text-accent transition-colors"><span class="sr-only">{{ s.label }}</span>{% if s.icon %}::{{ s.icon }}::{% else %}{{ s.label }}{% endif %}</a>{% endfor %}</div>{% endif %}{% endmacro %}
|
||||
{% macro newsletter() %}{% if showNewsletter %}<div class="max-w-sm"><h3 class="bru-meta text-foreground">{{ newsletterTitle|default:"Join the list" }}</h3>{% if newsletterDescription %}<p class="mt-1 text-sm text-muted-foreground">{{ newsletterDescription }}</p>{% endif %}<div class="mt-3">{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div></div>{% endif %}{% endmacro %}
|
||||
{% macro legalbar() %}<div class="mt-10 pt-6 flex flex-col sm:flex-row items-center justify-between gap-3 bru-meta" style="border-top:1px solid hsl(var(--border))"><p>© {% now "2006" %} {% if copyright %}{{ copyright }}{% else %}{{ companyName }}{% endif %}</p>{% if legal_items %}<nav class="flex flex-wrap items-center gap-x-4 gap-y-1">{% for l in legal_items %}<a href="{{ l.url|default:"#" }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="hover:text-accent transition-colors">{{ l.label }}</a>{% endfor %}</nav>{% endif %}</div>{% endmacro %}
|
||||
<footer class="w-full bg-background text-foreground {{ class }}" style="border-top:4px solid hsl(var(--border))">
|
||||
<div class="{% if fullWidth %}w-full px-6{% else %}bru-wrap{% endif %} py-16">
|
||||
{% if fl == "minimal" %}
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-6">
|
||||
<div class="flex items-center gap-3">{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-8 w-auto"/>{% elif companyName %}<span class="bru-kicker" style="font-size:1.3rem">{{ companyName }}</span>{% endif %}{% if tagline %}<span class="bru-meta hidden sm:inline">{{ tagline }}</span>{% endif %}</div>
|
||||
{{ sociallinks("") }}
|
||||
</div>
|
||||
{{ legalbar() }}
|
||||
{% else %}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8">
|
||||
<div class="col-span-2 lg:col-span-2">
|
||||
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-9 w-auto"/>{% elif companyName %}<span class="bru-kicker" style="font-size:1.6rem">{{ companyName }}</span>{% endif %}
|
||||
{% if tagline %}<p class="mt-3 max-w-xs text-sm text-muted-foreground">{{ tagline }}</p>{% endif %}
|
||||
{% if address or email or phone %}<address class="mt-4 not-italic bru-meta space-y-1">{% if address %}<div style="white-space:pre-line">{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="hover:text-accent">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="hover:text-accent">{{ phone }}</a></div>{% endif %}</address>{% endif %}
|
||||
{{ sociallinks("mt-5") }}
|
||||
</div>
|
||||
{% for col in columns %}<div><h3 class="bru-meta text-foreground">{{ col.header }}</h3><ul class="mt-3 space-y-2">{% for item in col.items %}<li><a href="{{ item.url|default:"#" }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="text-sm text-muted-foreground hover:text-accent transition-colors">{{ item.label }}</a></li>{% endfor %}</ul></div>{% endfor %}
|
||||
{% if showNewsletter %}<div class="col-span-2 lg:col-span-1">{{ newsletter() }}</div>{% endif %}
|
||||
</div>
|
||||
{{ legalbar() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</footer>
|
||||
52
templates/overrides/brutalist/gallery.ninjatpl
Normal file
52
templates/overrides/brutalist/gallery.ninjatpl
Normal file
@ -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 %}<div class="bn-gallery my-8{% if class %} {{ class }}{% endif %}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}"><div class="{% if layout == "carousel" %}flex snap-x snap-mandatory overflow-x-auto pb-2 {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% elif layout == "masonry" %}columns-2 {% if cols == 4 %}md:columns-4{% elif cols == 2 %}md:columns-2{% else %}md:columns-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% else %}grid grid-cols-2 {% if cols == 4 %}md:grid-cols-4{% elif cols == 2 %}md:grid-cols-2{% else %}md:grid-cols-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% endif %}">{% for image in images %}<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:"" }}" class="group relative block overflow-hidden bru-slab bru-hover bru-shadow-sm focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring {% if layout == "carousel" %}w-64 shrink-0 snap-start sm:w-80{% elif layout == "masonry" %}mb-4 w-full break-inside-avoid{% endif %}">{% if layout == "grid" %}{% img image.src alt=image.alt|default:"" class="aspect-square w-full object-cover" %}{% else %}{% img image.src alt=image.alt|default:"" class="h-auto w-full" %}{% endif %}{% if image.caption %}<span class="sr-only">{{ image.caption }}</span>{% endif %}</a>{% endfor %}</div></div>{% if lightbox %}<script>
|
||||
(function(){
|
||||
if(window.__bnLightbox)return;window.__bnLightbox=true;
|
||||
var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
var ov,imgEl,capEl,items=[],idx=0,lastFocus=null;
|
||||
function build(){
|
||||
ov=document.createElement('div');
|
||||
ov.setAttribute('role','dialog');ov.setAttribute('aria-modal','true');ov.setAttribute('aria-label','Image viewer');ov.tabIndex=-1;
|
||||
ov.style.cssText='position:fixed;inset:0;z-index:9999;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.9);padding:2rem;';
|
||||
var btn='position:absolute;width:2.75rem;height:2.75rem;display:inline-flex;align-items:center;justify-content:center;border:2px solid hsl(var(--foreground));background:hsl(var(--background));color:hsl(var(--foreground));cursor:pointer;';
|
||||
ov.innerHTML=
|
||||
'<button type="button" data-lb-close aria-label="Close" style="'+btn+'top:1rem;right:1rem;"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#x"/></svg></button>'+
|
||||
'<button type="button" data-lb-prev aria-label="Previous image" style="'+btn+'left:1rem;top:50%;transform:translateY(-50%);"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#chevron-left"/></svg></button>'+
|
||||
'<figure style="margin:0;max-width:92vw;max-height:92vh;display:flex;flex-direction:column;align-items:center;gap:0.75rem;"><img data-lb-img alt="" style="max-width:92vw;max-height:82vh;object-fit:contain;border:3px solid hsl(var(--background));'+(reduce?'':'transition:opacity .2s ease;')+'"><figcaption data-lb-cap style="color:hsl(var(--background));opacity:.85;font-family:var(--font-mono,ui-monospace,monospace);font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;text-align:center;max-width:60ch;"></figcaption></figure>'+
|
||||
'<button type="button" data-lb-next aria-label="Next image" style="'+btn+'right:1rem;top:50%;transform:translateY(-50%);"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"/></svg></button>';
|
||||
document.body.appendChild(ov);
|
||||
imgEl=ov.querySelector('[data-lb-img]');capEl=ov.querySelector('[data-lb-cap]');
|
||||
ov.addEventListener('click',function(e){if(e.target===ov||e.target.closest('[data-lb-close]'))close();});
|
||||
ov.querySelector('[data-lb-prev]').addEventListener('click',function(e){e.stopPropagation();show(idx-1);});
|
||||
ov.querySelector('[data-lb-next]').addEventListener('click',function(e){e.stopPropagation();show(idx+1);});
|
||||
}
|
||||
function show(i){
|
||||
if(!items.length)return;
|
||||
idx=(i+items.length)%items.length;
|
||||
var a=items[idx];var im=a.querySelector('img');
|
||||
imgEl.src=(im&&(im.currentSrc||im.src))||a.getAttribute('href');
|
||||
imgEl.alt=im?im.alt:'';
|
||||
var cap=a.getAttribute('data-caption')||'';capEl.textContent=cap;capEl.style.display=cap?'block':'none';
|
||||
}
|
||||
function open(g,link){
|
||||
if(!ov)build();
|
||||
items=Array.prototype.slice.call(g.querySelectorAll('[data-bn-gallery-item]'));
|
||||
lastFocus=document.activeElement;ov.style.display='flex';
|
||||
var multi=items.length>1;
|
||||
ov.querySelector('[data-lb-prev]').style.display=multi?'inline-flex':'none';
|
||||
ov.querySelector('[data-lb-next]').style.display=multi?'inline-flex':'none';
|
||||
show(items.indexOf(link));ov.focus();
|
||||
}
|
||||
function close(){if(ov)ov.style.display='none';if(lastFocus&&lastFocus.focus)lastFocus.focus();}
|
||||
document.addEventListener('click',function(e){
|
||||
var link=e.target.closest('[data-bn-gallery][data-lightbox="true"] [data-bn-gallery-item]');
|
||||
if(!link)return;e.preventDefault();open(link.closest('[data-bn-gallery]'),link);
|
||||
});
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(!ov||ov.style.display==='none')return;
|
||||
if(e.key==='Escape'){e.preventDefault();close();}
|
||||
else if(e.key==='ArrowLeft')show(idx-1);
|
||||
else if(e.key==='ArrowRight')show(idx+1);
|
||||
});
|
||||
})();
|
||||
</script>{% endif %}{% endwith %}
|
||||
@ -1 +1,7 @@
|
||||
{% if level|integer == 1 %}<h1 class="{{ textClass }}" style="font-family: var(--font-heading, &#39;Space Grotesk&#39;, &#39;Inter Tight&#39;, sans-serif); font-weight: 700; font-size: clamp(2.5rem, 6vw, 5rem); line-height: 0.9; letter-spacing: -0.03em; text-transform: uppercase; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="{{ textClass }}" style="font-family: var(--font-heading, &#39;Space Grotesk&#39;, sans-serif); font-weight: 700; font-size: 1.5rem; line-height: 1; letter-spacing: -0.015em; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="{{ textClass }}" style="font-family: var(--font-heading, &#39;Space Grotesk&#39;, sans-serif); font-weight: 600; font-size: 1.25rem; line-height: 1.1; letter-spacing: -0.01em; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="{{ textClass }}" style="font-family: var(--font-heading, &#39;Space Grotesk&#39;, sans-serif); font-weight: 600; font-size: 1.125rem; line-height: 1.15; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="{{ textClass }}" style="font-family: var(--font-heading, &#39;Space Grotesk&#39;, sans-serif); font-weight: 600; font-size: 1rem; line-height: 1.2; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h6>{% else %}<h2 class="{{ textClass }}" style="font-family: var(--font-heading, &#39;Space Grotesk&#39;, &#39;Inter Tight&#39;, sans-serif); font-weight: 700; font-size: clamp(1.875rem, 4vw, 3rem); line-height: 0.95; letter-spacing: -0.02em; text-transform: uppercase; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h2>{% endif %}
|
||||
{# brutalist heading override — oversized uppercase display type. Fields: level, text, textClass. #}
|
||||
{% if level == 1 %}<h1 class="bru-kicker {{ textClass }} flex-1" style="font-size:clamp(2.5rem,7vw,5rem)">{{ text }}</h1>
|
||||
{% elif level == 3 %}<h3 class="bru-kicker {{ textClass }} flex-1" style="font-size:1.5rem;letter-spacing:-0.01em">{{ text }}</h3>
|
||||
{% elif level == 4 %}<h4 class="bru-kicker {{ textClass }} flex-1" style="font-size:1.25rem;letter-spacing:-0.005em">{{ text }}</h4>
|
||||
{% elif level == 5 %}<h5 class="bru-meta {{ textClass }} flex-1" style="font-size:0.95rem">{{ text }}</h5>
|
||||
{% elif level == 6 %}<h6 class="bru-meta {{ textClass }} flex-1" style="font-size:0.8rem">{{ text }}</h6>
|
||||
{% else %}<h2 class="bru-kicker {{ textClass }} flex-1" style="font-size:clamp(1.875rem,4.5vw,3.25rem)">{{ text }}</h2>{% endif %}
|
||||
|
||||
24
templates/overrides/brutalist/hero.ninjatpl
Normal file
24
templates/overrides/brutalist/hero.ninjatpl
Normal file
@ -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 %}
|
||||
<section class="brutalist-hero relative overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}" data-has-media="{% if hasImage %}true{% else %}false{% endif %}">
|
||||
{% if hasImage %}<div class="absolute inset-0 z-0">{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}</div>{% endif %}
|
||||
<div class="bru-wrap relative z-10 {% if variant == "centered" %}text-center{% endif %}">
|
||||
<div style="max-width:70rem;{% if variant == "centered" %}margin:0 auto{% endif %}">
|
||||
{% if eyebrowText %}<span class="bru-tag mb-6">{{ eyebrowText }}</span>{% endif %}
|
||||
{% if headlineText %}<h1 class="headline">{{ headlineText }}</h1>{% endif %}
|
||||
{% if subText %}<p class="mt-6" style="font-family:var(--font-heading,'Space Grotesk',sans-serif);font-weight:400;font-size:clamp(1.1rem,2vw,1.6rem);color:hsl(var(--muted-foreground));max-width:42rem;{% if variant == "centered" %}margin-left:auto;margin-right:auto{% endif %}">{{ subText }}</p>{% endif %}
|
||||
{% if descText %}<p class="mt-4 text-lg text-muted-foreground" style="max-width:42rem;{% if variant == "centered" %}margin-left:auto;margin-right:auto{% endif %}">{{ descText }}</p>{% endif %}
|
||||
{% if pText or sText %}<div class="mt-9 flex flex-col sm:flex-row gap-4 {% if variant == "centered" %}justify-center{% endif %}">{% if pText and pUrl %}<a href="{{ pUrl }}" class="bru-btn bru-btn-solid">{{ pText }}</a>{% endif %}{% if sText and sUrl %}<a href="{{ sUrl }}" class="bru-btn bru-btn-ghost">{{ sText }}</a>{% endif %}</div>{% endif %}
|
||||
{% if trustLogos %}<div class="mt-12"><p class="bru-meta mb-4">Trusted by</p><div class="flex flex-wrap items-center gap-8 {% if variant == "centered" %}justify-center{% endif %}">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}" target="_blank" rel="noopener noreferrer" class="opacity-70 hover:opacity-100 transition-opacity">{% img logo.image alt=logo.alt class="h-8 w-auto" %}</a>{% else %}{% img logo.image alt=logo.alt class="h-8 w-auto opacity-70" %}{% endif %}{% endfor %}</div></div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
31
templates/overrides/brutalist/hours-location.ninjatpl
Normal file
31
templates/overrides/brutalist/hours-location.ninjatpl
Normal file
@ -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). #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap-mid">
|
||||
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||
<div class="bru-slab bru-shadow-sm p-6">
|
||||
{% if hours %}
|
||||
<table class="w-full text-sm">
|
||||
<tbody>
|
||||
{% for row in hours %}
|
||||
<tr style="border-bottom:1px solid hsl(var(--border))">
|
||||
<th scope="row" class="py-2 pr-4 text-left bru-meta text-foreground">{{ row.day }}</th>
|
||||
<td class="py-2 text-right {% if row.closed %}text-muted-foreground{% else %}text-foreground{% endif %} tabular-nums">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if specialHoursNote %}<p class="mt-4 bru-slab-bg px-3 py-2 text-sm text-muted-foreground">{{ specialHoursNote }}</p>{% endif %}
|
||||
{% if address %}<div class="mt-6"><div class="bru-meta text-muted-foreground">Address</div><address class="mt-1 whitespace-pre-line not-italic">{{ address }}</address></div>{% endif %}
|
||||
{% if phone %}<div class="mt-4"><div class="bru-meta text-muted-foreground">Phone</div><a href="tel:{{ phone }}" class="mt-1 inline-block bru-kicker transition-colors hover:text-accent" style="font-size:1rem;text-transform:none">{{ phone }}</a></div>{% endif %}
|
||||
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener noreferrer" class="bru-btn bru-btn-solid mt-6"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#navigation"></use></svg>Get directions</a>{% endif %}
|
||||
</div>
|
||||
{% if mapEmbedUrl %}
|
||||
<div class="bru-slab bru-shadow aspect-square w-full overflow-hidden bg-muted md:aspect-auto">
|
||||
<button type="button" data-map-src="{{ mapEmbedUrl }}" onclick="var f=document.createElement('iframe');f.src=this.getAttribute('data-map-src');f.setAttribute('class','h-full w-full border-0');f.setAttribute('loading','lazy');f.setAttribute('referrerpolicy','no-referrer-when-downgrade');f.setAttribute('title','Map');this.parentNode.replaceChild(f,this);" class="flex h-full min-h-[16rem] w-full items-center justify-center gap-2 bru-meta transition-colors hover:bg-accent hover:text-accent-foreground"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#map-pin"></use></svg>{{ mapButtonLabel|default:"Load map" }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1 +1,2 @@
|
||||
<figure class="brutalist-figure" style="margin: 2rem 0;">{% if src %}<img src="{{ src }}" alt="{{ alt }}" style="display: block; width: 100%; height: auto;">{% elif url %}<img src="{{ url }}" alt="{{ alt }}" style="display: block; width: 100%; height: auto;">{% endif %}{% if caption %}<figcaption>{{ caption }}</figcaption>{% endif %}</figure>
|
||||
{# 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" %}<figure class="flex-1 {% if class %}{{ class }} {% endif %}{% if align == "left" %}flex justify-start{% elif align == "right" %}flex justify-end{% else %}flex justify-center{% endif %}"><div class="bru-slab bru-shadow relative overflow-hidden {% if size == "xs" %}max-w-[200px] w-full{% elif size == "sm" %}max-w-[400px] w-full{% elif size == "md" %}max-w-[600px] w-full{% elif size == "lg" %}max-w-[800px] w-full{% else %}w-full{% endif %}{% if aspectRatio == "square" %} aspect-square{% elif aspectRatio == "video" %} aspect-video{% elif aspectRatio == "portrait" %} aspect-[3/4]{% elif aspectRatio == "wide" %} aspect-[21/9]{% endif %}">{% 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 %}</div>{% if caption %}<figcaption class="bru-meta mt-3">{{ caption }}</figcaption>{% endif %}</figure>{% endwith %}
|
||||
|
||||
16
templates/overrides/brutalist/logo-strip.ninjatpl
Normal file
16
templates/overrides/brutalist/logo-strip.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
||||
{# brutalist logo-strip override — mono heading + optional marquee, keyframe kept verbatim. Fields: heading, marquee, grayscale, logos[] (url, image, alt). #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap">
|
||||
{% if heading %}<p class="bru-meta text-center text-muted-foreground">{{ heading }}</p>{% endif %}
|
||||
<div class="mt-8 {% if marquee %}group relative overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]{% endif %}">
|
||||
<div class="flex flex-wrap items-center justify-center gap-x-10 gap-y-6 {% if marquee %}flex-nowrap [animation:logo-marquee_30s_linear_infinite] motion-reduce:[animation:none] group-hover:[animation-play-state:paused]{% endif %}">
|
||||
{% for logo in logos %}
|
||||
{% if logo.url %}<a href="{{ logo.url }}" class="block shrink-0 opacity-80 transition hover:opacity-100">{% endif %}
|
||||
<span class="inline-flex h-8 items-center{% if grayscale %} grayscale transition-[filter] hover:grayscale-0{% endif %}">{% img logo.image alt=logo.alt class="h-8 w-auto object-contain" %}</span>
|
||||
{% if logo.url %}</a>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% if marquee %}<style>@keyframes logo-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}</style>{% endif %}
|
||||
30
templates/overrides/brutalist/menu-list.ninjatpl
Normal file
30
templates/overrides/brutalist/menu-list.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap-mid">
|
||||
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mt-3 text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-6 {% if columns == 2 %}md:grid-cols-2{% endif %}">
|
||||
{% for section in sections %}
|
||||
<div class="bru-slab bru-shadow-sm menu-section p-6">
|
||||
{% if section.title %}<h3 class="bru-kicker" style="font-size:1.35rem;text-transform:none">{{ section.title }}</h3>{% endif %}
|
||||
{% if section.note %}<p class="mt-2 text-sm text-muted-foreground">{{ section.note }}</p>{% endif %}
|
||||
<ul class="mt-4 space-y-5" style="border-top:3px solid hsl(var(--border));padding-top:1rem">
|
||||
{% for item in section.items %}
|
||||
<li class="flex items-baseline gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<span class="bru-kicker" style="font-size:1rem;text-transform:none">{{ item.name }}</span>
|
||||
{% for badge in item.badges %}{% if badge.label %}<span class="bru-tag-ghost" style="font-size:0.65rem;padding:0.05rem 0.4rem">{{ badge.label }}</span>{% endif %}{% endfor %}
|
||||
</div>
|
||||
{% if item.description %}<p class="mt-0.5 text-sm text-muted-foreground">{{ item.description }}</p>{% endif %}
|
||||
</div>
|
||||
{% if item.price %}<span class="shrink-0 bru-index" style="font-size:1.1rem">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if footnote %}<p class="mt-10 bru-meta text-muted-foreground">{{ footnote }}</p>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
64
templates/overrides/brutalist/navbar.ninjatpl
Normal file
64
templates/overrides/brutalist/navbar.ninjatpl
Normal file
@ -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() %}<a href="/" class="flex-shrink-0 flex items-center">{% if logoType == "text" and logoText.text %}<span class="bru-kicker" style="font-size:1.4rem">{{ logoText.text }}</span>{% elif logo %}<img src="{{ logo }}" alt="{{ alt|default:companyName }}" class="h-8 w-auto"/>{% else %}<span class="bru-kicker" style="font-size:1.4rem">{{ companyName|default:"STUDIO" }}</span>{% endif %}</a>{% endmacro %}
|
||||
{% macro navlink(item) %}<a href="{{ item.url|default:"#" }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} class="bru-meta px-3 py-2 text-foreground hover:text-accent transition-colors {{ item.css_class }}">{{ item.label }}</a>{% endmacro %}
|
||||
{% macro dropdown(item) %}<div class="relative group"><button type="button" class="bru-meta px-3 py-2 text-foreground hover:text-accent transition-colors inline-flex items-center gap-1">{{ item.label }}<svg class="h-3 w-3" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg></button><div class="absolute left-0 mt-0 w-56 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-150 z-50"><div class="mt-2 bru-slab bru-shadow py-2">{% for child in item.children %}{% if child.item_type == "header" %}<div class="bru-meta px-4 py-2 text-muted-foreground">{{ child.label }}</div>{% else %}<a href="{{ child.url|default:"#" }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground transition-colors {{ child.css_class }}">{{ child.label }}</a>{% endif %}{% endfor %}</div></div></div>{% endmacro %}
|
||||
{% macro ctabtn(extra) %}{% if cta_text %}<a href="{{ cta_url|default:"#" }}" class="bru-btn bru-btn-solid {{ extra }}" style="padding:0.5rem 1rem">{{ cta_text }}</a>{% endif %}{% endmacro %}
|
||||
<nav data-bn-navbar data-bn-sticky="{{ sb }}" class="bn-navbar w-full bg-background text-foreground {% if sb == "sticky" or sb == "shrink-on-scroll" %}sticky top-0 z-40 {% endif %}{{ class }}" style="border-bottom:4px solid hsl(var(--border))">
|
||||
{% if showUtilityBar %}<div class="w-full bg-foreground text-background {% if not showOnMobile %}hidden sm:block{% endif %}"><div class="bru-wrap flex items-center justify-between h-8 bru-meta" style="color:hsl(var(--background))"><div>{{ utilityBarText|safe }}</div><div class="flex items-center gap-4">{% if utilityBarPhone %}<a href="tel:{{ utilityBarPhone }}" class="hover:text-accent">{{ utilityBarPhone }}</a>{% endif %}{% if utilityBarEmail %}<a href="mailto:{{ utilityBarEmail }}" class="hover:text-accent">{{ utilityBarEmail }}</a>{% endif %}</div></div></div>{% endif %}
|
||||
<div class="bru-wrap"><div class="bn-navbar-row flex items-center justify-between h-16">
|
||||
<div class="flex items-center">{{ brand() }}</div>
|
||||
<div class="hidden md:flex items-center gap-1">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}</div>
|
||||
<div class="flex items-center md:hidden"><label for="bn-navbar-toggle" data-bn-nav-open aria-controls="bn-navbar-drawer" aria-expanded="false" class="inline-flex items-center justify-center p-2 cursor-pointer" style="border:2px solid hsl(var(--border))"><span class="sr-only">Open menu</span><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#menu"></use></svg></label></div>
|
||||
</div></div>
|
||||
<input type="checkbox" id="bn-navbar-toggle" class="bn-navbar-toggle" aria-hidden="true"/>
|
||||
<label for="bn-navbar-toggle" class="bn-navbar-overlay fixed inset-0 bg-black/50 z-40 md:hidden" aria-hidden="true"></label>
|
||||
<div id="bn-navbar-drawer" class="bn-navbar-drawer fixed top-0 right-0 h-full w-80 max-w-[85vw] bg-background text-foreground z-50 md:hidden flex flex-col" style="border-left:4px solid hsl(var(--border))" role="dialog" aria-modal="true" aria-label="Site menu">
|
||||
<div class="flex items-center justify-between p-4" style="border-bottom:4px solid hsl(var(--border))">{{ brand() }}<label for="bn-navbar-toggle" data-bn-nav-close class="p-2 cursor-pointer text-foreground" style="border:2px solid hsl(var(--border))"><span class="sr-only">Close menu</span><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#x"></use></svg></label></div>
|
||||
<nav class="flex-1 overflow-y-auto py-4" aria-label="Mobile">{% for item in menu.items %}{% if item.children %}<details class="group/acc"><summary class="flex items-center justify-between px-6 py-3 bru-meta cursor-pointer hover:bg-accent hover:text-accent-foreground list-none">{{ item.label }}<svg class="h-3 w-3 transition-transform group-open/acc:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg></summary><div class="pl-8 pb-2">{% for child in item.children %}{% if child.item_type != "header" %}<a href="{{ child.url|default:"#" }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-2 py-2 text-sm text-muted-foreground hover:text-foreground">{{ child.label }}</a>{% endif %}{% endfor %}</div></details>{% else %}<a href="{{ item.url|default:"#" }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-6 py-3 bru-meta hover:bg-accent hover:text-accent-foreground">{{ item.label }}</a>{% endif %}{% endfor %}</nav>
|
||||
{% if cta_text %}<div class="p-4" style="border-top:4px solid hsl(var(--border))"><a href="{{ cta_url|default:"#" }}" class="bru-btn bru-btn-solid w-full">{{ cta_text }}</a></div>{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
<style>
|
||||
.bn-navbar-toggle{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;}
|
||||
.bn-navbar-drawer{transform:translateX(100%);transition:transform .3s ease;}
|
||||
.bn-navbar-overlay{opacity:0;pointer-events:none;transition:opacity .3s ease;}
|
||||
.bn-navbar-toggle:checked ~ .bn-navbar-drawer{transform:translateX(0);}
|
||||
.bn-navbar-toggle:checked ~ .bn-navbar-overlay{opacity:1;pointer-events:auto;}
|
||||
.bn-navbar-drawer details > summary::-webkit-details-marker{display:none;}
|
||||
</style>
|
||||
<script>
|
||||
(function(){
|
||||
if(window.__bnNavInit)return;window.__bnNavInit=1;
|
||||
function ready(fn){if(document.readyState!='loading')fn();else document.addEventListener('DOMContentLoaded',fn);}
|
||||
ready(function(){
|
||||
document.querySelectorAll('[data-bn-navbar]').forEach(function(nav){
|
||||
var toggle=nav.querySelector('.bn-navbar-toggle');
|
||||
var drawer=nav.querySelector('.bn-navbar-drawer');
|
||||
var opener=nav.querySelector('[data-bn-nav-open]');
|
||||
if(toggle&&drawer){
|
||||
function focusables(){return drawer.querySelectorAll('a[href],button,label[for],input,summary,[tabindex]:not([tabindex="-1"])');}
|
||||
toggle.addEventListener('change',function(){
|
||||
var open=toggle.checked;
|
||||
document.documentElement.style.overflow=open?'hidden':'';
|
||||
document.body.style.overflow=open?'hidden':'';
|
||||
if(opener)opener.setAttribute('aria-expanded',open?'true':'false');
|
||||
if(open){var f=focusables();if(f.length)f[0].focus();}
|
||||
else if(opener&&opener.focus)opener.focus();
|
||||
});
|
||||
drawer.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'){toggle.checked=false;toggle.dispatchEvent(new Event('change'));return;}
|
||||
if(e.key!=='Tab')return;
|
||||
var f=focusables();if(!f.length)return;
|
||||
var first=f[0],last=f[f.length-1];
|
||||
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}
|
||||
else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}
|
||||
});
|
||||
drawer.querySelectorAll('a[href]').forEach(function(a){a.addEventListener('click',function(){toggle.checked=false;toggle.dispatchEvent(new Event('change'));});});
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
10
templates/overrides/brutalist/newsletter-signup.ninjatpl
Normal file
10
templates/overrides/brutalist/newsletter-signup.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap-narrow">
|
||||
<div class="bru-slab bru-shadow px-6 py-10 text-center md:px-10"{% if layout == "inline" %} style="background:hsl(var(--accent));color:hsl(var(--accent-foreground))"{% endif %}>
|
||||
{% if title %}<h2 class="bru-kicker" style="font-size:clamp(1.5rem,3.5vw,2.25rem){% if layout == "inline" %};color:hsl(var(--accent-foreground)){% endif %}">{{ title }}</h2>{% endif %}
|
||||
{% if description %}<p class="mx-auto mt-3 max-w-xl" style="{% if layout == "inline" %}color:hsl(var(--accent-foreground));opacity:0.85{% else %}color:hsl(var(--muted-foreground)){% endif %}">{{ description }}</p>{% endif %}
|
||||
<div class="mx-auto mt-6 max-w-md">{% signup_form list=listSlug source=source submit=submitText success_message=successMessage placeholder=placeholder %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
20
templates/overrides/brutalist/page-suggestions.ninjatpl
Normal file
20
templates/overrides/brutalist/page-suggestions.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap-mid text-center">
|
||||
<p class="bru-kicker text-accent" style="font-size:clamp(4rem,18vw,11rem);line-height:0.85">404</p>
|
||||
<h2 class="bru-kicker mt-4" style="font-size:clamp(1.5rem,3.5vw,2.5rem)">{{ title|default:"Page not found" }}</h2>
|
||||
{% if suggestions %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-5 sm:grid-cols-2 text-left">
|
||||
{% for s in suggestions %}
|
||||
<a href="/{{ s.Slug }}" class="bru-slab bru-shadow bru-hover block p-5">
|
||||
<h3 class="bru-kicker" style="font-size:1.1rem;text-transform:none">{{ s.Title }}</h3>
|
||||
{% if showDescription and s.Excerpt %}<p class="mt-2 text-sm text-muted-foreground">{{ s.Excerpt }}</p>{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif emptyMessage %}
|
||||
<p class="mt-6 text-lg text-muted-foreground mx-auto" style="max-width:38rem">{{ emptyMessage }}</p>
|
||||
{% endif %}
|
||||
<div class="mt-10"><a href="/" class="bru-btn bru-btn-solid">Back to home</a></div>
|
||||
</div>
|
||||
</section>
|
||||
13
templates/overrides/brutalist/password-reset-form.ninjatpl
Normal file
13
templates/overrides/brutalist/password-reset-form.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
{# brutalist password-reset-form override — request-reset form. HTMX endpoint + input name load-bearing; kept exact. #}
|
||||
<div class="password-reset-form" style="max-width:28rem;margin:0 auto">
|
||||
<div id="reset-message"></div>
|
||||
<div id="request-reset-panel">
|
||||
<h2 class="bru-kicker" style="font-size:1.75rem">Reset Password</h2>
|
||||
<p class="mt-3 text-sm text-muted-foreground">Enter your email address and we will send you a link to reset your password.</p>
|
||||
<form hx-post="/api/auth/request-password-reset" hx-target="#reset-message" hx-swap="innerHTML" class="mt-6 space-y-4">
|
||||
<div><label class="bru-label">Email</label><input type="email" name="email" required class="bru-field"></div>
|
||||
<button type="submit" class="bru-btn bru-btn-solid w-full" hx-disabled-elt="this">Send Reset Link</button>
|
||||
</form>
|
||||
<p class="text-center mt-5 bru-meta"><a href="/login" class="text-accent underline underline-offset-4">Back to login</a></p>
|
||||
</div>
|
||||
</div>
|
||||
16
templates/overrides/brutalist/popular-posts.ninjatpl
Normal file
16
templates/overrides/brutalist/popular-posts.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
||||
{# brutalist popular-posts override — compact ranked list from the `posts` provider. Fields: heading, posts[], showRank, showThumbnail. #}
|
||||
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<h2 class="bru-meta mb-4 text-foreground">{{ heading }}</h2>{% endif %}
|
||||
<ol class="space-y-3">
|
||||
{% for post in posts %}
|
||||
<li class="bru-slab bru-shadow-sm bru-hover flex items-start gap-3 p-3">
|
||||
{% if showRank %}<span class="bru-index" style="font-size:1.5rem;line-height:1">{{ forloop.Counter }}</span>{% endif %}
|
||||
{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="h-12 w-12 flex-shrink-0 overflow-hidden bg-muted" style="border:2px solid hsl(var(--border))">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover" %}</a>{% endif %}
|
||||
<div class="min-w-0">
|
||||
<h3 class="bru-kicker text-sm leading-snug" style="text-transform:none"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
|
||||
{% if post.published_at %}<p class="bru-meta mt-0.5">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</section>
|
||||
20
templates/overrides/brutalist/post-hero.ninjatpl
Normal file
20
templates/overrides/brutalist/post-hero.ninjatpl
Normal file
@ -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 %}
|
||||
<section class="hero relative flex min-h-[50vh] flex-col justify-end overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="absolute inset-0 z-0">{% img post.featured_image_url alt=post.featured_image_alt class="h-full w-full object-cover" layout="hero" %}<div class="absolute inset-0 bg-black/60"></div></div>
|
||||
<div class="bru-wrap-mid relative z-10 w-full py-12 {% if center %}text-center{% endif %}">
|
||||
<h1 class="bru-kicker text-3xl leading-tight text-white sm:text-4xl lg:text-5xl">{{ post.title }}</h1>
|
||||
{% if showExcerpt and post.excerpt %}<p class="mt-4 max-w-2xl text-lg text-white/80{% if center %} mx-auto{% endif %}">{{ post.excerpt }}</p>{% endif %}
|
||||
{% if showMeta %}<div class="bru-meta mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-white/80{% if center %} justify-center{% endif %}">{% if post.author_name %}<span class="text-white">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap-narrow w-full {% if center %}text-center{% endif %}">
|
||||
<h1 class="bru-kicker text-3xl leading-tight sm:text-4xl lg:text-5xl">{{ post.title }}</h1>
|
||||
{% if showExcerpt and post.excerpt %}<p class="mt-4 text-lg text-muted-foreground{% if center %} mx-auto max-w-2xl{% endif %}">{{ post.excerpt }}</p>{% endif %}
|
||||
{% if showMeta %}<div class="bru-meta mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground{% if center %} justify-center{% endif %}">{% if post.author_name %}<span class="text-foreground">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
9
templates/overrides/brutalist/post-metadata.ninjatpl
Normal file
9
templates/overrides/brutalist/post-metadata.ninjatpl
Normal file
@ -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 %}
|
||||
<div class="bru-meta flex flex-wrap items-center gap-x-4 gap-y-2 text-muted-foreground{% if class %} {{ class }}{% endif %}">
|
||||
{% if showAuthor and post.author_name %}<span class="flex items-center gap-2">{% if showAvatar and post.author_avatar %}{% img post.author_avatar alt=post.author_name class="h-7 w-7 object-cover" %}{% endif %}<span class="text-foreground">{{ post.author_name }}</span></span>{% endif %}
|
||||
{% if showDate and post.published_at %}<time datetime="{{ post.published_at|date:"Y-m-d" }}">{{ post.published_at|date:"F j, Y" }}</time>{% endif %}
|
||||
{% if showReadingTime and post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
|
||||
{% if post.categories %}<span class="flex flex-wrap gap-1.5">{% for cat in post.categories %}<a href="{{ cat.url|default:'#' }}" class="bru-tag">{{ cat.name }}</a>{% endfor %}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
25
templates/overrides/brutalist/pricing.ninjatpl
Normal file
25
templates/overrides/brutalist/pricing.ninjatpl
Normal file
@ -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). #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap">
|
||||
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 text-center text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
|
||||
<div class="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{% for tier in tiers %}
|
||||
<div class="relative flex flex-col p-6 {% if tier.highlighted %}bru-slab bru-shadow{% else %}bru-slab bru-shadow-sm{% endif %}"{% if tier.highlighted %} style="background:hsl(var(--accent));color:hsl(var(--accent-foreground))"{% endif %}>
|
||||
{% if tier.badge %}<span class="bru-tag absolute -top-4 left-6">{{ tier.badge }}</span>{% endif %}
|
||||
<h3 class="bru-kicker" style="font-size:1.25rem">{{ tier.name }}</h3>
|
||||
<p class="mt-4 flex items-baseline gap-1">
|
||||
<span class="bru-index" style="font-size:2.75rem;line-height:1">{% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }}</span>
|
||||
{% if tier.period %}<span class="bru-meta {% if tier.highlighted %}opacity-80{% else %}text-muted-foreground{% endif %}">{{ tier.period }}</span>{% endif %}
|
||||
</p>
|
||||
{% if tier.description %}<p class="mt-3 text-sm {% if tier.highlighted %}opacity-90{% else %}text-muted-foreground{% endif %}">{{ tier.description }}</p>{% endif %}
|
||||
{% if tier.features %}<ul class="mt-6 flex-1 space-y-3 text-sm">
|
||||
{% for feature in tier.features %}<li class="flex items-start gap-2"><svg class="mt-0.5 h-4 w-4 shrink-0 {% if tier.highlighted %}text-accent-foreground{% else %}text-accent{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg><span>{{ feature }}</span></li>{% endfor %}
|
||||
</ul>{% endif %}
|
||||
{% if tier.ctaUrl and tier.ctaLabel %}<a href="{{ tier.ctaUrl }}" class="bru-btn mt-8 w-full justify-center {% if tier.highlighted %}bru-btn-ghost{% else %}bru-btn-solid{% endif %}">{{ tier.ctaLabel }}</a>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if billingNote %}<p class="mt-8 text-center bru-meta text-muted-foreground">{{ billingNote }}</p>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
2
templates/overrides/brutalist/quote.ninjatpl
Normal file
2
templates/overrides/brutalist/quote.ninjatpl
Normal file
@ -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" %}<figure class="my-8 {% if style == "inline-card" %}bru-slab bru-shadow p-6 md:p-8{% else %}pl-6 md:pl-8 py-2{% endif %}{% if align == "center" %} text-center{% endif %}{% if class %} {{ class }}{% endif %}"{% if style != "inline-card" %} style="border-left:6px solid hsl(var(--accent))"{% endif %}><blockquote{% if cite %} cite="{{ cite }}"{% endif %} class="bru-kicker text-foreground" style="text-transform:none;line-height:1.15;font-size:{% if style == "pull" %}clamp(1.75rem,4vw,2.75rem){% else %}1.35rem{% endif %}">{{ quote|safe }}</blockquote>{% if attribution or role or cite %}<figcaption class="bru-meta mt-4 {% if align == "center" %}flex flex-col items-center{% endif %}">{% if attribution %}<span class="text-foreground">{{ attribution }}</span>{% endif %}{% if role %}<span>{{ role }}</span>{% endif %}{% if cite %}<a href="{{ cite }}" class="text-accent underline underline-offset-4">Source</a>{% endif %}</figcaption>{% endif %}</figure>{% endwith %}
|
||||
19
templates/overrides/brutalist/related-posts.ninjatpl
Normal file
19
templates/overrides/brutalist/related-posts.ninjatpl
Normal file
@ -0,0 +1,19 @@
|
||||
{# brutalist related-posts override — grid from the `posts` provider, thick top rule. Fields: heading, columns, posts[]. #}
|
||||
{% if posts %}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}" style="border-top:4px solid hsl(var(--border))">
|
||||
<div class="bru-wrap">
|
||||
<h2 class="bru-kicker mb-8" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading|default:"Related posts" }}</h2>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% else %}lg:grid-cols-3{% endif %}">
|
||||
{% for post in posts %}
|
||||
<article class="group bru-slab bru-shadow bru-hover flex flex-col overflow-hidden">
|
||||
{% if post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-muted" style="border-bottom:3px solid hsl(var(--border))">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
||||
<div class="flex flex-col gap-1 p-5">
|
||||
<h3 class="bru-kicker text-base leading-snug" style="text-transform:none"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
|
||||
{% if post.published_at %}<p class="bru-meta mt-1">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
14
templates/overrides/brutalist/rich-section.ninjatpl
Normal file
14
templates/overrides/brutalist/rich-section.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section {% if background == "muted" %}bg-muted text-foreground{% elif background == "card" %}bg-card text-card-foreground{% else %}bg-background text-foreground{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap grid items-center gap-10 md:grid-cols-2 md:gap-16">
|
||||
<div class="{% if mediaPosition == "left" %}md:order-2{% else %}md:order-1{% endif %}">
|
||||
{% if eyebrow %}<span class="bru-tag mb-4">{{ eyebrow }}</span>{% endif %}
|
||||
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if body %}<div class="mt-4 space-y-4 text-lg text-muted-foreground [&_a]:text-accent [&_a]:underline [&_strong]:text-foreground">{{ body|safe }}</div>{% endif %}
|
||||
{% if ctaUrl and ctaLabel %}<a href="{{ ctaUrl }}" class="bru-btn bru-btn-solid mt-6">{{ ctaLabel }} →</a>{% endif %}
|
||||
</div>
|
||||
<div class="{% if mediaPosition == "left" %}md:order-1{% else %}md:order-2{% endif %}">
|
||||
{% if media %}<div class="bru-slab bru-shadow overflow-hidden">{% img media alt=mediaAlt class="w-full h-auto object-cover" %}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
2
templates/overrides/brutalist/rich-text.ninjatpl
Normal file
2
templates/overrides/brutalist/rich-text.ninjatpl
Normal file
@ -0,0 +1,2 @@
|
||||
{# brutalist rich-text override — imported HTML content. Field: html. #}
|
||||
{% if html %}<article class="prose max-w-none text-foreground [&_a]:text-accent [&_a]:underline [&_a]:underline-offset-4 [&_h2]:bru-kicker [&_h3]:bru-kicker [&_blockquote]:border-l-4 [&_blockquote]:border-border [&_blockquote]:pl-5" style="line-height:1.7">{{ html|safe }}</article>{% endif %}
|
||||
13
templates/overrides/brutalist/sidebar-nav.ninjatpl
Normal file
13
templates/overrides/brutalist/sidebar-nav.ninjatpl
Normal file
@ -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" %}
|
||||
<nav class="flex-1 {% if compact %}w-56 py-4 pr-3{% else %}w-64 py-6 pr-4{% endif %}{% if showBorder %} border-r-[3px] border-border{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
{% if title %}<h3 class="bru-meta text-foreground {% if compact %}px-2 mb-3{% else %}px-3 mb-4{% endif %}">{{ title }}</h3>{% endif %}
|
||||
<ul class="{% if compact %}space-y-0.5{% else %}space-y-1{% endif %}">
|
||||
{% for item in menu.items %}
|
||||
<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} data-bn-sidebar-link class="flex items-center text-sm text-muted-foreground transition-colors hover:text-foreground hover:bg-accent {% if compact %}px-2 py-1.5{% else %}px-3 py-2{% endif %} {{ item.css_class }}">{% if item.icon %}<svg class="mr-2 h-4 w-4 text-accent" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg>{% endif %}{{ item.label }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% if highlightCurrent %}<script>
|
||||
(function(){var p=window.location.pathname;document.querySelectorAll('[data-bn-sidebar-link]').forEach(function(a){if(a.getAttribute('href')===p){a.classList.remove('text-muted-foreground');a.classList.add('text-foreground','bg-accent','font-medium');}});})();
|
||||
</script>{% endif %}
|
||||
14
templates/overrides/brutalist/social-links.ninjatpl
Normal file
14
templates/overrides/brutalist/social-links.ninjatpl
Normal file
@ -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. #}
|
||||
<div class="social-links{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<p class="mb-3 bru-meta text-muted-foreground">{{ heading }}</p>{% endif %}
|
||||
<ul class="flex {% if style == "stack" %}flex-col gap-2{% else %}flex-wrap items-center gap-3{% endif %}">
|
||||
{% for link in links %}{% if link.url %}
|
||||
<li>
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" aria-label="{{ link.label|default:link.network }}" class="bru-shadow-sm bru-hover inline-flex items-center gap-2 bg-card text-foreground transition-colors hover:bg-accent hover:text-accent-foreground {% if style == "stack" %}w-full px-4 py-2{% else %}p-2.5{% endif %}" style="border:3px solid hsl(var(--border))">
|
||||
{% if link.network %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ link.network|split:":"|first }}.svg#{{ link.network|split:":"|last }}"></use></svg>{% endif %}
|
||||
{% if style == "stack" and link.label %}<span class="bru-meta">{{ link.label }}</span>{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
49
templates/overrides/brutalist/stats.ninjatpl
Normal file
49
templates/overrides/brutalist/stats.ninjatpl
Normal file
@ -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). #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}"{% if countUp %} data-stats{% endif %}>
|
||||
<div class="bru-wrap">
|
||||
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 text-center text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
|
||||
<dl class="mt-12 grid grid-cols-2 gap-6 {% if columns == 2 %}md:grid-cols-2{% elif columns == 3 %}md:grid-cols-3{% else %}md:grid-cols-4{% endif %}">
|
||||
{% for item in items %}
|
||||
<div class="bru-slab bru-shadow-sm p-6 text-center">
|
||||
<dd class="bru-index" style="font-size:clamp(2.25rem,5vw,3.25rem);line-height:1">{% if item.prefix %}{{ item.prefix }}{% endif %}<span{% if countUp %} data-countup-value="{{ item.value }}"{% endif %}>{{ item.value }}</span>{% if item.suffix %}{{ item.suffix }}{% endif %}</dd>
|
||||
{% if item.label %}<dt class="bru-meta mt-3 text-muted-foreground">{{ item.label }}</dt>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
{% if countUp %}<script>
|
||||
(function(){
|
||||
var sections = document.querySelectorAll('[data-stats]');
|
||||
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
function animate(section){
|
||||
var nodes = section.querySelectorAll('[data-countup-value]');
|
||||
nodes.forEach(function(node){
|
||||
var raw = node.getAttribute('data-countup-value') || '';
|
||||
var target = parseFloat(raw.replace(/[^0-9.\-]/g, ''));
|
||||
if (isNaN(target)) return;
|
||||
var decimals = (raw.split('.')[1] || '').length;
|
||||
var start = null, dur = 1200;
|
||||
function step(ts){
|
||||
if (start === null) start = ts;
|
||||
var p = Math.min((ts - start) / dur, 1);
|
||||
var eased = 1 - Math.pow(1 - p, 3);
|
||||
node.textContent = (target * eased).toFixed(decimals);
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
else node.textContent = target.toFixed(decimals);
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
});
|
||||
}
|
||||
sections.forEach(function(section){
|
||||
if (reduce || !('IntersectionObserver' in window)) return;
|
||||
var io = new IntersectionObserver(function(entries){
|
||||
entries.forEach(function(e){
|
||||
if (e.isIntersecting){ animate(section); io.disconnect(); }
|
||||
});
|
||||
}, { threshold: 0.3 });
|
||||
io.observe(section);
|
||||
});
|
||||
})();
|
||||
</script>{% endif %}
|
||||
22
templates/overrides/brutalist/team.ninjatpl
Normal file
22
templates/overrides/brutalist/team.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap">
|
||||
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 text-center text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
|
||||
<div class="mt-12 {% if layout == "list" %}mx-auto max-w-3xl space-y-6{% elif layout == "single" %}mx-auto max-w-xl{% else %}grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}{% endif %}">
|
||||
{% for member in members %}
|
||||
<div class="bru-slab bru-shadow-sm p-5 {% if layout == "list" %}flex items-start gap-5{% else %}text-center{% endif %}">
|
||||
{% if member.photo %}<span class="{% if layout == "list" %}h-20 w-20{% else %}mx-auto h-28 w-28{% endif %} block shrink-0 overflow-hidden bg-muted" style="border:3px solid hsl(var(--border))">{% img member.photo alt=member.name class="h-full w-full object-cover" %}</span>{% endif %}
|
||||
<div class="{% if layout != "list" %}mt-4{% endif %}">
|
||||
{% if member.name %}<h3 class="bru-kicker" style="font-size:1.15rem;text-transform:none">{{ member.name }}</h3>{% endif %}
|
||||
{% if member.role %}<p class="bru-meta text-accent">{{ member.role }}</p>{% endif %}
|
||||
{% if member.bio %}<p class="mt-2 text-sm text-muted-foreground">{{ member.bio }}</p>{% endif %}
|
||||
{% if member.socials %}<div class="mt-3 flex gap-3 {% if layout != "list" %}justify-center{% endif %}">
|
||||
{% for social in member.socials %}{% if social.url %}<a href="{{ social.url }}" class="text-muted-foreground transition-colors hover:text-accent" aria-label="Social link"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ social.icon|split:":"|first }}.svg#{{ social.icon|split:":"|last }}"></use></svg></a>{% endif %}{% endfor %}
|
||||
</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
27
templates/overrides/brutalist/testimonial.ninjatpl
Normal file
27
templates/overrides/brutalist/testimonial.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap">
|
||||
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
<div class="mt-12 {% if layout == "single" %}mx-auto max-w-3xl{% elif layout == "carousel" %}flex snap-x snap-mandatory gap-6 overflow-x-auto pb-4{% else %}grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3{% endif %}">
|
||||
{% for q in quotes %}
|
||||
<figure class="bru-slab bru-shadow flex flex-col p-6{% if layout == "carousel" %} min-w-[18rem] max-w-sm shrink-0 snap-start{% endif %}">
|
||||
{% if q.rating %}<div class="mb-3 flex gap-0.5" aria-label="Rated {{ q.rating }} out of 5">
|
||||
<svg class="h-4 w-4 {% if q.rating >= 1 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
<svg class="h-4 w-4 {% if q.rating >= 2 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
<svg class="h-4 w-4 {% if q.rating >= 3 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
<svg class="h-4 w-4 {% if q.rating >= 4 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
<svg class="h-4 w-4 {% if q.rating >= 5 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
</div>{% endif %}
|
||||
<blockquote class="flex-1 text-lg leading-relaxed">{% if q.quote %}“{{ q.quote }}”{% endif %}</blockquote>
|
||||
<figcaption class="mt-5 flex items-center gap-3">
|
||||
{% if q.avatar %}<span class="h-11 w-11 shrink-0 overflow-hidden bg-muted" style="border:3px solid hsl(var(--border))">{% img q.avatar alt=q.name class="h-full w-full object-cover" %}</span>{% endif %}
|
||||
<span>
|
||||
{% if q.name %}<span class="block bru-kicker" style="font-size:1rem;text-transform:none">{{ q.name }}</span>{% endif %}
|
||||
{% if q.role %}<span class="block bru-meta text-muted-foreground">{{ q.role }}</span>{% endif %}
|
||||
</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1 +1,2 @@
|
||||
<div class="brutalist-text {{ class }}" style="font-family: var(--font-body, 'Inter', -apple-system, sans-serif); font-size: 1.0625rem; line-height: 1.7; color: hsl(var(--foreground)); max-width: 70ch; letter-spacing: 0.005em;">{{ text|safe }}</div>
|
||||
{# brutalist text override — body copy, generous measure. Fields: text, proseClass. #}
|
||||
<div class="prose max-w-none flex-1 text-foreground [&_a]:text-accent [&_a]:underline [&_a]:underline-offset-4 [&_strong]:text-foreground [&_h2]:bru-kicker [&_h3]:bru-kicker {{ proseClass }}" style="line-height:1.7">{{ text|safe }}</div>
|
||||
|
||||
22
templates/overrides/brutalist/timeline.ninjatpl
Normal file
22
templates/overrides/brutalist/timeline.ninjatpl
Normal file
@ -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. #}
|
||||
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="bru-wrap-mid">
|
||||
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 text-center text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
|
||||
<ol class="mt-12 {% if orientation == "horizontal" %}grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4{% else %}relative space-y-8 before:absolute before:left-[0.6875rem] before:top-2 before:h-[calc(100%-1rem)] before:w-[3px] before:bg-border{% endif %}">
|
||||
{% for step in steps %}
|
||||
<li class="{% if orientation == "horizontal" %}text-center{% else %}relative pl-14{% endif %}">
|
||||
<span class="flex h-9 w-9 items-center justify-center bg-accent text-accent-foreground {% if orientation == "horizontal" %}mx-auto{% else %}absolute left-0 top-0{% endif %}" style="border:3px solid hsl(var(--border))">
|
||||
{% if marker == "numbered" %}<span class="bru-index" style="font-size:1rem">{{ forloop.counter }}</span>{% elif marker == "dot" %}<span class="h-2 w-2 bg-accent-foreground"></span>{% else %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#flag"></use></svg>{% endif %}
|
||||
</span>
|
||||
<div class="{% if orientation == "horizontal" %}mt-4{% else %}pt-1{% endif %}">
|
||||
{% if step.label %}<p class="bru-meta text-accent">{{ step.label }}</p>{% endif %}
|
||||
{% if step.title %}<h3 class="mt-1 bru-kicker" style="font-size:1.15rem;text-transform:none">{{ step.title }}</h3>{% endif %}
|
||||
{% if step.text %}<p class="mt-1 text-muted-foreground">{{ step.text }}</p>{% endif %}
|
||||
{% if step.media %}<div class="bru-slab bru-shadow-sm mt-3 overflow-hidden">{% img step.media alt=step.title class="w-full h-auto object-cover" %}</div>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
10
templates/overrides/brutalist/utility-bar.ninjatpl
Normal file
10
templates/overrides/brutalist/utility-bar.ninjatpl
Normal file
@ -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 %}<a href="{{ it.link }}" class="inline-flex items-center gap-1.5 hover:opacity-70 transition-opacity">{% if it.icon %}::{{ it.icon }}:sm::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</a>{% else %}<span class="inline-flex items-center gap-1.5">{% if it.icon %}::{{ it.icon }}:sm::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</span>{% endif %}{% endmacro %}
|
||||
<div class="w-full bru-meta {% if not showOnMobile %}hidden sm:block{% endif %} {{ class }}" style="{{ ubstyle }}">
|
||||
<div class="bru-wrap h-9 flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-5 flex-wrap">{% for it in leftItems %}{{ ubitem(it) }}{% endfor %}</div>
|
||||
<div class="flex items-center gap-5 flex-wrap justify-end">{% for it in rightItems %}{{ ubitem(it) }}{% endfor %}</div>
|
||||
</div>
|
||||
</div>
|
||||
23
templates/overrides/brutalist/video-embed.ninjatpl
Normal file
23
templates/overrides/brutalist/video-embed.ninjatpl
Normal file
@ -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" %}<figure class="bn-video my-8{% if class %} {{ class }}{% endif %}"><div class="bn-video-frame bru-slab bru-shadow relative w-full overflow-hidden {% if aspect == "4:3" %}aspect-[4/3]{% elif aspect == "1:1" %}aspect-square{% elif aspect == "21:9" %}aspect-[21/9]{% else %}aspect-video{% endif %}" data-bn-video data-video-url="{{ url|default:"" }}" data-video-media="{% if media %}{{ media|mediaURL }}{% endif %}"><button type="button" data-bn-video-play class="group absolute inset-0 flex h-full w-full items-center justify-center focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring" aria-label="Play video{% if title %}: {{ title }}{% endif %}">{% if poster %}{% img poster alt=title|default:"" class="absolute inset-0 h-full w-full object-cover" %}{% endif %}<span class="absolute inset-0 bg-foreground/10 transition-colors group-hover:bg-foreground/25" aria-hidden="true"></span><span class="relative inline-flex h-16 w-16 items-center justify-center text-accent-foreground transition-transform group-hover:scale-105 motion-reduce:transform-none" style="background:hsl(var(--accent));border:3px solid hsl(var(--border));box-shadow:4px 4px 0 0 hsl(var(--border))"><svg class="h-7 w-7" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg></span></button></div>{% if caption %}<figcaption class="bru-meta mt-3 text-center">{{ caption }}</figcaption>{% endif %}</figure><script>
|
||||
(function(){
|
||||
if(window.__bnVideoFacade)return;window.__bnVideoFacade=true;
|
||||
function yt(u){var m=u.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{6,})/);return m?m[1]:null;}
|
||||
function vim(u){var m=u.match(/vimeo\.com\/(?:video\/)?(\d+)/);return m?m[1]:null;}
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest('[data-bn-video-play]');if(!btn)return;
|
||||
var frame=btn.closest('[data-bn-video]');if(!frame)return;
|
||||
e.preventDefault();
|
||||
var url=frame.getAttribute('data-video-url')||'',media=frame.getAttribute('data-video-media')||'',node;
|
||||
var vstyle='position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:rgba(0,0,0,1);';
|
||||
if(media){node=document.createElement('video');node.src=media;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
|
||||
else{var y=yt(url),v=vim(url),embed;
|
||||
if(y)embed='https://www.youtube-nocookie.com/embed/'+y+'?autoplay=1&rel=0';
|
||||
else if(v)embed='https://player.vimeo.com/video/'+v+'?autoplay=1';
|
||||
if(embed){node=document.createElement('iframe');node.src=embed;node.allow='autoplay; fullscreen; picture-in-picture';node.setAttribute('allowfullscreen','');node.setAttribute('title',btn.getAttribute('aria-label')||'Video');node.style.cssText='position:absolute;inset:0;width:100%;height:100%;border:0;';}
|
||||
else if(url){node=document.createElement('video');node.src=url;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
|
||||
}
|
||||
if(node){btn.remove();frame.appendChild(node);}
|
||||
});
|
||||
})();
|
||||
</script>{% endwith %}
|
||||
Loading…
x
Reference in New Issue
Block a user