Skip to content

M2 Cycle 8 — UI Scale, Bible Live Filter & Neon Restyle

M2 Cycle 8 — UI Scale, Bible Live Filter & Neon Restyle

Section titled “M2 Cycle 8 — UI Scale, Bible Live Filter & Neon Restyle”

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: Make game HUD/weapon-panel elements larger, mark in-game bible content with a live filter, and restyle the bible tool to match the game’s neon aesthetic.

Architecture: T1 is pure GDScript constant changes (no logic). T2 adds live: true to 14 seed entries (spec says 13 but lists 14 — follow the list). T3 wires a filter toggle and badge in app.js/list.js. T4 is CSS-only. T3 depends on T2’s seed data; all others are independent.

Tech Stack: Godot 4.6.3 GDScript; vanilla ES-module JavaScript (no bundler); CSS3

  • Godot tests: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexit — must pass 150 tests, 0 failures
  • Bible tests: cd tools/design-bible && node --test — must pass 31 tests, 0 failures
  • GDScript: typed, no untyped var; no Engine/Time/Node APIs inside /sim files
  • No new GDScript tests for T1 (render-only). Existing hp_fill_width() test asserts > 230.0 — new BAR_W is 334.0, still passes
  • assert_push_error_count(0) is the correct GUT 9.6 API (not assert_no_push_errors())
  • Determinism hashes unchanged (T1 doesn’t touch sim): snapshot_string().hash() = 1314757315, state_checksum() = 1949813464
  • YAGNI: _tint_border stub in weapon_panel.gd stays as-is

Files:

  • Modify: ui/hud.gd
  • Modify: ui/weapon_panel.gd

Interfaces:

  • Consumes: NeonTheme.CYAN, NeonTheme.TEXT, NeonTheme.mono_font(), NeonTheme.title_font() — all unchanged

  • Produces: hp_fill_width() -> float returns BAR_W * frac where BAR_W is now 334.0

  • Step 1: Baseline smoke

Terminal window
godot --headless --path . --quit-after 300 2>&1 | grep "SCRIPT ERROR" | head -5
echo "smoke exit: $?"

Expected: no SCRIPT ERROR, exit 0.

  • Step 2: Replace ui/hud.gd

Changes: BAR_W 234→334, BAR_H 32→40, backing 240×40→340×52, fill inset (19,16)→(21,18), HP font 15→18, level x 264→364 (16+340+8=364), level size 80×40→100×52, level font 18→22, timer x offset -80→-110, timer width 160→220, timer font 28→38, kills x -160→-196, kills width 144→180, kills font 18→22.

