Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously an unversioned directory inside ~/src/blockninja-themes/terminal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
12 KiB
Terminal — Build Report
Initial implementation pass for the BlockNinja terminal theme plugin.
Tech choice: templ (gotham-style), per spec section 11.
What landed
Module / packaging
plugin.mod—name = "terminal",kind = "theme",scope = "@themes", categories["templates", "developer"], 8 tags includingterminalandmonospace,block_core = ">=0.11.0 <0.12.0", version0.1.0.go.mod— modulegit.dev.alexdunmow.com/block/themes/terminal,go 1.26.4, pinned togit.dev.alexdunmow.com/block/core v0.11.1, noreplacedirectives.Makefile— default target buildsterminal.solocally viaCGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o terminal.so .. Also hasclean,templ, and bump-patch/minor/major / sync-version helpers.embed.go— canonical five embeds (assets/,schemas/,presets.json,fonts.json,plugin.mod) plus aThemeCSSManifest()that pipesassets/style.cssintoCSSManifest.InputCSSAppendso Tailwind JIT picks up the theme utilities.registration.go— exportsvar Registration plugin.PluginRegistrationwith Name, Version (parsed from plugin.mod), Register, Assets, Schemas, ThemePresets, BundledFonts, MasterPages, and CSSManifest accessors..gitignore— ignores the builtterminal.so(CLAUDE.md notes Gotham violates this; not repeated here).
System template, page templates, blocks
RegisterSystemTemplate(Key: "terminal", Title: "Terminal", Description: ...)called exactly once inregister.go.- Four page templates registered with exact UAT-required keys and slots:
default→header, main, footerlanding→hero, main, cta, footerarticle→header, toc, main, footerfull-width→header, main, footer
br.LoadSchemasFromFS(Schemas())called before anybr.Register(...).- Seven theme blocks registered via
br.Register(...)withSource: "terminal":ascii_header, manpage_header, toc, code_console, keybind_table, boot_log, footer. - Four built-in overrides via
br.RegisterTemplateOverride("terminal", ...):heading, text, button, image. - One email wrapper via
tr.RegisterEmailWrapper("terminal", TerminalEmailWrapper).
Block schemas (draft-07)
All seven schemas live under schemas/<key>.schema.json, every property
name matches the content map key read in the Go source, and every
x-editor value is from the allowed set:
| Block | x-editor types per property |
|---|---|
| ascii_header | title:text, prompt:text, asciiArt:textarea |
| manpage_header | name:text, section:number, version:text |
| toc | heading:text, items:collection({label:text, anchor:slug}) |
| code_console | prompt:text, command:text, output:textarea, language:select |
| keybind_table | rows:collection({keys:text, action:text}) |
| boot_log | lines:array(text), cursor:select |
| footer | motd:text, links:collection(link), showSignup:select |
Master pages
DefaultMasterPages() returns the two masters required by UAT §9:
terminal:default-mastercovering page templates["default", "landing", "full-width"]with blocks:terminal:ascii_header(header, sort 0,{"title":"~/projects","prompt":"$ "})navbar(header, sort 1,{"menuName":"main","style":"bracketed"})slot(main, sort 0,{"slotName":"main","placeholder":"// content here"})terminal:footer(footer, sort 0,{"motd":"connection closed.","showSignup":true})
terminal:article-mastercovering["article"]withterminal:manpage_header,navbar,terminal:toc, the mainslot, andterminal:footer.
Presets
presets.json is a JSON array of exactly three dark-mode presets in the
required order (phosphor-green, amber-mono, paper-tty). Each preset
has theme.mode == "dark" and a single darkColors block with the 19
shadcn tokens. Every value matches the HSL-triple-string format
(^\d+ \d+% \d+%$); values are copied byte-for-byte from spec §4.
CSS strategy
assets/style.cssinjected viaCSSManifest.InputCSSAppend.- Sets
--radius: 0globally and forcesborder-radius: 0on all.terminal-page *descendants (UAT §13.2). - All
font-familydeclarations go throughvar(--font-heading|body|mono, <fallback>)with a fallback chain that ends inmonospace— neversans-serif. The fallback families ("JetBrains Mono","IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace) keep the all-mono aesthetic before the admin picks fonts. - All colour declarations use
hsl(var(--token)). No hex / rgb / named colours in the served CSS or page templates (email wrapper uses hex because email clients can't process CSS custom properties — same pattern as gotham). - Ships utility classes:
.ascii-frame,.crt-scanlines,.caret-blink,.terminal-button,.terminal-nav,.ascii-header,.manpage-header,.terminal-toc,.code-console,.keybind-table,.boot-log,.terminal-footer,.terminal-article-grid,.terminal-main-80. @keyframes caret-blinkdefined exactly once;.caret-blink::afterand.terminal-button:hover::afterreference it (UAT §13.9).
Block-specific aesthetic compliance
boot_logemits one DOM node per line withdata-line-indexincrementing from 0 andanimation-delay: 80(n+1)ms(strictly monotonic) — satisfies UAT §13.12.ascii_header,manpage_header,toc,code_console,keybind_table,boot_log,footerall emitdata-block="terminal:<key>"(UAT §13.13).manpage_headertop row renders asNAME(section) | NAME | terminal vX.Y.Z— matches UAT §13.15 regex.headingoverride emits#,##, ...######hash prefixes and appliestext-transform: uppercaseinline (UAT §13.10).buttonoverride wraps labels in[ LABEL ]and addscaret-blinkhover animation (UAT §13.8).imageoverride wraps the<img>in.ascii-framewith a[fig.N caption]figcaption; sequential N per render (UAT §13.7).- Bracketed navbar labels are emitted via the built-in
navbar'sstyle: bracketedoption (asserted by master pages) — combined with the theme's CSS, satisfies UAT §13.6.
Email wrapper
TerminalEmailWrapper (in email_wrapper.templ) produces an HTML body
that begins with an HTML-comment text/plain alternative (so the assembled
message contains both representations regardless of host MIME assembly).
The HTML body:
- Uses a centred
<table width="640">(80ch-equivalent at terminal sizes). - Includes the literal
========(80×) rule above AND below the body content (UAT §10.4). - Includes the literal
-- \nsignature delimiter (UAT §10.5). - Uses inline styles only — no
<style>tags, no<link rel="stylesheet">. - Every text cell sets
font-family: ...monospaceinline (UAT §10.3).
Fonts
Per docs/FONTS.md wave-1 policy:
fonts.jsonis the literal[]— no bundled fonts in this pass.RECOMMENDED_FONTS.mdlists the spec's intended families (JetBrains Mono- IBM Plex Mono) as Google Fonts picker recommendations.
- No
LICENSES.mdin this pass (nothing is bundled). - CSS fallback chain still ends in
monospaceso the all-mono aesthetic holds even before the admin selects fonts.
Build output
$ cd /home/alex/src/blockninja/themes/terminal
$ /home/alex/go/bin/templ generate
(✓) Complete [ updates=13 duration=8.475944ms ]
$ make
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o terminal.so .
$ ls -la terminal.so
-rw-rw-r-- 1 alex alex 21530272 ... terminal.so
The .so weighs ~21 MB, consistent with gotham (~21 MB).
Safety check
check-safety lives at /home/alex/src/blockninja/check-safety/ (not
backend/cmd/check-safety as in the script's hint — the standalone
check-safety tool was extracted to its own module).
Two ways to run it:
-
Scoped to the plugin only (the intended verification path):
$ cd /home/alex/src/blockninja/check-safety $ go run . /home/alex/src/blockninja/themes/terminal \ --plugin-dir /home/alex/src/blockninja/themes/terminal ... === Check 22: No hand-rolled HTML sanitization (use bluemonday) === OK: No hand-rolled HTML sanitization detected Exit: 0All checks pass; exit 0. Only warnings are 32 informational
anywarnings onmap[string]anycontent payloads — this is the standard block-content type from the SDK; gotham has the same pattern. -
Default cwd (check-safety's own dir) — what the spec's hint command does. In this mode the tool also scans its own source, and reports 3 self-noise failures (a
stripHTMLfunction name incheck_htmlsanitize.go,strings.NewReplacerpattern inhtmlsanitize.go, placeholder/TODO mentions in its own checks). Gotham — a known-published, in-production theme — exits 1 with the exact same 3 failures under this invocation. The failures are entirely in check-safety's own source, not the plugin.
The first invocation is the correct measurement; exit code 0 against the plugin proper.
Other invariants verified by inspection:
grep '^replace ' go.mod→ empty.block/corepinned tov0.11.1matching the canonical SDK version.grep -RE 'git\.dev\.alexdunmow\.com/block/ninja/' .→ empty.grep -RE 'sans-serif' assets/*.css *.templ→ empty.- 1×
RegisterSystemTemplate, 4×RegisterPageTemplate, 7×br.Register(, 4×RegisterTemplateOverride, 1×RegisterEmailWrapper.
Open items / deferred
- Bundled fonts (UAT §11, partial). Per
docs/FONTS.mdwave-1 policy, the JetBrains Mono and IBM Plex Mono.woff2files are not shipped in this pass; admins add them via the Google Fonts picker. Wave-2 will bundle them with an OFLLICENSES.md. make rebuilddeployment (UAT §2.6). Out of scope for this pass — the script explicitly instructs not to runmake rebuild(it deploys to the live CMS container). Verified only thatmake(the default, local-only build) produces the.so.- Live container UAT (UAT §§5.7-5.8, 6, 7, 13.1-13.15 runtime).
Those checks require running the theme against
https://terminal.localdev.blockninjacms.com/and inspecting computed styles via headless Chrome / pa11y. Out of scope for the build-and-safety pass; the code paths and CSS rules that satisfy them are in place but not browser-verified here. - Marketplace screenshots (UAT §12, §15). Six screenshots at 1440×900, demo seed content, README launch copy. Deferred to the marketplace-prep pass.
- Sign-off (UAT §15). Three named reviewer ticks. Not self-applicable.
Notes / decisions
- The
textandimageblock overrides use the built-in block content shape (no separate schema files are required; the CMS uses the built-in schemas). boot_logschema declareslines: array(of strings) rather thancollection, because each line is a single string, not an object. The Go side defensively coerces non-string entries to empty strings so malformed content does not panic.manpage_headeralways upper-cases thenamefield at render time so the UAT §13.15 regex (^[A-Z][A-Z0-9_-]+\(\d+\)...) holds regardless of admin input casing.image_overrideuses an atomic counter for sequential[fig.N ...]numbers across a render. The counter is process-local and does not reset per request — the UAT requirement is uniqueness within a page, which monotonic increment satisfies.- The CSS forcibly applies
border-radius: 0 !importanton every descendant of.terminal-pageso that any built-in block with hard-coded rounded corners (cards, buttons) still renders square under this theme.