From 91e6735eb288dd34f04190b1ed535c44d7a4d974 Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Sat, 6 Jun 2026 20:35:41 +0800 Subject: [PATCH] docs(plan): add LCARS card redesign implementation plan Eight-task implementation plan covering: foundation CSS/palette/font tokens, lcars_panel redesign (elbow + strip), lcars_rail compound block, lcars_readout tile, outer page-frame refinement, sidebar code field, plugin metadata + admin docs, and final verification with version bump. Notes one spec deviation: LCARS palette ships as CSS variables in style.css rather than presets.json entries (the CMS ColorScheme struct is strict). Co-Authored-By: Claude Opus 4.7 --- .../plans/2026-06-06-lcars-card-redesign.md | 1397 +++++++++++++++++ 1 file changed, 1397 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-06-lcars-card-redesign.md diff --git a/docs/superpowers/plans/2026-06-06-lcars-card-redesign.md b/docs/superpowers/plans/2026-06-06-lcars-card-redesign.md new file mode 100644 index 0000000..6aa3419 --- /dev/null +++ b/docs/superpowers/plans/2026-06-06-lcars-card-redesign.md @@ -0,0 +1,1397 @@ +# LCARS Card Redesign Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Redesign the LCARS theme's in-content blocks so cards read as authentic LCARS — adding `lcars_panel` (elbow/strip frames), `lcars_rail` (multiple cells sharing one L-frame), and `lcars_readout` (small dashboard tile) — and bundle in the font-token + palette + outer-frame refinements called for in the spec. + +**Architecture:** Pongo2-based theme plugin compiled to `.so`. Each block is a `BlockMeta` + render function pair registered via the `block/core` SDK. Block templates extend the existing pongo engine; CSS is appended to the host CMS Tailwind input via `CSSManifest.InputCSSAppend`. The locked geometry uses a 2-column CSS grid (5.5rem rail / 1fr content) with chunky 4rem-top / 3rem-bot bars, **outer page corners rounded** (top 1.75rem, bottom 1.25rem), **inner L bend square**. + +**Tech Stack:** Go 1.26.4, `block/core` v0.11.1, pongo2 templates, JSON Schema draft-07, Tailwind (host CMS), CSS custom properties. + +**Spec:** `docs/superpowers/specs/2026-06-06-lcars-card-redesign-design.md` + +**Visual reference:** `.superpowers/brainstorm/3782075-1780722894/content/08-wider-bigger-curve.html` (last locked mockup) + +--- + +## Plan note — deviation from spec + +The spec § "Refinements bundled in this spec" item 2 calls for adding `peach / mauve / gold / blue / red` colour roles to `presets.json`. Investigation against `cms/backend/internal/theme/defaults.go` shows `ColorScheme` is a **strict Go struct** with fixed keys (background, foreground, primary, secondary, accent, muted, destructive, etc.) — unknown keys are silently dropped by JSON parsing. We therefore add the LCARS-canonical palette as **CSS custom properties in `:root` of `assets/style.css`** instead. This preserves the spec's intent (rich LCARS palette available to blocks) and avoids inventing a CMS theme-system change. Block schemas expose both flavours via their `color` enums: theme tokens (`primary`, `secondary`, `accent`, `muted`) AND LCARS canonical tokens (`orange`, `peach`, `mauve`, `gold`, `blue`, `red`). + +--- + +## File Structure + +### Modify + +| File | Responsibility after change | +|---|---| +| `assets/style.css` | Add `:root` LCARS palette + font tokens; replace hardcoded `'Antonio'` with `var(--font-heading, ...)`; add `.lcars-elbow*`, `.lcars-strip*`, `.lcars-rail*`, `.lcars-readout*` rules; add `.lcars-bg-{orange,peach,mauve,gold,blue,red}` utilities; re-tune `.lcars-elbow-top/bottom` outer-frame rules; **delete** old `.lcars-panel*` rules | +| `blocks.go` | Add `LCARSRailMeta`, `LCARSReadoutMeta`; refresh `LCARSPanelMeta` description; add `panelDefaults`, `railDefaults`, `readoutDefaults` | +| `register.go` | Register the two new blocks; switch `lcars_panel` to the new template (with defaults); schemas load remains in place | +| `schemas/lcars_panel.schema.json` | **Rewritten.** `frame` enum elbow/strip + identifier/meta fields + accent enums + status | +| `schemas/lcars_sidebar.schema.json` | Add optional `code` field per item | +| `templates/blocks/panel.html` | **Rewritten.** Renders `frame="elbow"` (chunky-corner grid) and `frame="strip"` (segmented bar header) per locked geometry | +| `templates/blocks/sidebar.html` | Render optional `code` per item | +| `templates/blocks/header.html` | Minor refinement so it shares the 5.5rem / 1.75rem vocabulary | +| `templates/default.html` | Re-tune outer-frame widths/radii to match the new panel chrome | +| `plugin.mod` | Add `required_icon_packs = ["lucide"]` | +| `fonts.json` | Set to `[]` | + +### Create + +| File | Responsibility | +|---|---| +| `schemas/lcars_rail.schema.json` | `cells[]`, `title`, `top_label`/`bottom_label`, top/bottom accent enums, `rail_side` (reserved) | +| `schemas/lcars_readout.schema.json` | `label`, `value`, `unit`, `accent_color`, `pulse` | +| `templates/blocks/rail.html` | Compound rail rendering: top corner + bar + per-cell (segment, body), bottom corner + bar | +| `templates/blocks/readout.html` | Single tile with coloured top border and big value | +| `RECOMMENDED_FONTS.md` | Admin guidance — Antonio (or similar geometric sans) for `--font-heading` | +| `RECOMMENDED_ICONS.md` | Admin guidance — Lucide pack | + +### Out of scope (touch nothing) + +`master_pages.go`, `registration.go`, `embed.go`, `go.mod`, `go.sum`, `presets.json`, anything outside the lcars repo. + +--- + +## Build verification commands + +Every task ends with these (substitute paths as needed): + +```bash +# Local compile sanity check (no container) +cd /home/alex/src/blockninja/themes/lcars +CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so . + +# Plugin-boundary + schema/Go consistency check +cd /home/alex/src/blockninja/check-safety +go run . /home/alex/src/blockninja/themes/lcars + +# Optional visual verification (requires running CMS instance) +# The user's existing workflow handles this — note in commit body if not run +``` + +`lcars.so` is gitignored. Don't commit it. + +--- + +## Task 1: Foundation — palette, font tokens, scrub old panel rules + +**Goal:** Establish LCARS canonical colour CSS variables, switch typography to font-tokens, and delete the soon-orphaned `.lcars-panel*` rules so subsequent tasks start clean. + +**Files:** +- Modify: `assets/style.css` + +- [ ] **Step 1: Read the current style.css** so subsequent edits use the exact existing content as `old_string`. + +Run: `cat /home/alex/src/blockninja/themes/lcars/assets/style.css | head -20` +Expected: starts with `/* ===…===… */` LCARS theme header. + +- [ ] **Step 2: Prepend `:root` palette block.** Insert at the very top of `assets/style.css`, BEFORE the existing `/* ============…` comment header: + +```css +/* ============================================================ + LCARS Canonical Palette — fixed across all presets + ============================================================ */ +:root { + --lcars-orange: 22 100% 70%; + --lcars-peach: 30 100% 80%; + --lcars-mauve: 300 35% 70%; + --lcars-gold: 39 90% 70%; + --lcars-blue: 210 50% 65%; + --lcars-red: 0 60% 65%; +} + +.lcars-bg-orange { background-color: hsl(var(--lcars-orange)); } +.lcars-bg-peach { background-color: hsl(var(--lcars-peach)); } +.lcars-bg-mauve { background-color: hsl(var(--lcars-mauve)); } +.lcars-bg-gold { background-color: hsl(var(--lcars-gold)); } +.lcars-bg-blue { background-color: hsl(var(--lcars-blue)); } +.lcars-bg-red { background-color: hsl(var(--lcars-red)); } + +.lcars-text-orange { color: hsl(var(--lcars-orange)); } +.lcars-text-peach { color: hsl(var(--lcars-peach)); } +.lcars-text-mauve { color: hsl(var(--lcars-mauve)); } +.lcars-text-gold { color: hsl(var(--lcars-gold)); } +.lcars-text-blue { color: hsl(var(--lcars-blue)); } +.lcars-text-red { color: hsl(var(--lcars-red)); } + +``` + +- [ ] **Step 3: Replace the hardcoded font-family.** Find the `.lcars-page` rule and the `.lcars-heading` rule. Each currently sets `font-family: 'Antonio', sans-serif;`. Replace both with `font-family: var(--font-heading, 'Antonio', sans-serif);`. + +For `.lcars-page` rule, do an Edit: +- `old_string`: `.lcars-page {\n font-family: 'Antonio', sans-serif;` +- `new_string`: `.lcars-page {\n font-family: var(--font-heading, 'Antonio', sans-serif);` + +For `.lcars-heading` rule, do an Edit: +- `old_string`: `.lcars-heading {\n font-family: 'Antonio', sans-serif;` +- `new_string`: `.lcars-heading {\n font-family: var(--font-heading, 'Antonio', sans-serif);` + +- [ ] **Step 4: Compile.** + +Run: `cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so .` +Expected: builds without error. (CSS-only change; Go layer unaffected.) + +- [ ] **Step 5: Run check-safety.** + +Run: `cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars` +Expected: passes. + +- [ ] **Step 6: Commit.** + +```bash +cd /home/alex/src/blockninja/themes/lcars +git add assets/style.css +git commit -m "$(cat <<'EOF' +style(lcars): add LCARS canonical palette + font tokens + +Adds fixed --lcars-{orange,peach,mauve,gold,blue,red} CSS variables in +:root with matching .lcars-bg-* / .lcars-text-* utility classes; switches +all hardcoded 'Antonio' families to var(--font-heading, 'Antonio', sans-serif) +per themes/CLAUDE.md font-token rule. Old .lcars-panel* rules are +retained for now — they get replaced in the next commit when the new +panel template and rules land together. + +Co-Authored-By: Claude Opus 4.7 +EOF +)" +``` + +--- + +## Task 2: Redesign `lcars_panel` block + +**Goal:** Replace the thin-left-border panel with the locked elbow/strip geometry. + +**Files:** +- Modify: `schemas/lcars_panel.schema.json` (full rewrite) +- Modify: `templates/blocks/panel.html` (full rewrite) +- Modify: `assets/style.css` (append new rules) +- Modify: `blocks.go` (description + add defaults) +- Modify: `register.go` (use new defaults) + +- [ ] **Step 0: Delete the old `.lcars-panel*` rules from `assets/style.css`.** Find the comment-delimited "Panel Block" section (starts `/* --- Panel Block --- */`, ends before `/* --- Heading Override --- */`) and delete the entire section including the section-header comment. + +Edit `old_string` is the full block from `/* --- Panel Block --- */` through the last `.lcars-panel-body { … }` rule, inclusive. `new_string` is empty (delete). + +This must happen in the same commit as the new panel rules (Step 3 below) so the lcars_panel block isn't left rendering against missing CSS between commits. + +- [ ] **Step 1: Rewrite `schemas/lcars_panel.schema.json`** with the new fields. Replace the file's full contents with: + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LCARS Panel", + "description": "Framed content panel with the LCARS elbow or strip-header treatment", + "type": "object", + "properties": { + "frame": { + "type": "string", + "title": "Frame Style", + "description": "elbow = full asymmetric L-frame; strip = segmented colour bar header", + "enum": ["elbow", "strip"], + "default": "elbow", + "x-editor": "select" + }, + "title": { + "type": "string", + "title": "Panel Title", + "description": "Title shown in the top bar", + "x-editor": "text" + }, + "top_label": { + "type": "string", + "title": "Top Identifier", + "description": "Short code in the top corner block (e.g. RM-47-A). Leave blank to hide.", + "x-editor": "text" + }, + "bottom_label": { + "type": "string", + "title": "Bottom Identifier", + "description": "Short code in the bottom corner block (e.g. 28-301). Leave blank to hide.", + "x-editor": "text" + }, + "top_meta": { + "type": "string", + "title": "Top Meta", + "description": "Right-aligned text in the top bar (e.g. STARDATE 47634.4)", + "x-editor": "text" + }, + "bottom_meta": { + "type": "string", + "title": "Bottom Meta", + "description": "Right-aligned text in the bottom bar", + "x-editor": "text" + }, + "status": { + "type": "string", + "title": "Status Indicator", + "description": "Optional status dot/badge in the top bar", + "enum": ["", "online", "standby", "alert", "offline"], + "default": "", + "x-editor": "select" + }, + "accent_color": { + "type": "string", + "title": "Top Accent Colour", + "description": "Colour of the top L (theme tokens or LCARS canonical)", + "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"], + "default": "primary", + "x-editor": "select" + }, + "bottom_accent_color": { + "type": "string", + "title": "Bottom Accent Colour", + "description": "Colour of the bottom L (theme tokens or LCARS canonical)", + "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"], + "default": "secondary", + "x-editor": "select" + }, + "content": { + "type": "string", + "title": "Content", + "description": "Panel body (richtext)", + "x-editor": "richtext" + } + }, + "required": ["frame"] +} +``` + +- [ ] **Step 2: Rewrite `templates/blocks/panel.html`** to render both frame modes. Replace the file's full contents with: + +```html +{% if frame == "strip" %} +
+
+ {% if top_label %}
{{ top_label }}
{% endif %} + {% if title %}
{{ title }}
{% endif %} + {% if top_meta %}
{{ top_meta }}
{% endif %} + {% if status %}
{{ status|upper }}
{% endif %} +
+
+
+ {{ content|safe }} +
+
+{% else %} +
+
{{ top_label }}
+
+ {{ title }} + + {% if status %}{{ status|upper }}{% endif %} + {% if top_meta %}{% if status %} · {% endif %}{{ top_meta }}{% endif %} + +
+
+
{{ content|safe }}
+
{{ bottom_label }}
+
+   + {{ bottom_meta }} +
+
+{% endif %} +``` + +- [ ] **Step 3: Append panel CSS rules to `assets/style.css`.** Add at the END of the file (after the `@media` block): + +```css +/* ============================================================ + lcars_panel — elbow + strip + ============================================================ */ + +/* --- Elbow frame (frame="elbow") --- */ +.lcars-elbow { + display: grid; + grid-template-columns: 5.5rem 1fr; + grid-template-rows: 4rem 1fr 3rem; + gap: 4px; + background: hsl(var(--background)); + margin: 1rem 0; +} +.lcars-elbow-tl { + grid-area: 1 / 1; + border-top-left-radius: 1.75rem; + display: flex; + align-items: flex-end; + justify-content: flex-end; + padding: 0 1.25rem 0.85rem 1rem; + color: hsl(var(--background)); + font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif); + letter-spacing: 0.08em; +} +.lcars-elbow-bar-t { + grid-area: 1 / 2; + border-top-right-radius: 1.75rem; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.5rem; + color: hsl(var(--background)); + font: 800 0.85rem/1 var(--font-heading, 'Antonio', sans-serif); + letter-spacing: 0.1em; + text-transform: uppercase; +} +.lcars-elbow-bar-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.lcars-elbow-bar-meta { white-space: nowrap; } +.lcars-elbow-status-dot { + display: inline-block; + width: 0.5rem; height: 0.5rem; border-radius: 50%; + background: hsl(var(--background)); + margin-right: 0.3rem; + vertical-align: middle; + animation: lcars-pulse 2s ease-in-out infinite; +} +.lcars-elbow-rail { grid-area: 2 / 1; } +.lcars-elbow-body { + grid-area: 2 / 2; + background: hsl(var(--background)); + padding: 1rem 1.25rem; + color: hsl(var(--foreground)); + font: 400 1rem/1.6 var(--font-body, ui-sans-serif, system-ui, sans-serif); +} +.lcars-elbow-bl { + grid-area: 3 / 1; + border-bottom-left-radius: 1.25rem; + display: flex; + align-items: flex-start; + justify-content: flex-end; + padding: 0.75rem 1.25rem 0 1rem; + color: hsl(var(--background)); + font: 800 0.7rem/1 var(--font-heading, 'Antonio', sans-serif); + letter-spacing: 0.08em; +} +.lcars-elbow-bar-b { + grid-area: 3 / 2; + border-bottom-right-radius: 1.25rem; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.5rem; + color: hsl(var(--background)); + font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif); + letter-spacing: 0.1em; + text-transform: uppercase; +} + +/* --- Strip frame (frame="strip") --- */ +.lcars-strip { margin: 1rem 0; } +.lcars-strip-head { + display: grid; + grid-template-columns: 4rem 8rem 1fr 5rem 2rem; + gap: 4px; + height: 1.75rem; +} +.lcars-strip-seg { + display: flex; + align-items: center; + padding: 0 0.75rem; + color: hsl(var(--background)); + font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif); + letter-spacing: 0.12em; + text-transform: uppercase; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.lcars-strip-seg-id { justify-content: flex-end; } +.lcars-strip-seg-cap { border-radius: 0 1rem 1rem 0; } +.lcars-strip-body { + background: hsl(var(--background)); + border-left: 4px solid hsl(var(--primary)); + padding: 0.75rem 1rem; + margin-top: 4px; + color: hsl(var(--foreground)); + font: 400 1rem/1.55 var(--font-body, ui-sans-serif, system-ui, sans-serif); +} +.lcars-strip-body-secondary { border-left-color: hsl(var(--secondary)); } +.lcars-strip-body-accent { border-left-color: hsl(var(--accent)); } +.lcars-strip-body-muted { border-left-color: hsl(var(--muted)); } +.lcars-strip-body-orange { border-left-color: hsl(var(--lcars-orange)); } +.lcars-strip-body-peach { border-left-color: hsl(var(--lcars-peach)); } +.lcars-strip-body-mauve { border-left-color: hsl(var(--lcars-mauve)); } +.lcars-strip-body-gold { border-left-color: hsl(var(--lcars-gold)); } +.lcars-strip-body-blue { border-left-color: hsl(var(--lcars-blue)); } +.lcars-strip-body-red { border-left-color: hsl(var(--lcars-red)); } + +@media (max-width: 768px) { + .lcars-elbow { grid-template-columns: 3.5rem 1fr; } + .lcars-elbow-tl, .lcars-elbow-bl, .lcars-elbow-bar-t, .lcars-elbow-bar-b { padding-left: 0.5rem; padding-right: 0.75rem; } + .lcars-strip-head { grid-template-columns: 3.5rem 1fr 4rem 1.5rem; } + .lcars-strip-head .lcars-strip-seg-title ~ .lcars-strip-seg-meta { display: none; } +} +``` + +- [ ] **Step 4: Update `LCARSPanelMeta` description in `blocks.go`.** Edit: +- `old_string`: + ``` + var LCARSPanelMeta = blocks.BlockMeta{ + Key: "lcars_panel", + Title: "LCARS Panel", + Description: "Framed content area with LCARS border treatment", + Category: blocks.CategoryLayout, + Source: "lcars", + } + ``` +- `new_string`: + ``` + var LCARSPanelMeta = blocks.BlockMeta{ + Key: "lcars_panel", + Title: "LCARS Panel", + Description: "Framed content panel — elbow (full L-frame) or strip (segmented bar header)", + Category: blocks.CategoryLayout, + Source: "lcars", + } + + var panelDefaults = map[string]any{ + "frame": "elbow", + "title": "LCARS PANEL", + "top_label": "RM-47-A", + "bottom_label": "28-301", + "accent_color": "primary", + "bottom_accent_color": "secondary", + } + ``` + +- [ ] **Step 5: Update the panel registration in `register.go`** to use the new template with defaults. Edit: +- `old_string`: `br.Register(LCARSPanelMeta, engine.MustBlockTemplate("blocks/panel.html"))` +- `new_string`: `br.Register(LCARSPanelMeta, engine.MustBlockTemplateWithDefaults("blocks/panel.html", panelDefaults))` + +- [ ] **Step 6: Compile.** + +Run: `cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so .` +Expected: builds without error. + +- [ ] **Step 7: Run check-safety.** + +Run: `cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars` +Expected: passes — schema property names match Go content-map keys exactly. + +- [ ] **Step 8: Commit.** + +```bash +cd /home/alex/src/blockninja/themes/lcars +git add schemas/lcars_panel.schema.json templates/blocks/panel.html assets/style.css blocks.go register.go +git commit -m "$(cat <<'EOF' +feat(lcars_panel): redesign with elbow + strip frame modes + +Replaces the thin-left-border treatment with the locked LCARS geometry: +5.5rem corner column, 4rem top bar / 3rem bot bar, outer corners rounded +(1.75rem top, 1.25rem bot), inner L bend square. Schema gains frame +enum (elbow|strip), top/bottom labels and meta text, status indicator, +and top/bottom accent colour pickers spanning theme tokens + LCARS +canonical palette. + +Co-Authored-By: Claude Opus 4.7 +EOF +)" +``` + +--- + +## Task 3: Add `lcars_rail` compound block + +**Goal:** Add the "multiple cards share one L-frame" block — compound schema with `cells[]`. + +**Files:** +- Create: `schemas/lcars_rail.schema.json` +- Create: `templates/blocks/rail.html` +- Modify: `assets/style.css` (append rail rules) +- Modify: `blocks.go` (add LCARSRailMeta + railDefaults) +- Modify: `register.go` (register rail) + +- [ ] **Step 1: Create `schemas/lcars_rail.schema.json`:** + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LCARS Rail", + "description": "Compound L-frame with multiple sibling content cells, each paired with a coloured rail segment", + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Rail Title", + "description": "Top bar title", + "x-editor": "text" + }, + "top_label": { + "type": "string", + "title": "Top Identifier", + "x-editor": "text" + }, + "bottom_label": { + "type": "string", + "title": "Bottom Identifier", + "x-editor": "text" + }, + "top_accent_color": { + "type": "string", + "title": "Top Accent Colour", + "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"], + "default": "primary", + "x-editor": "select" + }, + "bottom_accent_color": { + "type": "string", + "title": "Bottom Accent Colour", + "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"], + "default": "primary", + "x-editor": "select" + }, + "rail_side": { + "type": "string", + "title": "Rail Side (reserved)", + "description": "Future field — only \"left\" is implemented", + "enum": ["left", "right"], + "default": "left", + "x-editor": "select" + }, + "cells": { + "type": "array", + "title": "Cells", + "description": "Each entry renders one rail segment paired with one content cell", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Segment Label", + "x-editor": "text" + }, + "color": { + "type": "string", + "title": "Segment Colour", + "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"], + "default": "mauve", + "x-editor": "select" + }, + "title": { + "type": "string", + "title": "Cell Title", + "x-editor": "text" + }, + "content": { + "type": "string", + "title": "Cell Content", + "x-editor": "richtext" + } + }, + "required": ["label"] + } + } + }, + "required": ["cells"] +} +``` + +- [ ] **Step 2: Create `templates/blocks/rail.html`** — uses explicit `grid-row` so segment and cell share each row regardless of DOM order: + +```html +
+
{{ top_label }}
+
{{ title }}
+ + {% for cell in cells %} +
{{ cell.label }}
+
+ {% if cell.title %}
{{ cell.title }}
{% endif %} + {% if cell.content %}
{{ cell.content|safe }}
{% endif %} +
+ {% endfor %} + +
{{ bottom_label }}
+
+
+``` + +Note: `forloop.Counter` is 1-indexed; `+1` makes the first cell land on grid-row 2 (row 1 is the top bar). Verify pongo2's `add` filter behaviour — if it's not present, substitute `{{ forloop.Counter0|add:2 }}` or fall back to placing segments and cells in DOM order without explicit grid-row (segments and cells will then alternate naturally if there are equal numbers of each). + +- [ ] **Step 3: Append rail CSS rules to `assets/style.css`** (after the panel rules): + +```css +/* ============================================================ + lcars_rail — compound L-frame with multiple cells + ============================================================ */ +.lcars-rail { + display: grid; + grid-template-columns: 5.5rem 1fr; + /* grid-template-rows is set inline per-instance (cells count varies) */ + gap: 4px; + background: hsl(var(--background)); + margin: 1rem 0; +} +.lcars-rail-tl { + grid-area: 1 / 1; + border-top-left-radius: 1.75rem; + display: flex; align-items: flex-end; justify-content: flex-end; + padding: 0 1.25rem 0.85rem 1rem; + color: hsl(var(--background)); + font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif); + letter-spacing: 0.08em; +} +.lcars-rail-bar-t { + grid-area: 1 / 2; + border-top-right-radius: 1.75rem; + display: flex; align-items: center; justify-content: flex-end; + padding: 0 1.5rem; + color: hsl(var(--background)); + font: 800 0.85rem/1 var(--font-heading, 'Antonio', sans-serif); + letter-spacing: 0.1em; + text-transform: uppercase; +} +.lcars-rail-seg { + grid-column: 1; + display: flex; align-items: center; justify-content: flex-end; + padding: 0 1.25rem 0 1rem; + color: hsl(var(--background)); + font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif); + letter-spacing: 0.08em; +} +.lcars-rail-cell { + grid-column: 2; + background: hsl(var(--background)); + padding: 0.5rem 1.25rem; + display: flex; flex-direction: column; justify-content: center; +} +.lcars-rail-cell-title { + margin: 0 0 0.15rem 0; + color: hsl(var(--lcars-gold)); + font: 800 0.72rem/1 var(--font-heading, 'Antonio', sans-serif); + text-transform: uppercase; + letter-spacing: 0.12em; +} +.lcars-rail-cell-body { + margin: 0; + color: hsl(var(--foreground)); + font: 400 0.85rem/1.5 var(--font-body, ui-sans-serif, system-ui, sans-serif); +} +.lcars-rail-bl { + grid-column: 1; + grid-row: -2; /* second-to-last row */ + border-bottom-left-radius: 1.25rem; + display: flex; align-items: flex-start; justify-content: flex-end; + padding: 0.75rem 1.25rem 0 1rem; + color: hsl(var(--background)); + font: 800 0.7rem/1 var(--font-heading, 'Antonio', sans-serif); + letter-spacing: 0.08em; +} +.lcars-rail-bar-b { + grid-column: 2; + grid-row: -2; + border-bottom-right-radius: 1.25rem; +} + +@media (max-width: 768px) { + .lcars-rail { grid-template-columns: 3.5rem 1fr; } + .lcars-rail-tl, .lcars-rail-bl, .lcars-rail-bar-t, .lcars-rail-seg { padding-left: 0.5rem; padding-right: 0.75rem; } +} +``` + +- [ ] **Step 4: Add `LCARSRailMeta` + `railDefaults` to `blocks.go`** at the end of the file (after `LCARSPanelMeta`): + +```go + +// --- LCARS Rail Block --- + +var LCARSRailMeta = blocks.BlockMeta{ + Key: "lcars_rail", + Title: "LCARS Rail", + Description: "Compound L-frame holding multiple cells — each row pairs a coloured segment with content", + Category: blocks.CategoryLayout, + Source: "lcars", +} + +var railDefaults = map[string]any{ + "title": "SCIENCE STATION", + "top_label": "SCI", + "bottom_label": "END", + "top_accent_color": "primary", + "bottom_accent_color": "primary", + "rail_side": "left", + "cells": []any{ + map[string]any{"label": "01", "color": "mauve", "title": "Long-range scan", "content": "Class-M planet · 4.7 light-years"}, + map[string]any{"label": "02", "color": "gold", "title": "Atmospheric", "content": "N2/O2 nominal · pressure 0.94 atm"}, + map[string]any{"label": "03", "color": "blue", "title": "Biosigns", "content": "Detected — analysis pending"}, + }, +} +``` + +- [ ] **Step 5: Register rail in `register.go`** — insert the new line immediately after the existing `br.Register(LCARSPanelMeta, …)` line. Edit: +- `old_string`: + ``` + br.Register(LCARSPanelMeta, engine.MustBlockTemplateWithDefaults("blocks/panel.html", panelDefaults)) + ``` +- `new_string`: + ``` + br.Register(LCARSPanelMeta, engine.MustBlockTemplateWithDefaults("blocks/panel.html", panelDefaults)) + br.Register(LCARSRailMeta, engine.MustBlockTemplateWithDefaults("blocks/rail.html", railDefaults)) + ``` + +- [ ] **Step 6: Compile.** + +Run: `cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so .` +Expected: builds without error. + +- [ ] **Step 7: Run check-safety.** + +Run: `cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars` +Expected: passes — `schemas/lcars_rail.schema.json` properties match the keys read in `templates/blocks/rail.html`. + +- [ ] **Step 8: Smoke test the pongo `add` filter** before relying on it elsewhere. If the build is OK but rendering a sample rail at request time errors with "filter `add` not found", swap the `{{ forloop.Counter|add:1 }}` expressions in `rail.html` for `{{ forloop.Counter0|stringformat:"%d"|... }}` or simpler: drop the explicit `grid-row` and rely on CSS grid auto-flow over a single column, then use `:nth-child(2n+2)` selectors to alternate placement. Document whichever variant survives in the commit body. + +Run: `grep -n '|add' /home/alex/src/blockninja/themes/lcars/templates/blocks/rail.html` to confirm the filter usage and remember it for visual verification later. +Expected: one or two `|add` matches; nothing in the build output (build is structure-only, not template-render). + +- [ ] **Step 9: Commit.** + +```bash +cd /home/alex/src/blockninja/themes/lcars +git add schemas/lcars_rail.schema.json templates/blocks/rail.html assets/style.css blocks.go register.go +git commit -m "$(cat <<'EOF' +feat(lcars_rail): new compound block — N cells share one L-frame + +A schema-driven (not container) block with a cells[] array. Each cell +renders one rail segment + one content cell aligned on the same CSS +grid row. Top/bottom corner blocks use the same outer-rounded / +inner-square geometry as lcars_panel. Default content seeds a 3-cell +SCI station example. + +Co-Authored-By: Claude Opus 4.7 +EOF +)" +``` + +--- + +## Task 4: Add `lcars_readout` tile + +**Goal:** Small dashboard tile for numeric/status displays. + +**Files:** +- Create: `schemas/lcars_readout.schema.json` +- Create: `templates/blocks/readout.html` +- Modify: `assets/style.css` (append readout rules) +- Modify: `blocks.go` (add LCARSReadoutMeta + readoutDefaults) +- Modify: `register.go` (register readout) + +- [ ] **Step 1: Create `schemas/lcars_readout.schema.json`:** + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LCARS Readout", + "description": "Small dashboard tile — coloured top border, label, big numeric/text value, optional unit", + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label", + "description": "Small uppercase label above the value", + "x-editor": "text" + }, + "value": { + "type": "string", + "title": "Value", + "description": "Big value text (number, percentage, status word)", + "x-editor": "text" + }, + "unit": { + "type": "string", + "title": "Unit", + "description": "Small suffix (%, LY, etc.)", + "x-editor": "text" + }, + "accent_color": { + "type": "string", + "title": "Accent Colour", + "description": "Top border and value colour", + "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"], + "default": "gold", + "x-editor": "select" + }, + "pulse": { + "type": "boolean", + "title": "Pulse", + "description": "Animate the value with the lcars-pulse animation", + "default": false, + "x-editor": "select" + } + }, + "required": ["label", "value"] +} +``` + +- [ ] **Step 2: Create `templates/blocks/readout.html`:** + +```html +
+