class_name Hud
extends CanvasLayer
const BAR_W: float = 334.0 # usable fill width inside 340px backing
const BAR_H: float = 40.0
var _hp_fill: ColorRect
var _hp_label: Label
var _level_label: Label
var _timer_label: Label
var _kills_label: Label
func _ready() -> void:
var vp_w: float = ProjectSettings.get_setting("display/window/size/viewport_width", 1152)
var vp: Vector2 = Vector2(float(vp_w), 648.0)
# ── HP bar (top-left) ──────────────────────────────────────────────
var bar_backing := ColorRect.new()
bar_backing.position = Vector2(16, 12)
bar_backing.size = Vector2(340, 52)
bar_backing.color = Color(0, 0, 0, 0.55)
bar_backing.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(bar_backing)
_hp_fill = ColorRect.new()
_hp_fill.position = Vector2(21, 18) # 5px inset
_hp_fill.size = Vector2(BAR_W, BAR_H)
_hp_fill.color = NeonTheme.CYAN
_hp_fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
bar_backing.add_child(_hp_fill)
_hp_label = Label.new()
_hp_label.position = Vector2(0, 0)
_hp_label.size = Vector2(340, 52)
_hp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_hp_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_hp_label.add_theme_font_override("font", NeonTheme.mono_font())
_hp_label.add_theme_font_size_override("font_size", 18)
_hp_label.add_theme_color_override("font_color", NeonTheme.TEXT)
_hp_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
bar_backing.add_child(_hp_label)
# ── Level badge (right of HP bar) ──────────────────────────────────
_level_label = Label.new()
_level_label.position = Vector2(364, 12)
_level_label.size = Vector2(100, 52)
_level_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_level_label.add_theme_font_override("font", NeonTheme.title_font())
_level_label.add_theme_font_size_override("font_size", 22)
_level_label.add_theme_color_override("font_color", NeonTheme.CYAN)
_level_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_level_label)
# ── Run timer (top-centre) ─────────────────────────────────────────
_timer_label = Label.new()
_timer_label.position = Vector2(vp.x / 2.0 - 110, 10)
_timer_label.size = Vector2(220, 40)
_timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_timer_label.add_theme_font_override("font", NeonTheme.title_font())
_timer_label.add_theme_font_size_override("font_size", 38)
_timer_label.add_theme_color_override("font_color", NeonTheme.CYAN)
_timer_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_timer_label)
# ── Kill counter (top-right) ───────────────────────────────────────
_kills_label = Label.new()
_kills_label.position = Vector2(vp.x - 196, 12)
_kills_label.size = Vector2(180, 40)
_kills_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_kills_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_kills_label.add_theme_font_override("font", NeonTheme.mono_font())
_kills_label.add_theme_font_size_override("font_size", 22)
_kills_label.add_theme_color_override("font_color", NeonTheme.CYAN)
_kills_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_kills_label)
func update_hud(sim: Sim) -> void:
var frac: float = clampf(sim.player.hp / maxf(sim.player.max_hp, 1.0), 0.0, 1.0)
_hp_fill.size.x = BAR_W * frac
_hp_fill.color = _hp_color(frac)
_hp_label.text = "HP %d / %d" % [int(sim.player.hp), int(sim.player.max_hp)]
_level_label.text = "Lv %d" % sim.player.level
var t := int(sim.run_time)
_timer_label.text = "%02d:%02d" % [t / 60, t % 60]
_kills_label.text = "x %d" % sim.kills
# Test seams ──────────────────────────────────────────────────────────────
func hp_fill_width() -> float:
return _hp_fill.size.x
func hp_fill_color() -> Color:
return _hp_fill.color
# Private ─────────────────────────────────────────────────────────────────
func _hp_color(frac: float) -> Color:
if frac > 0.6:
return NeonTheme.CYAN
if frac > 0.3:
var t: float = (frac - 0.3) / 0.3
return Color(1.0, 0.85, 0.1).lerp(NeonTheme.CYAN, t)
return Color(1.0, 0.25, 0.2)
  • Step 3: Replace ui/weapon_panel.gd

Changes: arc radius 24→32, arc line width 3→4, SLOT_W 120→160, SLOT_H 70→90, SLOT_GAP 12→16, name height 20→24, name font 13→16, stat y-offset 28→34, stat width SLOT_W-44→SLOT_W-50, stat height 18→20, stat font 12→15, arc x offset SLOT_W-30→SLOT_W-38.

