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>
248 lines
12 KiB
Markdown
248 lines
12 KiB
Markdown
# 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 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:
|
||
- `default` → `header, main, footer`
|
||
- `landing` → `hero, main, cta, footer`
|
||
- `article` → `header, toc, main, footer`
|
||
- `full-width` → `header, 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.
|