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>
1398 lines
52 KiB
Markdown
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"> </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) |
|