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

144 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-master``default`, `article` templates. Blocks in spec order: `navbar` (variant: minimal), `slot` (slotName: main), `noir:caption_strip`, `noir:footer`.
- `noir:gallery-master``landing`, `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.
- `.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-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 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).