themes-noir/BUILD_REPORT.md
Alex Dunmow 1bebbea5ad initial: theme plugin noir
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>
2026-06-06 14:11:40 +08:00

9.9 KiB
Raw Permalink Blame History

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, kind theme, version 0.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 — pins block/core v0.11.1 and templ v0.3.1020, mirrors gotham's indirect set, no replace directives.
  • Makefile — local-only targets (all, templ, clean). No rebuild target. Default builds noir.so via CGO_ENABLED=1 go build -buildmode=plugin.
  • embed.go — five canonical //go:embed directives plus a ThemeCSSManifest() that surfaces assets/style.css through CSSManifest.InputCSSAppend so the custom utilities (.tracked-mono, .bleed, .hairline, sprocket motif, lightbox overlay) survive Tailwind's content scanner.
  • registration.go — exports var Registration plugin.PluginRegistration with 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.templ and consumes the shared bn.Head / bn.AdminBypassBanner / bn.BodyEnd helpers, 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-number attribute 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-editor value comes from the allowed set: text, media, select, number, array, link.
  • lightbox_gallery.columns uses x-editor: select with enum: [2, 3, 4] (spec §8).
  • case_study.year uses x-editor: number (spec §8).
  • case_study.credits is array<string>; case_study.images is array<media> per spec §8.
  • footer.social is array<link> with text/url fields per spec §8.

Template overrides (5)

  • RegisterTemplateOverride("noir", …) for heading, 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 .bleed utility for full-bleed and a figcaption.tracked-mono for 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.Colors is empty so the email always renders without theme CSS variables.

Master pages (2)

  • noir:default-masterdefault, article templates. Blocks in spec order: navbar (variant: minimal), slot (slotName: main), noir:caption_strip, noir:footer.
  • noir:gallery-masterlanding, full-width templates. Uses navbar variant dissolved and adds noir:contact_sheet_footer as the footer-rail block. (noir:contact_sheet_footer is 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 — mode dark, single darkColors block. Pure black ground, white primary, silver accent.
  • silver-print — mode light, single lightColors block. Bone white ground, charcoal type.
  • platinum — mode both, both lightColors and darkColors blocks per UAT §5.
  • Every value is an HSL triple string (no hsl(…) wrapper); verified by jq against ^\d+ \d+% \d+%$.

Fonts policy

  • fonts.json = [] per themes/docs/FONTS.md wave-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 in assets/style.css.
  • RECOMMENDED_FONTS.md written with picker instructions for the three Google Fonts.
  • LICENSES.md deliberately 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-sm at 11px.
  • .bleedwidth: 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-visible ring on --ring.
  • .noir-caption-strip — full-width 10px-strip flex container.
  • .noir-sprocket — sprocket-hole motif via radial-gradient pseudo-elements at top and bottom.
  • Lightbox overlay — [data-noir-lightbox] styled position: fixed; inset: 0; width: 100vw; height: 100vh with aria-hidden-driven visibility, Esc/click-outside dismiss, and a prefers-reduced-motion opt-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.mod does not locally replace block/core; only block/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 use hsl(var(--token)) or HSL triples in presets.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 BlockFunc and MasterPageBlock.Content signatures are map[string]any in 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

  1. Real woff2 files — deferred to wave-2 per FONTS.md §5. fonts.json = [], fallbacks in CSS, RECOMMENDED_FONTS.md documents the picker path.
  2. noir:contact_sheet_footer block — referenced by noir: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 existing noir:contact_sheet registration when wired by the host. Track for follow-up.
  3. Demo content / "Atelier Vance" seed — not part of theme-plugin scope in this pass.
  4. Marketplace screenshots (6 frames) — require a running instance-noir container; not produced here.
  5. Email render testing in Litmus / Apple Mail / Outlook 365 — out of scope without a live SMTP test.
  6. LICENSES.md — intentionally omitted per FONTS.md wave-1 §4 (nothing bundled).
  7. 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.
  8. make rebuild workflow — deliberately omitted from the Makefile per task brief; the deploy targets live in gotham/Makefile and can be lifted across when the theme is ready to land in the live CMS container.
  9. Versioned git tagplugin.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).