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>
144 lines
9.9 KiB
Markdown
144 lines
9.9 KiB
Markdown
# 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).
|