class_name WeaponPanel
extends CanvasLayer
# ── Inner: cooldown arc drawn via _draw() ────────────────────────────────
class _CooldownArc extends Node2D:
var frac: float = 0.0
var col: Color = NeonTheme.CYAN
func _draw() -> void:
if frac <= 0.0:
return
draw_arc(Vector2.ZERO, 32.0, -PI / 2.0, -PI / 2.0 + TAU * frac, 48, col, 4.0, true)
const SLOT_W: float = 160.0
const SLOT_H: float = 90.0
const SLOT_GAP: float = 16.0
var _name_labels: Array[Label] = []
var _stat_labels: Array[Label] = []
var _arcs: Array[_CooldownArc] = []
func _ready() -> void:
var vp_w: float = float(ProjectSettings.get_setting("display/window/size/viewport_width", 1152))
var vp_h: float = float(ProjectSettings.get_setting("display/window/size/viewport_height", 648))
var x0: float = vp_w / 2.0 - SLOT_W - SLOT_GAP / 2.0
var y0: float = vp_h - SLOT_H - 16.0
for i in range(2):
var sx: float = x0 + i * (SLOT_W + SLOT_GAP)
_build_slot(sx, y0, i)
func _build_slot(x: float, y: float, _idx: int) -> void:
var backing := ColorRect.new()
backing.position = Vector2(x, y)
backing.size = Vector2(SLOT_W, SLOT_H)
backing.color = Color(0, 0, 0, 0.55)
backing.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(backing)
var border := Panel.new()
border.position = Vector2(x, y)
border.size = Vector2(SLOT_W, SLOT_H)
border.mouse_filter = Control.MOUSE_FILTER_IGNORE
var sb := StyleBoxFlat.new()
sb.bg_color = Color.TRANSPARENT
sb.border_width_left = 1; sb.border_width_right = 1
sb.border_width_top = 1; sb.border_width_bottom = 1
sb.border_color = NeonTheme.CYAN
sb.corner_radius_top_left = 8; sb.corner_radius_top_right = 8
sb.corner_radius_bottom_left = 8; sb.corner_radius_bottom_right = 8
border.add_theme_stylebox_override("panel", sb)
add_child(border)
var name_lbl := Label.new()
name_lbl.position = Vector2(x + 8, y + 6)
name_lbl.size = Vector2(SLOT_W - 16, 24)
name_lbl.add_theme_font_override("font", NeonTheme.title_font())
name_lbl.add_theme_font_size_override("font_size", 16)
name_lbl.add_theme_color_override("font_color", NeonTheme.TEXT)
name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(name_lbl)
_name_labels.append(name_lbl)
var stat_lbl := Label.new()
stat_lbl.position = Vector2(x + 8, y + 34)
stat_lbl.size = Vector2(SLOT_W - 50, 20)
stat_lbl.add_theme_font_override("font", NeonTheme.mono_font())
stat_lbl.add_theme_font_size_override("font_size", 15)
stat_lbl.add_theme_color_override("font_color", NeonTheme.CYAN)
stat_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(stat_lbl)
_stat_labels.append(stat_lbl)
var arc := _CooldownArc.new()
arc.position = Vector2(x + SLOT_W - 38, y + SLOT_H / 2.0)
add_child(arc)
_arcs.append(arc)
func update_panel(
pulse: WeaponPulse, nova: WeaponNova,
pulse_el: int, nova_el: int,
content: ContentDB, dmg_mult: float) -> void:
_name_labels[0].text = "Lightning"
_stat_labels[0].text = "dmg %.0f" % (pulse.base_damage * dmg_mult)
_arcs[0].frac = pulse.cooldown_frac()
_arcs[0].col = ElementPalette.color_for(content, pulse_el)
_arcs[0].queue_redraw()
_tint_border(0, ElementPalette.color_for(content, pulse_el))
_name_labels[1].text = "Fire Nova"
_stat_labels[1].text = "dmg %.0f r%.0f" % [nova.base_damage * dmg_mult, nova.area]
_arcs[1].frac = nova.cooldown_frac()
_arcs[1].col = ElementPalette.color_for(content, nova_el)
_arcs[1].queue_redraw()
_tint_border(1, ElementPalette.color_for(content, nova_el))
func _tint_border(_slot_idx: int, _col: Color) -> void:
pass # Element tinting via arc colour is sufficient; border stays cyan
  • Step 4: Run full suite + smoke
Terminal window
godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexit 2>&1 | tail -5

Expected: 150 passed, 0 failed (hp_fill_width() asserts > 230.0; new BAR_W 334.0 passes).

Terminal window
godot --headless --path . --quit-after 300 2>&1 | grep "SCRIPT ERROR" | head -5

Expected: no SCRIPT ERROR lines.

  • Step 5: Commit
Terminal window
git add ui/hud.gd ui/weapon_panel.gd
git commit -m "feat: M2 cycle 8 — UI scale pass (1.4× HUD and weapon-panel constants)"

