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>
52 KiB
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):
# 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
:rootpalette block. Insert at the very top ofassets/style.css, BEFORE the existing/* ============…comment header:
/* ============================================================
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-pagerule and the.lcars-headingrule. Each currently setsfont-family: 'Antonio', sans-serif;. Replace both withfont-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.
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 fromassets/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.jsonwith the new fields. Replace the file's full contents with:
{
"$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.htmlto render both frame modes. Replace the file's full contents with:
{% 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"> </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@mediablock):
/* ============================================================
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
LCARSPanelMetadescription inblocks.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.goto 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.
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:
{
"$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 explicitgrid-rowso segment and cell share each row regardless of DOM order:
<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):
/* ============================================================
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+railDefaultstoblocks.goat the end of the file (afterLCARSPanelMeta):
// --- 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 existingbr.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
addfilter before relying on it elsewhere. If the build is OK but rendering a sample rail at request time errors with "filteraddnot found", swap the{{ forloop.Counter|add:1 }}expressions inrail.htmlfor{{ forloop.Counter0|stringformat:"%d"|... }}or simpler: drop the explicitgrid-rowand 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.
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:
{
"$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:
<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:
/* ============================================================
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+readoutDefaultstoblocks.goat the end:
// --- 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.
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-arearules) -
Step 1: Update
.lcars-elbow-cornerwidth instyle.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-areagrid-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.
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
codeto the sidebar item schema. Editschemas/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
codeintemplates/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-labelrule):
.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.
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.
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_packstoplugin.mod. Edit by inserting one line. The exactold_stringdepends on what was in the file at Step 1 — typically thecategoriesline 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.jsonto[]. Replace the file's full contents with:
[]
(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:
# 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:
# 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.
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.modedits already staged or in their working tree).
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, viamake bump-minorif 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 rebuildagainstinstance-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_panelwithframe="elbow"and identifier/meta filled in - A page using
lcars_panelwithframe="strip" - A page using
lcars_railwith 3 cells of varying colours - A grid of 4
lcars_readouttiles 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
codefield show the code
- A page using
Note any issues, fix in a separate commit. If everything looks right, proceed.
- Step 5: Bump version. If there's a
Makefilewithbump-minor(copy fromgotham), run:
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:
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 rebuildagainst a CMS instance. Push when ready (out of scope for this plan).
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) |