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>
65 lines
7.9 KiB
Plaintext
65 lines
7.9 KiB
Plaintext
{# 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>
|