themes-lcars/docs/superpowers/plans/2026-06-06-lcars-card-redesign.md
Alex Dunmow 91e6735eb2 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 <noreply@anthropic.com>
2026-06-06 20:35:41 +08:00

1398 lines
52 KiB
Markdown

# 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 <noreply@anthropic.com>
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" %}
<div class="lcars-strip">
<div class="lcars-strip-head">
{% if top_label %}<div class="lcars-strip-seg lcars-strip-seg-id lcars-bg-{{ accent_color|default:"primary" }}">{{ top_label }}</div>{% endif %}
{% if title %}<div class="lcars-strip-seg lcars-strip-seg-title lcars-bg-gold">{{ title }}</div>{% endif %}
{% if top_meta %}<div class="lcars-strip-seg lcars-strip-seg-meta lcars-bg-muted">{{ top_meta }}</div>{% endif %}
{% if status %}<div class="lcars-strip-seg lcars-strip-seg-status lcars-bg-{{ accent_color|default:"primary" }}">{{ status|upper }}</div>{% endif %}
<div class="lcars-strip-seg lcars-strip-seg-cap lcars-bg-{{ bottom_accent_color|default:"secondary" }}"></div>
</div>
<div class="lcars-strip-body lcars-strip-body-{{ accent_color|default:"primary" }}">
{{ content|safe }}
</div>
</div>
{% else %}
<div class="lcars-elbow">
<div class="lcars-elbow-tl lcars-bg-{{ accent_color|default:"primary" }}">{{ top_label }}</div>
<div class="lcars-elbow-bar-t lcars-bg-{{ accent_color|default:"primary" }}">
<span class="lcars-elbow-bar-title">{{ title }}</span>
<span class="lcars-elbow-bar-meta">
{% if status %}<span class="lcars-elbow-status-dot lcars-elbow-status-{{ status }}"></span>{{ status|upper }}{% endif %}
{% if top_meta %}{% if status %} · {% endif %}{{ top_meta }}{% endif %}
</span>
</div>
<div class="lcars-elbow-rail lcars-bg-{{ accent_color|default:"primary" }}"></div>
<div class="lcars-elbow-body">{{ content|safe }}</div>
<div class="lcars-elbow-bl lcars-bg-{{ bottom_accent_color|default:"secondary" }}">{{ bottom_label }}</div>
<div class="lcars-elbow-bar-b lcars-bg-{{ bottom_accent_color|default:"secondary" }}">
<span class="lcars-elbow-bar-title">&nbsp;</span>
<span class="lcars-elbow-bar-meta">{{ bottom_meta }}</span>
</div>
</div>
{% 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 <noreply@anthropic.com>
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
<div class="lcars-rail" style="grid-template-rows: 4rem repeat({{ cells|length }}, var(--lcars-rail-row, 3.5rem)) 3rem;">
<div class="lcars-rail-tl lcars-bg-{{ top_accent_color|default:"primary" }}">{{ top_label }}</div>
<div class="lcars-rail-bar-t lcars-bg-{{ top_accent_color|default:"primary" }}">{{ title }}</div>
{% for cell in cells %}
<div class="lcars-rail-seg lcars-bg-{{ cell.color|default:"mauve" }}" style="grid-row: {{ forloop.Counter|add:1 }};">{{ cell.label }}</div>
<div class="lcars-rail-cell" style="grid-row: {{ forloop.Counter|add:1 }};">
{% if cell.title %}<h5 class="lcars-rail-cell-title">{{ cell.title }}</h5>{% endif %}
{% if cell.content %}<div class="lcars-rail-cell-body">{{ cell.content|safe }}</div>{% endif %}
</div>
{% endfor %}
<div class="lcars-rail-bl lcars-bg-{{ bottom_accent_color|default:"primary" }}">{{ bottom_label }}</div>
<div class="lcars-rail-bar-b lcars-bg-{{ bottom_accent_color|default:"primary" }}"></div>
</div>
```
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 <noreply@anthropic.com>
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
<div class="lcars-readout lcars-readout-{{ accent_color|default:"gold" }}">
<p class="lcars-readout-label">{{ label }}</p>
<p class="lcars-readout-value{% if pulse %} lcars-readout-pulse{% endif %}">{{ value }}{% if unit %}<span class="lcars-readout-unit">{{ unit }}</span>{% endif %}</p>
</div>
```
- [ ] **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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 %}
<a href="{{ item.url }}" class="lcars-sidebar-btn lcars-bg-{{ item.color|default:"primary" }}">
<span class="lcars-sidebar-btn-label">{{ item.label }}</span>
</a>
{% empty %}
```
- `new_string`:
```
{% for item in items %}
<a href="{{ item.url }}" class="lcars-sidebar-btn lcars-bg-{{ item.color|default:"primary" }}">
{% if item.code %}<span class="lcars-sidebar-btn-code">{{ item.code }}</span>{% endif %}
<span class="lcars-sidebar-btn-label">{{ item.label }}</span>
</a>
{% 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 <noreply@anthropic.com>
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 `<svg><use href="/icons/lucide.svg#name"/></svg>`
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 <noreply@anthropic.com>
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) |