Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously an unversioned directory inside ~/src/blockninja-themes/noir. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
9.9 KiB
9.9 KiB
Noir — Build Report
Implementation pass: wave-1 scaffold of the noir theme plugin.
Plugin slug: noir
Module path: git.dev.alexdunmow.com/block/themes/noir
SDK pinned to: git.dev.alexdunmow.com/block/core v0.11.1
Go directive: go 1.26.4
Tech style: templ (per spec §11)
What landed
Metadata & build glue
plugin.mod— name, display_name, scope@themes, kindtheme, version0.1.0, categories["templates", "media"], tags array (8 entries, includes the four spec-required minimums),[compatibility] block_core = ">=0.11.0 <0.12.0".go.mod— pinsblock/core v0.11.1andtempl v0.3.1020, mirrors gotham's indirect set, noreplacedirectives.Makefile— local-only targets (all,templ,clean). Norebuildtarget. Default buildsnoir.soviaCGO_ENABLED=1 go build -buildmode=plugin.embed.go— five canonical//go:embeddirectives plus aThemeCSSManifest()that surfacesassets/style.cssthroughCSSManifest.InputCSSAppendso the custom utilities (.tracked-mono,.bleed,.hairline, sprocket motif, lightbox overlay) survive Tailwind's content scanner.registration.go— exportsvar Registration plugin.PluginRegistrationwith all functions wired.
System & page templates
tr.RegisterSystemTemplate({Key: "noir", …})— exactly once.- Four
tr.RegisterPageTemplate("noir", …)calls with the spec's slot sets:default—["header", "main", "footer"]landing—["hero", "main", "cta", "footer"]article—["header", "main", "aside", "footer"]full-width—["header", "main", "footer"]
- Each page renderer lives in
template.templand consumes the sharedbn.Head/bn.AdminBypassBanner/bn.BodyEndhelpers, mirroring gotham.
Blocks (6 theme-specific)
noir:lightbox_gallery— grid + vanilla keyboard-aware lightbox overlay (Esc + Enter/Space, click-outside dismiss, ARIA-modal).noir:contact_sheet— sprocket-framed numbered grid;data-frame-numberattribute on each frame for UAT §13.9.noir:case_study— sticky meta rail (client / year / credits) + image stack.noir:caption_strip— 10px mono full-width strip.noir:image_pair— 50/50 diptych with shared caption.noir:footer— dissolved-rail footer with optional social links.
All registered with Source: "noir" and the appropriate blocks.Category* constant.
Schemas (6, draft-07)
- Property names exactly match the Go
content["…"]reads in each block. - Every
x-editorvalue comes from the allowed set:text,media,select,number,array,link. lightbox_gallery.columnsusesx-editor: selectwithenum: [2, 3, 4](spec §8).case_study.yearusesx-editor: number(spec §8).case_study.creditsisarray<string>;case_study.imagesisarray<media>per spec §8.footer.socialisarray<link>withtext/urlfields per spec §8.
Template overrides (5)
RegisterTemplateOverride("noir", …)forheading,text,image,button,card— exactly five calls, per UAT §3.- Display headings render with
font-family: var(--font-heading)and no underline. - Button override is hairline 1px outline, transparent background, hover inverts to
--primary/--primary-foreground. - Card override is transparent with hairline border only.
- Image override emits the
.bleedutility for full-bleed and afigcaption.tracked-monofor the mono caption.
Email wrapper
tr.RegisterEmailWrapper("noir", NoirEmailWrapper)— pure black canvas, inline 600px column table, Tenor Sans masthead (18px, letter-spacing +0.05em), 16:10 cover image (600×375), mono caption strip with copyright + unsubscribe.- All inline styles; no
<style>block (one minor<head><title>only). - Falls back to hex literals when
EmailContext.Colorsis empty so the email always renders without theme CSS variables.
Master pages (2)
noir:default-master—default,articletemplates. Blocks in spec order:navbar(variant: minimal),slot(slotName: main),noir:caption_strip,noir:footer.noir:gallery-master—landing,full-widthtemplates. Usesnavbarvariantdissolvedand addsnoir:contact_sheet_footeras the footer-rail block. (noir:contact_sheet_footeris referenced as a block key per spec §6; it is not currently provisioned as a separate block in this pass — see open items.)
Presets (3, all 19 tokens)
pure-noir— modedark, singledarkColorsblock. Pure black ground, white primary, silver accent.silver-print— modelight, singlelightColorsblock. Bone white ground, charcoal type.platinum— modeboth, bothlightColorsanddarkColorsblocks per UAT §5.- Every value is an HSL triple string (no
hsl(…)wrapper); verified byjqagainst^\d+ \d+% \d+%$.
Fonts policy
fonts.json = []perthemes/docs/FONTS.mdwave-1 policy.- CSS fallback stacks for
--font-heading(Tenor Sans → Georgia → serif),--font-body(Inter → system sans),--font-mono(JetBrains Mono → Consolas → SFMono-Regular → monospace) live inassets/style.css. RECOMMENDED_FONTS.mdwritten with picker instructions for the three Google Fonts.LICENSES.mddeliberately omitted (nothing bundled), per FONTS.md wave-1 §4.
Custom CSS (assets/style.css)
.tracked-mono— 10px JetBrains Mono fallback, 0.18em tracking, uppercase, line-height 1.4. Also.tracked-mono-smat 11px..bleed—width: 100vw+margin-left: calc((100vw - 100%) / 2 * -1). The UAT §13.7 grep is satisfied:calc((100vw - 100%) / 2)substring appears once..hairline— 1px border at 40% alpha against--border..noir-btn— hairline outline button with keyboard:focus-visiblering on--ring..noir-caption-strip— full-width 10px-strip flex container..noir-sprocket— sprocket-hole motif viaradial-gradientpseudo-elements at top and bottom.- Lightbox overlay —
[data-noir-lightbox]styledposition: fixed; inset: 0; width: 100vw; height: 100vhwitharia-hidden-driven visibility, Esc/click-outside dismiss, and aprefers-reduced-motionopt-out.
Build output
$ cd /home/alex/src/blockninja/themes/noir
$ go mod tidy # OK, go.sum populated, block/core v0.11.1 resolved
$ templ generate # OK, 9 *_templ.go produced
$ make # OK
$ ls -lh noir.so
-rw-rw-r-- 1 alex alex 21M noir.so # ~21 MB, expected for templ + bn helper compile
noir.so was produced via:
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o noir.so .
Safety check
$ cd /home/alex/src/blockninja/check-safety
$ go run . /home/alex/src/blockninja/themes/noir \
--plugin-dir /home/alex/src/blockninja/themes/noir
# … all checks OK or SKIP for noir, exit code 0
Notable results:
- Check 2c (Standalone plugin SDK import boundaries): OK.
go.moddoes not locally replaceblock/core; onlyblock/core/...is imported. - Check 3 (
go vet, golangci-lint, strict lint): OK for the single noir module. - Check 6 (No hardcoded colors): OK across
.templ,.ninjatpl, and.css. All colour values usehsl(var(--token))or HSL triples inpresets.json. - Check 11 (No placeholder code): OK.
- Check 17 (No TODO markers): OK.
- Check 21 (Plugin presets validation): OK — presets unmarshal cleanly against
theme.Theme. - Check 2e (Warn on any usage): 30 warnings — same shape as gotham/lcars baseline (the public
BlockFuncandMasterPageBlock.Contentsignatures aremap[string]anyin the SDK itself, so the warnings are inherent to the API). No failure.
Note: the task description references
~/src/blockninja/backend/cmd/check-safety, but on this checkout the binary lives at~/src/blockninja/check-safety/. The equivalent invocation is shown above and exits 0.
Open items / deferred
- Real woff2 files — deferred to wave-2 per FONTS.md §5.
fonts.json = [], fallbacks in CSS,RECOMMENDED_FONTS.mddocuments the picker path. noir:contact_sheet_footerblock — referenced bynoir:gallery-master(per spec §6) but not implemented as a distinct block in this pass; spec §8 lists six blocks and this would be a seventh. The master page entry is kept (so the seeder can find the block once added) but the gallery footer currently falls back to an empty contact-sheet item array. Build does not fail; the block resolves to the existingnoir:contact_sheetregistration when wired by the host. Track for follow-up.- Demo content / "Atelier Vance" seed — not part of theme-plugin scope in this pass.
- Marketplace screenshots (6 frames) — require a running
instance-noircontainer; not produced here. - Email render testing in Litmus / Apple Mail / Outlook 365 — out of scope without a live SMTP test.
LICENSES.md— intentionally omitted per FONTS.md wave-1 §4 (nothing bundled).- Lightbox JS hardening — vanilla keyboard-trap is implemented but a full focus-trap (Tab cycling within the overlay) is left as a wave-2 polish item; current behaviour traps Esc + click-outside + Enter/Space activation.
make rebuildworkflow — deliberately omitted from the Makefile per task brief; the deploy targets live ingotham/Makefileand can be lifted across when the theme is ready to land in the live CMS container.- Versioned git tag —
plugin.mod version = "0.1.0"is set but no git tag is created (theme directory is not a git repo on its own).
Counts
- Page templates registered: 4 (
default,landing,article,full-width). - Theme-specific blocks registered: 6 (
lightbox_gallery,contact_sheet,case_study,caption_strip,image_pair,footer). - Template overrides registered: 5 (
heading,text,image,button,card). - Email wrappers registered: 1 (
noir). - Master pages provisioned: 2 (
noir:default-master,noir:gallery-master). - Presets: 3 (
pure-noir,silver-print,platinum). - Schemas: 6 (one per theme-specific block).
- Bundled fonts: 0 (wave-1 policy).