themes-terminal/BUILD_REPORT.md
Alex Dunmow 0a9b177f7c initial: theme plugin terminal
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>
2026-06-06 14:11:44 +08:00

12 KiB
Raw Permalink Blame History

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.modname = "terminal", kind = "theme", scope = "@themes", categories ["templates", "developer"], 8 tags including terminal and monospace, block_core = ">=0.11.0 <0.12.0", version 0.1.0.
  • go.mod — module git.dev.alexdunmow.com/block/themes/terminal, go 1.26.4, pinned to git.dev.alexdunmow.com/block/core v0.11.1, no replace directives.
  • Makefile — default target builds terminal.so locally via CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o terminal.so .. Also has clean, templ, and bump-patch/minor/major / sync-version helpers.
  • embed.go — canonical five embeds (assets/, schemas/, presets.json, fonts.json, plugin.mod) plus a ThemeCSSManifest() that pipes assets/style.css into CSSManifest.InputCSSAppend so Tailwind JIT picks up the theme utilities.
  • registration.go — exports var Registration plugin.PluginRegistration with Name, Version (parsed from plugin.mod), Register, Assets, Schemas, ThemePresets, BundledFonts, MasterPages, and CSSManifest accessors.
  • .gitignore — ignores the built terminal.so (CLAUDE.md notes Gotham violates this; not repeated here).

System template, page templates, blocks

  • RegisterSystemTemplate(Key: "terminal", Title: "Terminal", Description: ...) called exactly once in register.go.
  • Four page templates registered with exact UAT-required keys and slots:
    • defaultheader, main, footer
    • landinghero, main, cta, footer
    • articleheader, toc, main, footer
    • full-widthheader, main, footer
  • br.LoadSchemasFromFS(Schemas()) called before any br.Register(...).
  • Seven theme blocks registered via br.Register(...) with Source: "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-master covering 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-master covering ["article"] with terminal:manpage_header, navbar, terminal:toc, the main slot, and terminal: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.css injected via CSSManifest.InputCSSAppend.
  • Sets --radius: 0 globally and forces border-radius: 0 on all .terminal-page * descendants (UAT §13.2).
  • All font-family declarations go through var(--font-heading|body|mono, <fallback>) with a fallback chain that ends in monospace — never sans-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-blink defined exactly once; .caret-blink::after and .terminal-button:hover::after reference it (UAT §13.9).

Block-specific aesthetic compliance

  • boot_log emits one DOM node per line with data-line-index incrementing from 0 and animation-delay: 80(n+1)ms (strictly monotonic) — satisfies UAT §13.12.
  • ascii_header, manpage_header, toc, code_console, keybind_table, boot_log, footer all emit data-block="terminal:<key>" (UAT §13.13).
  • manpage_header top row renders as NAME(section) | NAME | terminal vX.Y.Z — matches UAT §13.15 regex.
  • heading override emits # , ## , ... ###### hash prefixes and applies text-transform: uppercase inline (UAT §13.10).
  • button override wraps labels in [ LABEL ] and adds caret-blink hover animation (UAT §13.8).
  • image override wraps the <img> in .ascii-frame with a [fig.N caption] figcaption; sequential N per render (UAT §13.7).
  • Bracketed navbar labels are emitted via the built-in navbar's style: bracketed option (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 -- \n signature delimiter (UAT §10.5).
  • Uses inline styles only — no <style> tags, no <link rel="stylesheet">.
  • Every text cell sets font-family: ...monospace inline (UAT §10.3).

Fonts

Per docs/FONTS.md wave-1 policy:

  • fonts.json is the literal [] — no bundled fonts in this pass.
  • RECOMMENDED_FONTS.md lists the spec's intended families (JetBrains Mono
    • IBM Plex Mono) as Google Fonts picker recommendations.
  • No LICENSES.md in this pass (nothing is bundled).
  • CSS fallback chain still ends in monospace so 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:

  1. 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: 0
    

    All checks pass; exit 0. Only warnings are 32 informational any warnings on map[string]any content payloads — this is the standard block-content type from the SDK; gotham has the same pattern.

  2. 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 stripHTML function name in check_htmlsanitize.go, strings.NewReplacer pattern in htmlsanitize.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/core pinned to v0.11.1 matching 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.md wave-1 policy, the JetBrains Mono and IBM Plex Mono .woff2 files are not shipped in this pass; admins add them via the Google Fonts picker. Wave-2 will bundle them with an OFL LICENSES.md.
  • make rebuild deployment (UAT §2.6). Out of scope for this pass — the script explicitly instructs not to run make rebuild (it deploys to the live CMS container). Verified only that make (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 text and image block overrides use the built-in block content shape (no separate schema files are required; the CMS uses the built-in schemas).
  • boot_log schema declares lines: array (of strings) rather than collection, 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_header always upper-cases the name field at render time so the UAT §13.15 regex (^[A-Z][A-Z0-9_-]+\(\d+\)...) holds regardless of admin input casing.
  • image_override uses 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 !important on every descendant of .terminal-page so that any built-in block with hard-coded rounded corners (cards, buttons) still renders square under this theme.