Files:

  • Modify: tools/design-bible/src/seed.js

Interfaces:

  • Consumes: nothing from earlier tasks
  • Produces: live: true on 14 entries — consumed by T3’s filtered() and list badge

Note: spec header says “13 entries” but the itemised list totals 14 (2 weapons + 1 enemy + 8 mods + 2 elements + 1 reaction). Follow the list.

  • Step 1: Verify baseline
Terminal window
cd ~/Claude/bullet-heaven && node --test tools/design-bible/src/ 2>/dev/null; cd tools/design-bible && node --test 2>&1 | tail -3

Expected: 31 pass.

  • Step 2: Replace tools/design-bible/src/seed.js

weapon() and enemy() have an extra spread param — add live: true inside the extra object. el(), mod(), rx() have no spread — wrap with Object.assign(fn(...), { live: true }).

// Helper to keep element rows terse: id,name,color,decay,status,base,stacksMax,scale
const el = (id, name, color, status, status_base = 2) =>
({ id, name, color, aura_decay_s: 4, status, status_base, stacks_max: 6, per_stack_scale: 1.15, tags: ['element', id] });
const elements = [
Object.assign(el('fire', 'Fire', '#ff6a4d', 'burn'), { live: true }),
el('cold', 'Cold', '#6cc8ff', 'chill'),
Object.assign(el('lightning', 'Lightning', '#ffe34d', 'shock', 0.15), { live: true }),
el('poison', 'Poison', '#8cff6a', 'toxin'),
el('void', 'Void', '#b06aff', 'gravity'),
el('aether', 'Aether', '#e0e0ff', 'echo'),
el('kinetic', 'Kinetic', '#c9d1d9', 'impact'),
el('light', 'Light', '#fff3a0', 'mark'),
el('decay', 'Decay', '#8a7a5c', 'wither'),
el('time', 'Time', '#a0e0ff', 'timewarp'),
el('sound', 'Sound', '#ff9ee0', 'resonate'),
el('blood', 'Blood', '#ff5a5a', 'hemorrhage'),
el('psychic', 'Psychic', '#c08aff', 'charm'),
el('nano', 'Nano', '#6affd0', 'contagion'),
];
const rx = (aura, applied, name, effect, base_magnitude = 30) =>
({ id: `${aura}-${applied}`, name, aura, applied, effect, base_magnitude, per_stack_scale: 1.15, consumes_aura: true, notes: '' });
const reactions = [
rx('cold', 'lightning', 'Superconduct', 'shatter', 40),
rx('lightning', 'cold', 'Superconduct', 'shatter', 40),
rx('fire', 'cold', 'Thermal Shock', 'burst', 35),
rx('cold', 'fire', 'Thermal Shock', 'burst', 35),
Object.assign(rx('fire', 'lightning', 'Plasma', 'burst', 45), { live: true }),
rx('lightning', 'fire', 'Plasma', 'burst', 45),
rx('fire', 'poison', 'Combustion', 'spread', 30),
rx('poison', 'lightning', 'Electrolysis', 'spread', 30),
rx('fire', 'void', 'Collapse', 'pull', 50),
rx('cold', 'void', 'Absolute Zero', 'cc', 40),
rx('poison', 'void', 'Blight Implosion', 'pull', 35),
rx('cold', 'sound', 'Shatter', 'shatter', 55),
rx('void', 'aether', 'Paradox', 'special', 60),
];
const weapon = (id, name, archetype, element, base_damage, cooldown_s, extra = {}) =>
({ id, name, archetype, element, base_damage, cooldown_s, projectile_speed: 520, projectile_radius: 6,
lifetime_s: 1.4, projectile_count: 1, area: 0, pierce: 0, level_max: 8, evolution: '',
tags: [archetype], ...extra });
const weapons = [
weapon('pulse', 'Pulse', 'projectile', 'lightning', 1.0, 0.6, { tags: ['projectile','homing'], live: true }),
weapon('orbit', 'Orbit Shards', 'orbital', 'cold', 0.8, 0.0, { projectile_count: 3, tags: ['orbital'] }),
weapon('beam', 'Beam', 'beam', 'light', 0.4, 0.1, { pierce: 99, tags: ['beam','pierce'] }),
weapon('nova', 'Nova', 'area', 'fire', 3.0, 2.0, { area: 180, tags: ['area'], live: true }),
weapon('turret', 'Turret', 'summon', 'kinetic', 0.7, 0.4, { tags: ['summon','trap'] }),
];
const mod = (id, name, kind, effect, magnitude, applies = []) =>
({ id, name, kind, effect, magnitude, applies_to_tags: applies, tags: [] });
const mods = [
Object.assign(mod('damage', 'Sharpened', 'stat', 'damage_mult', 1.25), { live: true }),
Object.assign(mod('fire-rate', 'Overclock', 'stat', 'fire_rate_mult', 1.20), { live: true }),
Object.assign(mod('move-speed', 'Thrusters', 'stat', 'move_speed', 1.12), { live: true }),
Object.assign(mod('pickup', 'Magnet Field', 'stat', 'pickup_radius', 1.30), { live: true }),
Object.assign(mod('max-hp', 'Plating', 'stat', 'max_hp', 25), { live: true }),
mod('crit', 'Focus', 'stat', 'crit_chance', 0.15),
mod('pierce', 'Penetrator', 'transformative', 'projectiles_pierce', 1, ['projectile']),
mod('split', 'Fork', 'transformative', 'split_on_hit', 2, ['projectile']),
Object.assign(mod('overcharge', 'Overcharge', 'transformative', 'stack_bonus', 1), { live: true }),
Object.assign(mod('catalyst', 'Catalyst', 'transformative', 'reaction_damage_mult', 1.5), { live: true }),
Object.assign(mod('lingering', 'Lingering', 'transformative', 'aura_duration_mult', 1.5), { live: true }),
];
const enemy = (id, name, archetype, hp, speed, contact_damage, xp_value, extra = {}) =>
({ id, name, archetype, hp, speed, radius: 14, contact_damage, xp_value, armor: 0, tags: [archetype], ...extra });
const enemies = [
enemy('swarmer', 'Swarmer', 'swarmer', 3, 70, 12, 1, { live: true }),
enemy('tank', 'Tank', 'tank', 30, 40, 18, 4, { radius: 22, armor: 4 }),
enemy('shooter', 'Shooter', 'ranged', 8, 55, 10, 3),
enemy('splitter', 'Splitter', 'splitter', 10, 60, 10, 3),
enemy('elite', 'Elite', 'charger', 60, 90, 25, 12, { radius: 26, armor: 6 }),
];
const tagVocab = [
['projectile','delivery'],['orbital','delivery'],['beam','delivery'],['area','delivery'],
['melee','delivery'],['summon','delivery'],['trap','delivery'],['channeled','delivery'],
['fire','element'],['cold','element'],['lightning','element'],['poison','element'],
['void','element'],['aether','element'],['kinetic','element'],['light','element'],
['decay','element'],['time','element'],['sound','element'],['blood','element'],
['psychic','element'],['nano','element'],['element','trait'],
['crit','trait'],['pierce','trait'],['chain','trait'],['homing','trait'],['dot','trait'],['cc','trait'],
].map(([name, group]) => ({ id: name, name, group }));
export const SEED = {
schemaVersion: 1,
data: {
elements, reactions, weapons, mods, evolutions: [], enemies, elite_affixes: [],
bosses: [], characters: [], items: [], pickups: [], meta_upgrades: [], unlocks: [],
ascension_tiers: [], run_structure: [], tags: tagVocab,
},
};
  • Step 3: Run bible tests