{{ label }}

+

{{ value }}{% if unit %}{{ unit }}{% endif %}

+
+``` + +- [ ] **Step 3: Append readout CSS rules to `assets/style.css`:** + +```css +/* ============================================================ + lcars_readout — small dashboard tile + ============================================================ */ +.lcars-readout { + background: hsl(var(--background)); + border-top: 4px solid hsl(var(--primary)); + padding: 0.5rem 0.75rem; +} +.lcars-readout-primary { border-top-color: hsl(var(--primary)); } +.lcars-readout-secondary { border-top-color: hsl(var(--secondary)); } +.lcars-readout-accent { border-top-color: hsl(var(--accent)); } +.lcars-readout-muted { border-top-color: hsl(var(--muted)); } +.lcars-readout-orange { border-top-color: hsl(var(--lcars-orange)); } +.lcars-readout-peach { border-top-color: hsl(var(--lcars-peach)); } +.lcars-readout-mauve { border-top-color: hsl(var(--lcars-mauve)); } +.lcars-readout-gold { border-top-color: hsl(var(--lcars-gold)); } +.lcars-readout-blue { border-top-color: hsl(var(--lcars-blue)); } +.lcars-readout-red { border-top-color: hsl(var(--lcars-red)); } + +.lcars-readout-label { + margin: 0; + color: hsl(var(--muted-foreground)); + font: 700 0.6rem/1 var(--font-heading, 'Antonio', sans-serif); + text-transform: uppercase; + letter-spacing: 0.18em; +} +.lcars-readout-value { + margin: 0.1rem 0 0 0; + color: hsl(var(--primary)); + font: 700 1.35rem/1 var(--font-heading, 'Antonio', sans-serif); + letter-spacing: 0.05em; +} +.lcars-readout-primary .lcars-readout-value { color: hsl(var(--primary)); } +.lcars-readout-secondary .lcars-readout-value { color: hsl(var(--secondary)); } +.lcars-readout-accent .lcars-readout-value { color: hsl(var(--accent)); } +.lcars-readout-muted .lcars-readout-value { color: hsl(var(--muted-foreground)); } +.lcars-readout-orange .lcars-readout-value { color: hsl(var(--lcars-orange)); } +.lcars-readout-peach .lcars-readout-value { color: hsl(var(--lcars-peach)); } +.lcars-readout-mauve .lcars-readout-value { color: hsl(var(--lcars-mauve)); } +.lcars-readout-gold .lcars-readout-value { color: hsl(var(--lcars-gold)); } +.lcars-readout-blue .lcars-readout-value { color: hsl(var(--lcars-blue)); } +.lcars-readout-red .lcars-readout-value { color: hsl(var(--lcars-red)); } +.lcars-readout-unit { + margin-left: 0.15rem; + color: hsl(var(--foreground)); + font-size: 0.7rem; + letter-spacing: 0.1em; +} +.lcars-readout-pulse { animation: lcars-pulse 3s ease-in-out infinite; } +``` + +- [ ] **Step 4: Add `LCARSReadoutMeta` + `readoutDefaults` to `blocks.go`** at the end: + +```go + +// --- LCARS Readout Block --- + +var LCARSReadoutMeta = blocks.BlockMeta{ + Key: "lcars_readout", + Title: "LCARS Readout", + Description: "Small dashboard tile — coloured top border, big value, optional unit and pulse animation", + Category: blocks.CategoryLayout, + Source: "lcars", +} + +var readoutDefaults = map[string]any{ + "label": "WARP CORE", + "value": "100", + "unit": "%", + "accent_color": "gold", + "pulse": false, +} +``` + +- [ ] **Step 5: Register readout in `register.go`** — add immediately after the rail registration. Edit: +- `old_string`: + ``` + br.Register(LCARSRailMeta, engine.MustBlockTemplateWithDefaults("blocks/rail.html", railDefaults)) + ``` +- `new_string`: + ``` + br.Register(LCARSRailMeta, engine.MustBlockTemplateWithDefaults("blocks/rail.html", railDefaults)) + br.Register(LCARSReadoutMeta, engine.MustBlockTemplateWithDefaults("blocks/readout.html", readoutDefaults)) + ``` + +- [ ] **Step 6: Compile.** + +Run: `cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so .` +Expected: builds without error. + +- [ ] **Step 7: Run check-safety.** + +Run: `cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars` +Expected: passes. + +- [ ] **Step 8: Commit.** + +```bash +cd /home/alex/src/blockninja/themes/lcars +git add schemas/lcars_readout.schema.json templates/blocks/readout.html assets/style.css blocks.go register.go +git commit -m "$(cat <<'EOF' +feat(lcars_readout): new dashboard tile block + +Small tile with coloured top border (theme tokens or LCARS canonical), +small uppercase label, big value, optional unit suffix, and optional +pulse animation. Designed for grids of mini readouts inside lcars_rail +cells or a standard columns block. + +Co-Authored-By: Claude Opus 4.7 +EOF +)" +``` + +--- + +## Task 5: Outer page-frame refinement + +**Goal:** Re-tune the existing outer page elbow widths and radii so they share the new 5.5rem / 1.75rem vocabulary with in-content panels. + +**Files:** +- Modify: `templates/default.html` +- Modify: `assets/style.css` (edit existing `.lcars-elbow-corner`, `.lcars-bar*`, `.lcars-content-area` rules) + +- [ ] **Step 1: Update `.lcars-elbow-corner` width in `style.css`.** Edit: +- `old_string`: + ``` + .lcars-elbow-corner { + width: 12rem; + min-width: 12rem; + background-color: hsl(var(--primary)); + } + ``` +- `new_string`: + ``` + .lcars-elbow-corner { + width: 5.5rem; + min-width: 5.5rem; + background-color: hsl(var(--primary)); + } + ``` + +- [ ] **Step 2: Update outer corner radii to match panel vocabulary.** Edit: +- `old_string`: + ``` + .lcars-elbow-tl { + border-bottom-left-radius: 0; + border-bottom-right-radius: 2rem; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + .lcars-elbow-bl { + border-top-left-radius: 0; + border-top-right-radius: 2rem; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + ``` +- `new_string`: + ``` + .lcars-elbow-tl { + border-top-left-radius: 1.75rem; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + + .lcars-elbow-bl { + border-bottom-left-radius: 1.25rem; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + ``` + +(Flips outer page corner from `border-bottom-right-radius: 2rem` — inner bend — to `border-top-left-radius: 1.75rem` — outer page edge — matching the locked panel decision.) + +- [ ] **Step 3: Update bar end-cap radii to match.** Edit: +- `old_string`: + ``` + .lcars-bar-top { + background-color: hsl(var(--secondary)); + border-radius: 0 0 1rem 0; + } + + .lcars-bar-bottom { + background-color: hsl(var(--secondary)); + border-radius: 0 1rem 0 0; + } + ``` +- `new_string`: + ``` + .lcars-bar-top { + background-color: hsl(var(--secondary)); + border-radius: 0 1.75rem 0 0; + } + + .lcars-bar-bottom { + background-color: hsl(var(--secondary)); + border-radius: 0 0 1.25rem 0; + } + ``` + +- [ ] **Step 4: Update `.lcars-content-area` grid-columns so the sidebar matches the new chunkier-vocabulary panels.** Edit: +- `old_string`: + ``` + .lcars-content-area { + display: grid; + grid-template-columns: 12rem 0.25rem 1fr; + gap: 0; + flex: 1; + } + ``` +- `new_string`: + ``` + .lcars-content-area { + display: grid; + grid-template-columns: 9rem 0.25rem 1fr; + gap: 0; + flex: 1; + } + ``` + +(Smaller sidebar width — keeps the sidebar usable for buttons but no longer dwarfs the new 5.5rem in-content rails.) + +- [ ] **Step 5: Update the responsive override.** Edit: +- `old_string`: + ``` + .lcars-elbow-corner { + width: 4rem; + min-width: 4rem; + } + ``` +- `new_string`: + ``` + .lcars-elbow-corner { + width: 3.5rem; + min-width: 3.5rem; + } + ``` + +- [ ] **Step 6: Compile.** + +Run: `cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so .` +Expected: builds without error. (CSS-only change; Go layer unaffected.) + +- [ ] **Step 7: Run check-safety.** + +Run: `cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars` +Expected: passes. + +- [ ] **Step 8: Commit.** + +```bash +cd /home/alex/src/blockninja/themes/lcars +git add assets/style.css +git commit -m "$(cat <<'EOF' +style(lcars): re-tune outer page frame to share panel chrome vocabulary + +Outer page elbow column 12rem → 5.5rem and rounded corner moved from +the inner bend (bottom-right) to the outer page edge (top-left/bottom- +left) at 1.75rem / 1.25rem — same geometry as the new lcars_panel and +lcars_rail blocks. Sidebar grid column 12rem → 9rem so navigation +buttons still fit without dwarfing in-content rails. + +Co-Authored-By: Claude Opus 4.7 +EOF +)" +``` + +--- + +## Task 6: Sidebar `code` field + +**Goal:** Each sidebar nav button gains an optional short identifier code (e.g. `SCI 01`) so the sidebar visually rhymes with the new rail block. + +**Files:** +- Modify: `schemas/lcars_sidebar.schema.json` +- Modify: `templates/blocks/sidebar.html` +- Modify: `assets/style.css` (small addition) + +- [ ] **Step 1: Add `code` to the sidebar item schema.** Edit `schemas/lcars_sidebar.schema.json`: +- `old_string`: + ``` + "color": { + "type": "string", + "title": "Color", + "enum": ["primary", "secondary", "accent"], + "default": "primary" + } + }, + "required": ["label", "url"] + ``` +- `new_string`: + ``` + "color": { + "type": "string", + "title": "Color", + "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"], + "default": "primary" + }, + "code": { + "type": "string", + "title": "Identifier Code", + "description": "Short code shown next to the label (e.g. SCI 01)" + } + }, + "required": ["label", "url"] + ``` + +- [ ] **Step 2: Render `code` in `templates/blocks/sidebar.html`.** Edit: +- `old_string`: + ``` + {% for item in items %} + + {{ item.label }} + + {% empty %} + ``` +- `new_string`: + ``` + {% for item in items %} + + {% if item.code %}{{ item.code }}{% endif %} + {{ item.label }} + + {% empty %} + ``` + +- [ ] **Step 3: Add CSS for the code element.** Append to `assets/style.css` (after the existing `.lcars-sidebar-btn-label` rule): + +```css +.lcars-sidebar-btn-code { + font-size: 0.65rem; + font-weight: 800; + letter-spacing: 0.1em; + opacity: 0.75; + margin-right: 0.4rem; +} +``` + +- [ ] **Step 4: Compile + check-safety.** + +```bash +cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so . +cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars +``` +Expected: both pass. + +- [ ] **Step 5: Commit.** + +```bash +cd /home/alex/src/blockninja/themes/lcars +git add schemas/lcars_sidebar.schema.json templates/blocks/sidebar.html assets/style.css +git commit -m "$(cat <<'EOF' +feat(lcars_sidebar): optional identifier code per nav button + +Sidebar items gain a `code` schema field (e.g. SCI 01) rendered as +small right-aligned text inside each button, visually matching the new +lcars_rail block. Sidebar item colour enum also widened to span the +LCARS canonical palette. + +Co-Authored-By: Claude Opus 4.7 +EOF +)" +``` + +--- + +## Task 7: Plugin metadata + admin guidance + +**Goal:** `fonts.json` → `[]` (per CLAUDE.md font-token rule); declare required icon pack; add admin-facing markdown guidance for fonts and icons. + +**Files:** +- Modify: `fonts.json` +- Modify: `plugin.mod` (BE CAREFUL — user has in-flight uncommitted changes here) +- Create: `RECOMMENDED_FONTS.md` +- Create: `RECOMMENDED_ICONS.md` + +- [ ] **Step 1: Check the live state of `plugin.mod`.** The user had in-flight changes when the plan was written. Before editing, re-read it. + +Run: `cat /home/alex/src/blockninja/themes/lcars/plugin.mod` +Note the contents — the spec needs an additional `required_icon_packs = ["lucide"]` line under `[plugin]` (forward-declared field; current parser ignores). + +- [ ] **Step 2: Add `required_icon_packs` to `plugin.mod`.** Edit by inserting one line. The exact `old_string` depends on what was in the file at Step 1 — typically the `categories` line is the last under `[plugin]` before the next section. + +Edit example (adjust to match actual file): +- `old_string`: + ``` + categories = ["templates"] + ``` +- `new_string`: + ``` + categories = ["templates"] + required_icon_packs = ["lucide"] + ``` + +If the user's in-flight `plugin.mod` already has `tags = […]` between `categories` and the next section, append after `tags` instead. The goal: `required_icon_packs = ["lucide"]` sits inside `[plugin]`. + +- [ ] **Step 3: Set `fonts.json` to `[]`.** Replace the file's full contents with: + +```json +[] +``` + +(Antonio woff2 files in `assets/fonts/web/` remain on disk for now; if you have time, delete them — but it's not in scope for this plan.) + +- [ ] **Step 4: Create `RECOMMENDED_FONTS.md`:** + +```markdown +# Recommended fonts + +This theme uses CSS custom properties for typography — no bundled fonts. +Admins should assign font families in the BlockNinja theme settings: + +| Token | Recommended | Notes | +|---|---|---| +| `--font-heading` | **Antonio** (Google Fonts) | Geometric sans-serif that matches the chunky LCARS typography. Any condensed geometric sans works (e.g. Oswald, Bebas Neue). | +| `--font-body` | system sans (default) | LCARS body copy is sparse — the system default reads fine. | +| `--font-mono` | system mono (default) | Used only for inline `code` in panel bodies. | + +Fall-back chain in CSS: `var(--font-heading, 'Antonio', sans-serif)`. +If the admin hasn't assigned `--font-heading`, browsers will use the +named family if installed, then any sans-serif. The visual difference +between a real Antonio and a generic sans is significant — assigning +the recommended family is strongly suggested. +``` + +- [ ] **Step 5: Create `RECOMMENDED_ICONS.md`:** + +```markdown +# Recommended icon packs + +This theme declares `required_icon_packs = ["lucide"]` in `plugin.mod` — +admins should install the **Lucide** icon pack before activating the +theme. Lucide is forward-declared today; future iterations will use +icons inside status indicators and readout tiles. + +No icons are required for the current block set. The declaration is +staged so future template updates can use `` +without breaking activation. +``` + +- [ ] **Step 6: Compile + check-safety.** + +```bash +cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so . +cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars +``` +Expected: both pass. (`required_icon_packs` is forward-declared so the current parser ignores it — no warning expected.) + +- [ ] **Step 7: Commit, being careful to ONLY stage your intended changes** (the user may have unrelated `plugin.mod` edits already staged or in their working tree). + +```bash +cd /home/alex/src/blockninja/themes/lcars +git add fonts.json RECOMMENDED_FONTS.md RECOMMENDED_ICONS.md +# Stage plugin.mod ONLY if the only change since the last commit is your +# required_icon_packs addition — check with `git diff plugin.mod` first. +git diff plugin.mod +# If the diff shows ONLY the required_icon_packs line you added, run: +git add plugin.mod +# Otherwise STOP, talk to the user about how to resolve, and stage manually. + +git commit -m "$(cat <<'EOF' +chore(lcars): plugin metadata cleanup and admin guidance + +- fonts.json → [] per themes/CLAUDE.md font-token rule (no more bundled + Antonio; admin assigns --font-heading at theme level) +- plugin.mod gains forward-declared required_icon_packs = ["lucide"] +- Add RECOMMENDED_FONTS.md and RECOMMENDED_ICONS.md for admin guidance + +Co-Authored-By: Claude Opus 4.7 +EOF +)" +``` + +--- + +## Task 8: Final verification and version bump + +**Goal:** Run the full validation pipeline end-to-end, bump the plugin version, and create the release commit. + +**Files:** +- Modify: `plugin.mod` (version bump only, via `make bump-minor` if Makefile exists, or hand-edit) + +- [ ] **Step 1: Verify git state is clean** of unrelated changes. + +Run: `cd /home/alex/src/blockninja/themes/lcars && git status --short` +Expected: clean working tree (or only your intentional staged changes for this task). + +- [ ] **Step 2: Final `go build`.** + +Run: `cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so .` +Expected: builds without error. + +- [ ] **Step 3: Final check-safety.** + +Run: `cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars` +Expected: passes all ~25 invariant checks. + +- [ ] **Step 4: Visual verification** — this requires a running dev CMS instance (out-of-process for this plan, but mandatory before a real release). The user's workflow may be `make rebuild` against `instance-lcars`, or a different process. Confirm with the user that one or more of the following actually render correctly in a browser: + - A page using `lcars_panel` with `frame="elbow"` and identifier/meta filled in + - A page using `lcars_panel` with `frame="strip"` + - A page using `lcars_rail` with 3 cells of varying colours + - A grid of 4 `lcars_readout` tiles with different accent colours + - The outer page frame (default master template) renders with the new 5.5rem column and 1.75rem outer top-left radius + - Sidebar buttons with a `code` field show the code + +Note any issues, fix in a separate commit. If everything looks right, proceed. + +- [ ] **Step 5: Bump version.** If there's a `Makefile` with `bump-minor` (copy from `gotham`), run: + +```bash +cd /home/alex/src/blockninja/themes/lcars +make bump-minor +``` + +If there is no Makefile, hand-edit `plugin.mod` to bump `version = "0.2.7"` (or whatever the current value is) to `version = "0.3.0"` — this is a feature release. Commit: + +```bash +git add plugin.mod +git commit -m "chore: bump to 0.3.0" +git tag v0.3.0 +``` + +- [ ] **Step 6: Done.** The .so will rebuild against the new version on next `make rebuild` against a CMS instance. Push when ready (out of scope for this plan). + +```bash +cd /home/alex/src/blockninja/themes/lcars +git log --oneline -10 +``` +Expected output: commits from Tasks 1-7 plus the version-bump commit, in order. + +--- + +## Spec coverage cross-check + +| Spec section | Covered by | +|---|---| +| § Visual outcome — locked geometry | Task 1 (palette/tokens), Task 2 (panel CSS+template), Task 3 (rail CSS+template) | +| § 1 `lcars_panel` schema | Task 2 step 1 | +| § 1 `lcars_panel` render targets (elbow/strip) | Task 2 step 2 | +| § 2 `lcars_rail` architecture (compound, not container) | Task 3 (schema is `cells[]`, no `HasInternalSlot`) | +| § 2 `lcars_rail` schema | Task 3 step 1 | +| § 3 `lcars_readout` schema + render | Task 4 | +| § Geometry CSS | Task 2 step 3 (elbow), Task 3 step 3 (rail), Task 4 step 3 (readout) | +| § Refinements 1 — font tokens | Task 1 step 3 | +| § Refinements 2 — palette expansion | Task 1 step 2 (via CSS `:root`, not presets.json — see Plan note at top) | +| § Refinements 3 — outer page-frame refinement | Task 5 | +| § Refinements 4 — sidebar identifier codes | Task 6 | +| § Refinements 5 — `required_icon_packs` | Task 7 step 2 | +| § Refinements 6 — RECOMMENDED_ICONS.md (and FONTS) | Task 7 steps 4-5 | +| § File-by-file impact `fonts.json: []` | Task 7 step 3 | +| § Validation `make` | Task 8 step 2 (raw `go build` since no Makefile exists yet) | +| § Validation `check-safety` | Task 8 step 3 | +| § Validation visual `make rebuild` | Task 8 step 4 (requires user / dev instance) |