Terminal window
cd tools/design-bible && node --test 2>&1 | tail -3

Expected: 31 pass. The live field is extra data the pure-logic core ignores.

  • Step 4: Commit
Terminal window
git add tools/design-bible/src/seed.js
git commit -m "feat: mark 14 in-game bible entries live (weapons, enemy, mods, elements, reaction)"

Files:

  • Modify: tools/design-bible/src/app.js
  • Modify: tools/design-bible/src/views/list.js

Interfaces:

  • Consumes: T2’s live: true field on entries via e.live check

  • Produces: button[data-live="true/false"] attribute (styled gold by T4), .live-label class on spans (styled gold by T4)

  • Step 1: Replace tools/design-bible/src/app.js

Three changes from current: (1) liveOnly: true added to ui object on line 17; (2) filtered() filters by e.live when ui.liveOnly; (3) renderActions() prepends a live-toggle button with dataset.live.

import { CATEGORIES, categoryByKey } from './schema.js';
import { SEED } from './seed.js';
import { BibleModel } from './model.js';
import { save, load, exportJSON, importJSON } from './persistence.js';
import { renderNav } from './views/nav.js';
import { renderList } from './views/list.js';
import { renderDetail } from './views/detail.js';
import { renderMatrix } from './views/matrix.js';
import { renderTable } from './views/table.js';
const navEl = document.getElementById('nav');
const listEl = document.getElementById('list');
const detailEl = document.getElementById('detail');
const actionsEl = document.getElementById('actions');
let model = new BibleModel(load(localStorage, SEED));
const ui = { activeKey: 'elements', activeId: null, query: '', viewMode: 'detail', liveOnly: true };
function persist() { save(localStorage, model.bible); }
function selectCategory(key) { ui.activeKey = key; ui.activeId = null; ui.query = ''; render(); }
function selectEntry(id) { ui.activeId = id; render(); }
function filtered() {
const entries = model.list(ui.activeKey);
return ui.liveOnly ? entries.filter(e => e.live) : entries;
}
function renderActions() {
actionsEl.innerHTML = '';
const mk = (label, fn) => { const b = document.createElement('button'); b.textContent = label; b.onclick = fn; b.style.marginLeft = '6px'; return b; };
const liveBtn = mk(ui.liveOnly ? '◈ Live' : '◇ All', () => { ui.liveOnly = !ui.liveOnly; render(); });
liveBtn.title = ui.liveOnly ? 'Showing live items only — click to show all' : 'Showing all items — click to show live only';
liveBtn.dataset.live = ui.liveOnly ? 'true' : 'false';
const toggle = mk(ui.viewMode === 'detail' ? 'Table view' : 'Detail view', () => {
ui.viewMode = ui.viewMode === 'detail' ? 'table' : 'detail'; render();
});
const exp = mk('Export', () => {
const blob = new Blob([exportJSON(model.bible)], { type: 'application/json' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'bullet-heaven-bible.json'; a.click();
});
const imp = mk('Import', () => fileInput.click());
const reset = mk('Reset', () => { if (confirm('Reset to seed? Unsaved edits lost.')) { model = new BibleModel(SEED); ui.activeId = null; persist(); render(); } });
actionsEl.append(liveBtn, toggle, exp, imp, reset);
}
const fileInput = document.createElement('input');
fileInput.type = 'file'; fileInput.accept = 'application/json'; fileInput.style.display = 'none';
fileInput.onchange = () => {
const f = fileInput.files[0]; if (!f) return;
const reader = new FileReader();
reader.onload = () => {
try { model = new BibleModel(importJSON(reader.result)); persist(); render(); }
catch (e) { alert('Import failed: ' + e.message); }
fileInput.value = '';
};
reader.readAsText(f);
};
document.body.appendChild(fileInput);
function render() {
renderActions();
renderNav(navEl, { categories: CATEGORIES.map(c => ({ key: c.key, label: c.label })), activeKey: ui.activeKey, onSelect: selectCategory });
const cat = categoryByKey(ui.activeKey);
if (ui.activeKey === 'reactions') {
renderMatrix(listEl, { model, onPick: (aura, applied) => {
const id = `${aura}-${applied}`;
if (!model.get('reactions', id)) {
const e = model.add('reactions'); e.id = id; e.aura = aura; e.applied = applied; e.name = `${aura}+${applied}`;
persist();
}
selectEntry(id);
}});
const entry = model.get('reactions', ui.activeId);
renderDetail(detailEl, { category: cat, entry, model, onChange: onFieldChange });
return;
}
if (ui.viewMode === 'table') {
renderTable(listEl, { category: cat, entries: filtered(), model,
onChange: (id, field, value) => { model.setField(ui.activeKey, id, field, value); persist(); render(); },
onSelect: (id) => { ui.viewMode = 'detail'; selectEntry(id); } });
detailEl.innerHTML = '';
return;
}
renderList(listEl, {
entries: filtered(), activeId: ui.activeId, query: ui.query,
onQuery: (q) => { ui.query = q; render(); },
onSelect: selectEntry,
onAdd: () => { const e = model.add(ui.activeKey); persist(); selectEntry(e.id); },
onDuplicate: (id) => { const e = model.duplicate(ui.activeKey, id); persist(); selectEntry(e.id); },
onRemove: (id) => { model.remove(ui.activeKey, id); if (ui.activeId === id) ui.activeId = null; persist(); render(); },
});
const entry = model.get(ui.activeKey, ui.activeId);
renderDetail(detailEl, { category: cat, entry, model, onChange: onFieldChange });
}
function onFieldChange(field, value) {
if (!ui.activeId) return;
model.setField(ui.activeKey, ui.activeId, field, value);
persist();
render();
}
render();
  • Step 2: Replace tools/design-bible/src/views/list.js

Add prefix and live-label class to the label span. Only the label block changes (lines 22-24 of original):

export function renderList(container, opts) {
const { entries, activeId, onSelect, onAdd, onDuplicate, onRemove, query, onQuery } = opts;
container.innerHTML = '';
const search = document.createElement('input');
search.placeholder = 'search…';
search.value = query ?? '';
search.oninput = () => onQuery(search.value);
container.appendChild(search);
const add = document.createElement('button');
add.textContent = '+ Add';
add.style.margin = '8px 0';
add.onclick = () => onAdd();
container.appendChild(add);
const q = (query ?? '').toLowerCase();
for (const e of entries) {
if (q && !(e.name ?? '').toLowerCase().includes(q) && !(e.id ?? '').includes(q)) continue;
const row = document.createElement('div');
row.className = 'listitem' + (e.id === activeId ? ' active' : '');
const label = document.createElement('span');
label.textContent = (e.live ? '' : '') + (e.name ?? e.id);
label.style.cursor = 'pointer';
if (e.live) label.classList.add('live-label');
label.onclick = () => onSelect(e.id);
const dup = document.createElement('button');
dup.textContent = ''; dup.title = 'duplicate'; dup.style.float = 'right'; dup.onclick = () => onDuplicate(e.id);
const del = document.createElement('button');
del.textContent = ''; del.style.float = 'right'; del.onclick = () => onRemove(e.id);
row.append(label, del, dup);
container.appendChild(row);
}
}
  • Step 3: Run bible tests
Terminal window
cd tools/design-bible && node --test 2>&1 | tail -3

Expected: 31 pass — app.js and list.js are UI code, not in the node-testable core.

  • Step 4: Commit
Terminal window
git add tools/design-bible/src/app.js tools/design-bible/src/views/list.js
git commit -m "feat: bible live filter — defaults live-only, toggle button, ◈ badge on live items"

Files:

  • Modify: tools/design-bible/index.html
  • Modify: tools/design-bible/css/style.css

Interfaces:

  • Consumes: T3’s button[data-live="true"] attribute and .live-label class — styles them gold

  • Produces: visual restyle only

  • Step 1: Replace tools/design-bible/index.html

Add Google Fonts preconnect + stylesheet before css/style.css. No other changes.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bullet Heaven &mdash; Design Bible</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header id="topbar"><span class="brand">&#9671; Design Bible</span><span id="actions"></span></header>
<main id="layout">
<nav id="nav"></nav>
<section id="list"></section>
<section id="detail"></section>
</main>
<script type="module" src="src/app.js"></script>
</body>
</html>
  • Step 2: Replace tools/design-bible/css/style.css
:root { --bg:#0a0a12; --panel:#12121f; --line:#2a2a44; --text:#e6e6f0; --accent:#6cc8ff; --muted:#8a8ab0; }
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--text); font:14px/1.5 'JetBrains Mono',monospace; }
#topbar { display:flex; justify-content:space-between; align-items:center; padding:8px 14px; background:linear-gradient(180deg,#14141f 0%,#0a0a12 100%); border-bottom:1px solid #1e1e38; }
.brand { color:var(--accent); font-family:'Orbitron',sans-serif; font-size:15px; letter-spacing:0.08em; text-shadow:0 0 12px rgba(108,200,255,0.5); }
#layout { display:grid; grid-template-columns:200px 280px 1fr; height:calc(100vh - 41px); }
#nav,#list,#detail { overflow:auto; padding:10px; }
#nav,#list { border-right:1px solid var(--line); }
.navitem,.listitem { padding:6px 8px; border-radius:6px; cursor:pointer; }
.navitem:hover,.listitem:hover { background:var(--panel); }
.navitem.active,.listitem.active { background:var(--panel); color:var(--accent); border-left:2px solid var(--accent); padding-left:6px; box-shadow:inset 0 0 10px rgba(108,200,255,0.06); }
button { background:var(--panel); color:var(--text); border:1px solid var(--line); border-radius:6px; padding:5px 10px; cursor:pointer; font-family:'JetBrains Mono',monospace; font-size:12px; transition:border-color 0.15s,box-shadow 0.15s; }
button:hover { border-color:var(--accent); box-shadow:0 0 6px rgba(108,200,255,0.3); }
button[data-live="true"] { border-color:rgba(255,227,77,0.6); color:#ffe34d; box-shadow:0 0 8px rgba(255,227,77,0.35); }
input,select { background:#0d0d18; color:var(--text); border:1px solid var(--line); border-radius:5px; padding:4px 7px; }
input:focus,select:focus { outline:none; border-color:var(--accent); box-shadow:0 0 5px rgba(108,200,255,0.25); }
.field { display:flex; gap:8px; align-items:center; margin:5px 0; }
.field label { width:130px; color:var(--muted); }
.metrics { margin-top:14px; padding:10px; background:var(--panel); border-radius:8px; border:1px solid #1e1e38; }
.metrics .m { display:flex; justify-content:space-between; }
.metrics .m b { color:var(--accent); text-shadow:0 0 6px rgba(108,200,255,0.4); }
.flag { color:#ffcf5a; }
.live-label { color:#ffe34d; }
table.matrix,table.bulk { border-collapse:collapse; font-size:12px; }
table.matrix td,table.matrix th,table.bulk td,table.bulk th { border:1px solid var(--line); padding:3px 6px; text-align:center; }
.cell { cursor:pointer; } .cell:hover { background:rgba(108,200,255,0.08); color:var(--accent); }
  • Step 3: Visual verification
Terminal window
cd tools/design-bible && python3 -m http.server 8080 &

Open http://localhost:8080. Verify (live filter active by default):

  • Brand “◇ Design Bible” renders in Orbitron with cyan glow

  • “◈ Live” button shows gold border and gold text

  • Other buttons use JetBrains Mono, show cyan glow on hover

  • Active nav/list item has a visible cyan left border

  • Fire and Lightning list items show gold “◈ “ prefix

  • Metric values (bold) have a faint cyan glow

  • Reaction matrix cells show cyan tint on hover (not grey)

  • Kill server: kill %1 (or pkill -f "http.server 8080")

  • Step 4: Run bible tests

Terminal window
cd tools/design-bible && node --test 2>&1 | tail -3

Expected: 31 pass.

  • Step 5: Commit
Terminal window
git add tools/design-bible/index.html tools/design-bible/css/style.css
git commit -m "feat: bible neon restyle — Orbitron brand, JetBrains Mono body, gold live button, glow accents"