GENERATED VIEW · PRIVATE ARCHIVE · DO NOT SERVE
RobCo Industries · Archive Museum

UI_CONSISTENCY_AUDIT.md



RELEASE

planning/2.8.0/audits/UI_CONSISTENCY_AUDIT.md

sha256 242075839ddb93ca · 42810 bytes · original held in the private archive

# UI_CONSISTENCY_AUDIT.md — RobCo U.O.S. v2.6.0 > **FILED UNDER 2.8.0 — PRODUCED-BY, NOT MEASURED-VERSION.** This document *measures* the earlier v2.6.0/v2.7.0 > codebase, but it was *produced* as a stage of the PARKED-CHAIN audit sequence — the analysis phase that became > the 2.8.0 overhaul. Planning artifacts are filed by the release whose work produced them, never by the version > they discuss. The version in the title is the code it audited, not the folder it belongs in. > **[ARCHIVE-class snapshot — not current truth.]** Per the 2.8.5 U-B1 doc-maintenance model (see `CLAUDE.md`'s > Reference Pointer Index), this doc is frozen as of the date below and is never updated to track current code. It > audited a codebase roughly 30% smaller than today's (pre-`test-console.js`, pre-OCR, pre-Diagnostic-Shell, > pre-CHASSIS/Overseer). Reuse its structural reasoning as historical input only — re-verify every measurement and > file-split proposal against current code before acting on it. > Stage A7 of the PARKED-CHAIN audit sequence. PLAN-ONLY. Nothing here is implemented or committed. > Companion docs: DIEGETIC_AUDIT.md (A0) · PERFORMANCE_AUDIT.md (A1) · ACCESSIBILITY_AUDIT.md (A2) · > CONTENT_AUDIT.md (A3) · MOBILE_AUDIT.md (A4) · CODE_QUALITY_AUDIT.md (A5) · TEST_STRENGTH_AUDIT.md (A6). > Target: bc8c2eb (v2.6.0-r1, 1078 tests / 89 suites). > > Files studied: index.html · css/terminal.css · js/ui-render.js · js/ui-core.js · js/ui-saves.js · js/ui-account.js --- ## Executive summary The tracker panels (collectibles, Lincoln, traits, skill books, magazines) established a consistent **compact collapsible** pattern as they were built in v2.4–v2.6. That pattern is now the house standard. The problem is that **four older panels predate it and were never brought up to it**, and **three new sub-panels were added without wiring the persistence mechanism**. The row template is correct across all five tracker functions but is repeated as raw inline styles five times rather than being a shared CSS class — meaning the pattern is consistent but brittle, and the next new tracker will drift again. The enforcement story is worse: there are no gate guards that would catch a new panel deviating from the standard. Everything identified here that has slipped through has done so because the tests only check for the existence of certain strings, not structural compliance. The single highest-value change is extracting the tracker row inline styles into `.tracker-row` / `.tracker-toggle` CSS classes AND replacing the `<span onclick>` toggle pattern with `<button class="tracker-toggle">` — this fix simultaneously resolves the A2 accessibility finding (no keyboard access to tracker toggles) and locks in the pattern for future trackers. --- ## THE TARGET STANDARD (reference) Established by the most-recently authored panels (collectibles → Lincoln → traits → skill books → magazines → craft). This is the declared house standard for all current and future panels. ### Panel anatomy ```html <!-- Top-level panel --> <details class="panel" data-tab="TABNAME"> <summary><h2>&gt; PANEL TITLE</h2></summary> <!-- panel content --> <details class="sub-panel" data-sub-id="unique_key"> <summary><h3>&gt; SUBSECTION TITLE [n/total]</h3></summary> <!-- sub-panel content --> </details> </details> ``` - All panels: `<details class="panel" data-tab="X">` — collapse via CSS `details.panel summary h2::after` - Panel badge: `[n]` via `_updatePanelBadges()` appended to the h2 - Sub-panels: `<details class="sub-panel" data-sub-id="KEY">` — sub-badge text updated by the `render*()` function as `> SUBTITLE [n/total]` - Persistence: both panel and sub-panel open/closed state in `robco_panel_state` localStorage — panels via the static loop in `ui-core.js:265-312`; dynamically-rendered sub-panels wire their own toggle handlers within the `render*()` function ### Tracker row anatomy (currently inline, should become a class) ```html <div style="font-size:11px;letter-spacing:0.5px;margin-bottom:2px;[opacity:0.7;]"> <span style="[color/opacity];cursor:pointer;margin-right:4px;" onclick="toggle*('SAFENAME')">[STATUS]</span> ITEM NAME <span style="font-size:10px;opacity:0.6;">— metadata</span> </div> ``` TARGET (after extraction): ```html <div class="tracker-row [tracker-row--active]"> <button class="tracker-toggle [tracker-toggle--active]" data-name="SAFENAME" onclick="toggle*(this.dataset.name)">[STATUS]</button> <span class="tracker-name">ITEM NAME</span> <span class="tracker-meta">— metadata</span> </div> ``` ### Empty state convention ```js container.innerHTML = emptyState('No X found'); // sentence case, uses shared helper ``` NOT: `'<span class="empty-state" style="font-size:11px;">No X</span>'` (tracker-internal variant) ### Button label convention (emergent, should be formal) | Context | Format | Example | |---|---|---| | Create/add | `+ VERB NOUN` | `+ ADD ITEM`, `+ ADD QUEST` | | Execute/command | `> VERB NOUN` | `> TRANSMIT PROTOCOL`, `> WIPE TERMINAL` | | State toggle | `[ STATUS ]` | `[ACQUIRED]`, `[READ]`, `[SEL]` | | Macro/quick-action | `[NOUN]` | `[THREAT]`, `[VATS]` | --- ## Summary counts | Severity | Count | |---|---| | **CRITICAL** | 0 | | **HIGH** | 3 | | **MEDIUM** | 7 | | **LOW** | 9 | | **SKIP/INFO** | 4 | | **Total findings** | **23** | --- ## SECTION 1 — Panel/Sub-Panel Structure Deviations --- ### UC-STR-1 · [MEDIUM] · `ammoSubPanel` missing `data-sub-id` — collapse not persisted **Location:** `index.html:583` — `<details id="ammoSubPanel" class="sub-panel">` The AMMO RESERVES sub-panel uses `class="sub-panel"` (correct) but has no `data-sub-id` attribute. The persistence loop in `ui-core.js:294` selects on `details[data-sub-id]` — an element without that attribute is silently skipped. The user's choice to collapse AMMO RESERVES is forgotten on every reload. Every other HTML-static sub-panel (`collectiblesSubPanel`, `lincolnSubPanel`, `traitsSection`, `craft_breakdown`) has a `data-sub-id`. This is the only one missing it. **Fix:** ```html <!-- index.html:583 — change: --> <details id="ammoSubPanel" class="sub-panel"> <!-- to: --> <details id="ammoSubPanel" class="sub-panel" data-sub-id="ammo_reserves"> ``` **File touches:** `index.html` only. No cache bump beyond the served-file bump for any batch that includes index.html. **Verdict:** BUILD-NOW · `index.html` --- ### UC-STR-2 · [MEDIUM] · Faction "MINOR FACTIONS" rendered sub-panel is not a `sub-panel` — not persisted **Location:** `js/ui-render.js:1557-1565` — `renderFactionRep()` builds: ```js container.innerHTML = `... <details style="margin-top:6px;"> <summary class="config-summary" style="font-size:11px;opacity:0.6;padding:2px 0;">MINOR FACTIONS</summary> <div class="faction-grid" style="margin-top:5px;">...</div> </details>`; ``` This `<details>` has no `class="sub-panel"`, no `data-sub-id`, and no persistence handler. The user's choice to expand MINOR FACTIONS is forgotten after any re-render (which happens on every F+/F-/I+/I- click — the open state is manually preserved across re-renders for the re-render itself via `minorWasOpen`, but not across page reloads). The comment `// Restore open state after re-render` only addresses the in-session problem, not the cross-reload problem. **Fix:** ```js // Change the details tag in the template: `<details class="sub-panel" data-sub-id="factions_minor" style="margin-top:6px;"> <summary><h3>&gt; MINOR FACTIONS</h3></summary> <div class="faction-grid" style="margin-top:5px;">...</div> </details>` // Add persistence handler after container.innerHTML = ... (same pattern as renderSkillBooks): const minorSubPanel = container.querySelector('details[data-sub-id="factions_minor"]'); if (minorSubPanel) { // Restore saved state try { const ps = JSON.parse(localStorage.getItem('robco_panel_state') || '{}'); if (ps['factions_minor'] === true) minorSubPanel.open = true; else if (ps['factions_minor'] === false) minorSubPanel.open = false; // else default (closed) on first load } catch (_) {} minorSubPanel.addEventListener('toggle', () => { try { const p = JSON.parse(localStorage.getItem('robco_panel_state') || '{}'); p['factions_minor'] = minorSubPanel.open; localStorage.setItem('robco_panel_state', JSON.stringify(p)); } catch (_) {} }); } ``` Also change the `minorWasOpen` approach — if using the persisted `robco_panel_state`, the re-render can restore from `ps['factions_minor']` directly instead of the DOM-snapshot approach. **File touches:** `js/ui-render.js` **Verdict:** BUILD-NOW · `js/ui-render.js` --- ### UC-STR-3 · [LOW] · Audio Systems panel is bare `<details style="...">` with no persistence **Location:** `index.html:1310` — `<details id="audioSystemsDetails" style="margin-top: 10px">` Uses `class="config-summary"` summary (Security panel convention) but no `data-sub-id`. The Audio Systems section collapses when the user closes it, then re-opens to whatever default on reload. The user's expanded/collapsed preference is not remembered. This is lower priority than UC-STR-2 because Audio Systems is rarely changed and lives inside the Security & Configuration panel (which is itself rarely visited). But it violates the principle that every `<details>` should persist. **Fix:** ```html <!-- index.html:1310 — change: --> <details id="audioSystemsDetails" style="margin-top: 10px"> <summary class="config-summary" style="color: var(--robco-alert); font-size: 13px"> <!-- to: --> <details id="audioSystemsDetails" class="sub-panel" data-sub-id="audio_systems" style="margin-top: 10px"> <summary><h3 style="color: var(--robco-alert);">&gt; AUDIO SYSTEMS</h3></summary> ``` **File touches:** `index.html`. The alert-color on the summary can stay as an inline h3 style (it is the correct alert-context color) or as a modifier class. **Verdict:** BUILD-NOW (trivial) · `index.html` --- ### UC-STR-4 · [LOW] · Campaign Config sub-panels (Status/Crossroads/Timeline) not persisted **Location:** `index.html:1109`, `1122`, `1145` — three `<details style="margin-bottom: 10px">` with `class="config-summary"` summary, no `data-sub-id`. These are inside the CAMPG tab (Campaign Configuration panel), which has a different interaction context — it's the configuration panel, not a data tracker, and defaults open. Lower urgency. But consistently: if the user collapses "Campaign Status", it should stay collapsed. **Fix:** Add `data-sub-id` to each: - `campaignStatusPanel` → already has `id="campaignStatusPanel"` but the static persistence loop selects on `data-sub-id`, not `id`. Add `data-sub-id="campaign_status"`. - Crossroads: `data-sub-id="campaign_crossroads"` - Timeline: `data-sub-id="campaign_timeline"` The IDs already exist; only `data-sub-id` attributes are needed for the existing persistence loop to pick them up. **File touches:** `index.html` only. **Verdict:** BUILD-NOW (one attribute per element, 3 lines) · `index.html` --- ## SECTION 2 — Density / Spacing / Typography Consistency --- ### UC-DENS-1 · [HIGH] · Tracker row pattern is repeated 5× as inline styles — no shared CSS class **Location:** `js/ui-render.js` — `renderCollectibles()`, `renderLincolnMemorabilia()`, `renderTraits()`, `renderSkillBooks()`, `renderMagazines()` all build rows with this identical inline style pattern: ```js `<div style="font-size:11px;letter-spacing:0.5px;margin-bottom:2px;">` `<span style="color:var(--robco-green);cursor:pointer;margin-right:4px;" onclick="...">` `<span style="font-size:10px;opacity:0.6;">` ``` The pattern IS consistent — but it's inline. There is no `.tracker-row` CSS class. This means: 1. Any future tracker will repeat the same inline blocks or visually drift. 2. A density change (e.g., `margin-bottom: 3px` for mobile) must be applied in 5 places. 3. There's no gate guard that would catch a new tracker using different pixel values. **Fix — extract to CSS in `terminal.css`:** ```css /* Tracker rows — used by collectibles, Lincoln, traits, skill books, magazines */ .tracker-row { font-size: 11px; letter-spacing: 0.5px; margin-bottom: 2px; display: flex; align-items: baseline; gap: 0; /* match current margin-right: 4px on toggle span */ } .tracker-row--inactive { opacity: 0.7; } .tracker-toggle { /* replaces <span onclick> — see UC-CTL-1 for full treatment */ font-family: inherit; font-size: 11px; background: transparent; border: none; padding: 0; margin-right: 4px; cursor: pointer; color: inherit; letter-spacing: 0.5px; min-height: 28px; /* mobile tap target — A2/Protocol 17 */ min-width: 28px; display: inline-flex; align-items: center; } .tracker-toggle--active { color: var(--robco-green); } .tracker-toggle--inactive { opacity: 0.5; } .tracker-name { /* replaces direct text node after the toggle */ } .tracker-meta { font-size: 10px; opacity: 0.6; } ``` **Then in each `render*()` function:** ```js // BEFORE: `<div style="font-size:11px;letter-spacing:0.5px;margin-bottom:2px;">`+ `<span style="color:var(--robco-green);cursor:pointer;margin-right:4px;" onclick="...">[ACQUIRED]</span>`+ // AFTER: `<div class="tracker-row tracker-row--active">`+ `<button class="tracker-toggle tracker-toggle--active" data-name="${safeName}" onclick="...">[ACQUIRED]</button>`+ ``` **This change simultaneously resolves UC-CTL-1 (span→button) and the A2 accessibility finding (keyboard access to tracker toggles). It is the single highest-value change in this audit.** **A2 cross-reference:** ACCESSIBILITY_AUDIT.md cited all `<span onclick>` tracker toggles as needing `<button>` for keyboard operability. This extraction creates the CSS class infrastructure that makes the A2 fix clean and complete. **Protocol 17 cross-reference:** The `.tracker-toggle` class can enforce `min-height: 28px; min-width: 28px` in the CSS, fulfilling the mobile tap-target requirement in one place rather than hoping each render function gets it right. **File touches:** `css/terminal.css`, `js/ui-render.js` (all 5 tracker render functions) **Verdict:** SPEC-FIRST (cross-cuts 5 render functions + 1 CSS file; must be done as one atomic change) · **HIGH** --- ### UC-DENS-2 · [MEDIUM] · Two density tiers coexist without a documented standard — legacy 12px vs tracker 11px **Location:** All panels The app has two distinct row density tiers: | Tier | Panels | Row element | Font | Padding | Classes used | |---|---|---|---|---|---| | **Legacy** (pre-v2.4) | Perks, Quests, Notes, Status Effects, Squad | `<li>` in `<ul class="notes-list">` | 12px | `padding: 3px 0` + border-bottom | `notes-list`, `list-row-prefix`, `list-row-content`, `delete-btn` | | **Tracker** (v2.4+) | Collectibles, Lincoln, Traits, Skill Books, Magazines | `<div>` inline | 11px | `margin-bottom: 2px`, no border-bottom | None (all inline) | Neither tier is wrong in isolation, but having two undocumented tiers means future panels have no clear guidance on which to use. The tracker tier is more compact and space-efficient; the legacy tier is more semantically correct (lists ARE lists). **Fix options:** 1. **Option A (SPEC-FIRST):** Migrate legacy panels to the tracker density. Requires reworking `renderPerks()`, `renderQuests()`, `renderStatus()`, `renderCampaignNotes()`, `renderSquad()`. 2. **Option B (documentation only):** Formalize the distinction — tracker tier for registry-backed read-only toggle lists; legacy tier for user-created/mutable lists. Document in RULES.md Protocol UI-2 and gate guard against mixing. Option B is the safer choice. The legacy panels' use of semantic `<ul>/<li>` with `delete-btn` is correct for mutable lists. The tracker panels' compact `<div>` rows are correct for fixed-registry toggle lists. The inconsistency is aesthetic (11px vs 12px), not structural. **Verdict:** SPEC-FIRST (Option A) or document-only (Option B) · **MEDIUM** --- ### UC-DENS-3 · [LOW] · Security & Configuration panel uses inline styles for its alert theming **Location:** `index.html:1219-1220` ```html <details class="panel" style="border-color: var(--robco-alert);"> <summary><h2 style="border-color: var(--robco-alert); color: var(--robco-alert);"> &gt; SECURITY &amp; CONFIGURATION </h2></summary> ``` This is correct behavior (the Security panel should visually distinguish itself as the alert-context panel) but the styling is inline rather than a modifier class. A future alert-themed panel (or a theme change) requires tracking down the inline attributes. **Fix:** Add `.panel--alert` modifier class to `terminal.css`: ```css .panel--alert { border-color: var(--robco-alert); } .panel--alert .panel h2, details.panel--alert > summary > h2 { border-color: var(--robco-alert); color: var(--robco-alert); } ``` Then `index.html:1219`: `<details class="panel panel--alert">` with no inline styles. **File touches:** `css/terminal.css`, `index.html` **Verdict:** BUILD-NOW (CSS-only, purely cosmetic) · **LOW** --- ## SECTION 3 — Control Consistency --- ### UC-CTL-1 · [HIGH] · All 5 tracker toggle elements are `<span onclick>` — not keyboard-accessible **Location:** `js/ui-render.js` — every tracker render function builds toggles as: ```js `<span style="color:var(--robco-green);cursor:pointer;margin-right:4px;" onclick="toggleCollectible('${safeName}')">[ACQUIRED]</span>` ``` This is consistent WITHIN the tracker tier — all 5 use the identical pattern. But `<span onclick>` is not focusable by keyboard, not announced as interactive by screen readers, and fails the A2 accessibility standard (WCAG 4.1.2 Name, Role, Value — interactive elements must be natively interactive). This finding is shared with ACCESSIBILITY_AUDIT.md which cites the same `<span onclick>` elements. The fix here and the A2 fix are the same change — do them together. **Fix:** See UC-DENS-1 for the `<button class="tracker-toggle">` extraction. The accessibility and consistency fixes are one and the same change. Fixing only this (without extracting the CSS class from UC-DENS-1) would still leave the inline styles problem. **File touches:** `js/ui-render.js` (5 functions), `css/terminal.css` **Cross-references:** ACCESSIBILITY_AUDIT.md, Protocol 17 (28px tap target) **Verdict:** SPEC-FIRST (same implementation as UC-DENS-1) · **HIGH** --- ### UC-CTL-2 · [MEDIUM] · Faction `renderFactionRep()` tooltip/label says "±50" — actual delta is ±5 **Location:** `js/ui-render.js:1530` (the helper text rendered inside `container.innerHTML`): ```js `<div style="font-size:9px;opacity:0.45;margin-bottom:4px;letter-spacing:0.5px;"> F+/F- = Fame ±50 &nbsp; I+/I- = Infamy ±50 &nbsp; BAR = F/(F+I) ratio </div>` ``` The actual `adjustFaction()` function at `ui-render.js:1484` applies a delta of `5`, not `50`: ```js function adjustFaction(key, field, delta) { state.factions[key][field] = Math.max(0, Math.min(1000, cur + delta)); ``` And the buttons call `adjustFaction('key','fame',5)` and `adjustFaction('key','fame',-5)`. The tooltip is factually wrong. A5 (CODE_QUALITY_AUDIT) also flagged this. Users who rely on the label to understand the increment will be misled. **Fix:** ```js // Change line in renderFactionRep() template: `F+/F- = Fame ±5 &nbsp; I+/I- = Infamy ±5 &nbsp; BAR = F/(F+I) ratio` ``` **File touches:** `js/ui-render.js` only. **Verdict:** BUILD-NOW (one string change; bug fix) · **MEDIUM** --- ### UC-CTL-3 · [MEDIUM] · Lincoln disposition `<select>` has inline styles — no shared class **Location:** `js/ui-render.js:~755`: ```js `<select data-lname="${safeName}" onchange="setLincolnDisposition(this.dataset.lname,this.value)" style="font-size:11px;background:transparent;color:inherit;border:1px solid var(--robco-green); min-height:28px;cursor:pointer;">` ``` This is the only select element inside a tracker row. Its inline styles are not shared with any other select. The `min-height: 28px` for tap target is good (Protocol 17) but would be lost in a tracker refactor. **Fix:** Extract to a `.tracker-select` CSS class in `terminal.css`: ```css .tracker-select { font-family: inherit; font-size: 11px; background: transparent; color: inherit; border: 1px solid var(--robco-green); min-height: 28px; cursor: pointer; } ``` **File touches:** `css/terminal.css`, `js/ui-render.js` **Verdict:** BUILD-NOW (simple extraction; should be part of UC-DENS-1 batch) · **MEDIUM** --- ## SECTION 4 — Badge Format Deviations --- ### UC-BADGE-1 · [LOW] · Skill Books and Magazines show `[n]` in sub-panel h3 — not `[n/total]` **Location:** `js/ui-render.js:947, 1036` — `renderSkillBooks()` and `renderMagazines()`: ```js `<summary><h3>&gt; READ [${readDefs.length}]</h3></summary>` `<summary><h3>&gt; UNREAD [${unreadDefs.length}]</h3></summary>` ``` Compare to `renderCollectibles()` which shows `[acquiredCount/total]` and `renderLincolnMemorabilia()` which shows `[foundCount/9]`. The READ/UNREAD split is semantically different (the list is exhaustive — there are no "unknown" entries) but the asymmetry is visible. The total IS knowable: `defs.length` is available in both functions. `> READ [2/13]` is more informative than `> READ [2]` because the user sees how many books/magazines exist in total. **Fix:** ```js // renderSkillBooks(): `<summary><h3>&gt; READ [${readDefs.length}/${defs.length}]</h3></summary>` `<summary><h3>&gt; UNREAD [${unreadDefs.length}/${defs.length}]</h3></summary>` // renderMagazines(): `<summary><h3>&gt; READ [${readDefs.length}/${defs.length}]</h3></summary>` `<summary><h3>&gt; UNREAD [${unreadDefs.length}/${defs.length}]</h3></summary>` ``` **File touches:** `js/ui-render.js` **Verdict:** BUILD-NOW (2-char change per line) · **LOW** --- ### UC-BADGE-2 · [LOW] · `renderTraits()` count shown in a content div, not the summary h3 **Location:** `js/ui-render.js:832`: ```js let html = `<div style="font-size:11px;color:var(--robco-blue);font-weight:bold; letter-spacing:1px;margin-bottom:4px;">TRAITS [${n}/2]</div>`; ``` The Traits tracker doesn't have its own sub-panel — it's a sub-panel ITSELF (`#traitsSection data-sub-id="traits"`), not a container of sub-panels. So the count goes in the content area as a header div, not in the h3. The inconsistency: all other trackers update the h3 text to include the count. Traits can't do that cleanly because the `> TRAITS` text needs to be in the static h3 in `index.html` for accessibility, and the count is dynamic. **Fix:** Update the h3 in `#traitsSection > summary` dynamically, matching the pattern of `renderCollectibles()` which does: ```js const collectiblesH3 = document.querySelector('#collectiblesSubPanel > summary > h3'); if (collectiblesH3) collectiblesH3.textContent = `> ${typeLabel} [${acquiredCount}/${total}]`; ``` Apply the same approach in `renderTraits()`: ```js const traitsH3 = document.querySelector('#traitsSection > summary > h3'); if (traitsH3) traitsH3.textContent = `> TRAITS [${n}/2]`; // Then remove the header div from the html template. ``` **File touches:** `js/ui-render.js` **Verdict:** BUILD-NOW · **LOW** --- ### UC-BADGE-3 · [LOW] · FACTION STANDING panel has no panel-level badge **Location:** `js/ui-core.js:797-847` — `_updatePanelBadges()` does not include `> FACTION STANDING`. Factions don't have a simple item count — they show standing labels. But the FACT that the user has factions with non-neutral standing is meaningful. A badge of `[n hostile]` or just `[n]` for total tracked factions could be informative. Note: this is an intentional-seeming omission (factions are always present in any NV/FO3 playthrough) rather than a bug. The F:n/I:n display inside each card already provides the relevant information. **Fix (if desired):** Add to `_updatePanelBadges()`: ```js { h2text: '> FACTION STANDING', count: Object.values(state.factions || {}).filter(f => (f.fame + f.infamy) > 0).length }, ``` This shows the count of factions with any rep action taken. **Verdict:** SKIP (intentional design choice; no user-facing regression) · **LOW** --- ### UC-BADGE-4 · [INFO] · `renderCollectibles()` empty state uses inline style rather than `emptyState()` helper **Location:** `js/ui-render.js:675`: ```js container.innerHTML = '<span class="empty-state" style="font-size:11px;">No collectibles registry loaded</span>'; ``` All other empty states use the shared `emptyState(msg)` helper from `ui-core.js`: ```js function emptyState(msg) { return `<span class="empty-state">${msg}</span>`; } ``` The tracker functions don't use `emptyState()` because they were authored in `ui-render.js` and the helper lives in `ui-core.js` (load order dependency — `ui-core.js` loads after `ui-render.js`). Actually, `emptyState()` is defined in `ui-core.js` which loads AFTER `ui-render.js` — but `renderCollectibles()` is only CALLED from `loadUI()` in `ui-core.js`, so by call time `emptyState()` exists. The tracker functions CAN use `emptyState()` — the scripts are all globals on `window`. **Fix:** In `renderCollectibles()`, `renderTraits()`, etc. — replace inline `<span class="empty-state" style="...">` with `emptyState('message')`. Remove the `style="font-size:11px;"` override (the `.empty-state` class in `terminal.css` handles color; font-size should inherit from `.tracker-row` context after UC-DENS-1 is done). **File touches:** `js/ui-render.js` **Verdict:** BUILD-NOW (4-line change) · **LOW** --- ## SECTION 5 — Ordering / Empty-State Consistency --- ### UC-ORDER-1 · [MEDIUM] · Quest log shows no ordering — active quests not sorted first **Location:** `js/ui-render.js:582-607` — `renderQuests()` renders `state.quests` in array order (addition order). A player can have completed and failed quests in the list; active quests are not sorted to the top. The panel badge in `_updatePanelBadges()` counts only active quests — implying active quests are the most relevant. But the render order doesn't reflect this. **Fix:** Sort before rendering: ```js const sorted = [...state.quests].sort((a, b) => { const order = { active: 0, undefined: 0, complete: 1, failed: 2 }; return (order[a.status] ?? 0) - (order[b.status] ?? 0); }); ``` **File touches:** `js/ui-render.js` **Verdict:** BUILD-NOW · **MEDIUM** --- ### UC-ORDER-2 · [LOW] · Empty-state text inconsistency: ALL CAPS vs sentence case **Location:** Across `js/ui-render.js` | Panel | Empty state | Style | |---|---|---| | Skill Books | `NO BOOKS READ` / `ALL BOOKS READ` | ALL CAPS | | Magazines | `NO MAGAZINES READ` / `ALL MAGAZINES READ` | ALL CAPS | | Collectibles | `No collectibles registry loaded` | Sentence case | | Traits | `No matching traits.` | Sentence case | | Perks | `No perks acquired` | Sentence case | | Quests | `No active quests` | Sentence case | | Craft | `No recipes for this game.` / `No breakdown recipes for this game.` | Sentence case | Skill books and magazines diverge because their empty states are LIST-SECTION empty states (`NO BOOKS READ` means the READ sub-panel is empty), while all others are PANEL empty states. The difference is meaningful: `NO BOOKS READ` = "this sub-list has 0 items" vs `No perks acquired` = "the entire panel has 0 items." **Fix (small):** Standardize to sentence case for all empty states for visual uniformity: - `NO BOOKS READ` → `No books read` - `ALL BOOKS READ` → `All books read` - Same for magazines. This is cosmetic but ensures the `.empty-state` CSS class renders consistently. **File touches:** `js/ui-render.js` **Verdict:** BUILD-NOW (string changes) · **LOW** --- ## SECTION 6 — Collapse-State Persistence (Summary — individual findings above in Section 1) | Sub-panel | `data-sub-id` | Persisted? | Status | |---|---|---|---| | `ammoSubPanel` | ✗ absent | ✗ no | **UC-STR-1 — needs data-sub-id** | | `collectiblesSubPanel` | `collectibles_main` | ✓ yes | ✓ | | `lincolnSubPanel` | `lincoln_memorabilia` | ✓ yes | ✓ | | `traitsSection` | `traits` | ✓ yes | ✓ | | `craft_breakdown` | `craft_breakdown` | ✓ yes | ✓ | | Faction MINOR FACTIONS | ✗ absent (rendered) | ✗ no | **UC-STR-2 — needs handler in render fn** | | Skill Books READ | `skill_books_read` | ✓ yes (wired in render fn) | ✓ | | Skill Books UNREAD | `skill_books_unread` | ✓ yes | ✓ | | Magazines READ | `magazines_read` | ✓ yes | ✓ | | Magazines UNREAD | `magazines_unread` | ✓ yes | ✓ | | Audio Systems | ✗ absent | ✗ no | **UC-STR-3 — needs data-sub-id** | | Campaign Status | ✗ absent | ✗ no | **UC-STR-4 — needs data-sub-id** | | Campaign Crossroads | ✗ absent | ✗ no | **UC-STR-4** | | Campaign Timeline | ✗ absent | ✗ no | **UC-STR-4** | --- ## SECTION 7 — Naming / Heading Format --- ### UC-NAME-1 · [LOW] · Campaign Config sub-panel summaries use `config-summary` class instead of `<h3>` heading **Location:** `index.html:1111-1115` (and 1125, 1148): ```html <summary class="config-summary" style="font-size: 12px; opacity: 0.85"> CAMPAIGN STATUS </summary> ``` The tracker sub-panels all use `<summary><h3>&gt; SUBSECTION</h3></summary>`. The Campaign Config sub-panels use bare text in a `config-summary` summary. This means: (a) no `>` prefix character; (b) no h3 for heading semantics; (c) the `[+]/[-]` indicator comes from `config-summary::after` instead of `details.sub-panel summary h3::after`. Both CSS paths produce the same visual `[+]/[-]` toggle. The semantic inconsistency (h3 vs no h3) affects screen reader heading navigation. **Fix:** Convert to h3-in-summary: ```html <summary><h3>&gt; CAMPAIGN STATUS</h3></summary> ``` Remove `class="config-summary"` and the inline style (the h3 inside sub-panel summary inherits the correct opacity from `details.sub-panel > summary h3 { opacity: 0.85; }`). **File touches:** `index.html` **Verdict:** BUILD-NOW (5 elements) · **LOW** --- ### UC-NAME-2 · [INFO] · COMM-LINK chat panel uses `<h2>` outside `<details>` — structurally special **Location:** `index.html:1719` — The chat panel is `<div class="panel chat-panel" style="border-color: var(--robco-blue)">` not a `<details class="panel">`. It's the only non-collapsible panel. This is intentional — the chat panel is always visible and always occupies the right column on desktop. It should NOT be a collapsible `<details>`. ✓ But it uses `<h2>` without `<summary>` (because there's no `<details>`), which is correct. The `panel--blue` modifier class pattern from UC-DENS-3 could apply here too (`<div class="panel chat-panel panel--blue">` instead of inline border-color). **Verdict:** SKIP (intentional structural exception) · **INFO** --- ## SECTION 8 — PROPOSED PROTOCOLS (for RULES.md / CLAUDE.md) The following protocol text should be appended to RULES.md and CLAUDE.md in the same commit as their gate guards. --- ### Protocol UI-1 — Panel Structure Standard Every new top-level panel must use the standard collapsible structure: ```html <details class="panel" data-tab="TABNAME"> <summary><h2>&gt; PANEL TITLE</h2></summary> <!-- content --> </details> ``` Sub-panels must use: ```html <details class="sub-panel" data-sub-id="unique_snake_case_key"> <summary><h3>&gt; SUBSECTION TITLE [n/total]</h3></summary> <!-- content --> </details> ``` - `data-sub-id` is **required** for every `<details class="sub-panel">` — without it the collapse state is not persisted. - `class="config-summary"` is reserved for the CAMPG tab configuration context only. Do not use it in data panels. - Dynamically-rendered sub-panels (built inside `render*()` functions) must wire their own `toggle` persistence handler using `robco_panel_state` — see `renderSkillBooks()` as the reference. - The `>` prefix is required on all panel and sub-panel heading text (matching the terminal prompt aesthetic). ### Protocol UI-2 — Tracker Row Pattern Registry-backed toggle-list trackers (collectibles, Lincoln, traits, skill books, magazines, and any future game-data tracker) must use the standard tracker row classes: ```html <div class="tracker-row [tracker-row--active|tracker-row--inactive]"> <button class="tracker-toggle [tracker-toggle--active|tracker-toggle--inactive]" data-name="SAFENAME" onclick="toggle*(this.dataset.name)">[STATUS]</button> <span class="tracker-name">ITEM NAME</span> <span class="tracker-meta">— metadata</span> </div> ``` - Always `<button>`, never `<span onclick>` — this is both a consistency rule and an accessibility requirement (Protocol 17, WCAG 4.1.2). - The `.tracker-toggle` CSS class must enforce `min-height: 28px; min-width: 28px` for mobile tap targets (Protocol 17). - Do NOT add inline `style=` attributes to tracker rows — if the visual needs to change, update the CSS class. - User-created/mutable lists (perks, quests, notes, status, squad) continue using the legacy `<ul class="notes-list"><li>` pattern with `delete-btn`. Only fixed-registry toggle lists use the tracker pattern. ### Protocol UI-3 — Badge Format Standard - **Panel-level badge:** plain count `[n]` via `_updatePanelBadges()`, appended to the panel `<h2>`. - **Sub-panel badge (in h3 text):** `[n/total]` when the total is finite and known; `[n]` when no meaningful total exists. Update the h3 text on every `render*()` call. - **Content-area count (rare):** use only when the panel has no sub-panel to host the h3 — update via `querySelector` as in `renderTraits()`. - Never hardcode the total (e.g., `/9`) — always reference `defs.length`. ### Protocol UI-4 — Collapse Persistence Required Every `<details>` element visible to the user must have its open/closed state persisted in `robco_panel_state`. This means: - Static HTML `<details class="sub-panel">`: add `data-sub-id="unique_key"`. - Dynamically-rendered `<details class="sub-panel">`: wire the toggle handler in the `render*()` function. See `renderSkillBooks()` for the reference pattern. - No `<details style="...">` at visible scope without a class (`sub-panel` or `panel`) and a `data-sub-id`. ### Protocol UI-5 — Button Label Convention | Context | Format | Example | |---|---|---| | Create / add item | `+ VERB NOUN` | `+ ADD ITEM`, `+ ADD QUEST` | | Execute / command action | `> VERB NOUN` | `> TRANSMIT PROTOCOL`, `> WIPE TERMINAL` | | State indicator / toggle | `[STATUS]` | `[ACQUIRED]`, `[READ]`, `[SEL]`, `[MISSING]` | | Macro quick-action | `[NOUN]` | `[THREAT]`, `[VATS]`, `[LOOT]` | | Danger/destructive | `> VERB NOUN — QUALIFIER` | `> WIPE TERMINAL — NEW CAMPAIGN` | Do not mix formats within a panel. The `>` prefix is the terminal command convention; `+` is the add convention; `[NOUN]` is the state/toggle convention. These are visually distinct and semantically consistent with the in-world terminal aesthetic. --- ## SECTION 9 — PROPOSED GATE GUARDS Each guard should be added to **both** `tests/check-persistence.js` and `tests/check-persistence.ps1` as a new suite (e.g., Suite 88 — UI Consistency). All guards are static source checks (no runtime execution needed). Protocol 2a test count sync required in the same commit. --- ### GATE-UI-1 · Every `<details class="panel">` has `<summary>` with `<h2>` starting with `> ` ```js // Extract all panel details elements from index.html source const panelMatches = [...indexSrc.matchAll(/<details[^>]*class="[^"]*panel[^"]*"/g)]; // For each, check the following summary contains <h2>&gt; (or >) // Allow chat-panel (has no summary — it's a div) as the documented exception assert(panelMatches.every(m => { // simplified: check each details.panel is followed within 200 chars by <summary><h2>&gt; const segment = indexSrc.slice(m.index, m.index + 300); return segment.includes('<summary><h2>') || segment.includes('chat-panel'); }), 'All details.panel have <summary><h2>> heading'); ``` --- ### GATE-UI-2 · Every `<details class="sub-panel">` in index.html has `data-sub-id` ```js const subPanelMatches = [...indexSrc.matchAll(/<details[^>]*class="sub-panel"[^>]*>/g)]; subPanelMatches.forEach((m, i) => { assert(m[0].includes('data-sub-id'), `sub-panel #${i} has data-sub-id attribute`); }); ``` This catches UC-STR-1 (ammoSubPanel missing data-sub-id) and any future HTML sub-panel added without it. --- ### GATE-UI-3 · No `<span` with `onclick` containing `toggle` in tracker render functions ```js // Extract renderCollectibles, renderLincolnMemorabilia, renderTraits, // renderSkillBooks, renderMagazines function bodies from ui-render.js const trackerFnNames = ['renderCollectibles', 'renderLincolnMemorabilia', 'renderTraits', 'renderSkillBooks', 'renderMagazines']; // ... extract each function body ... assert(!/<span[^>]+onclick[^>]*toggle/.test(trackerBodies), 'Tracker toggles use <button>, not <span onclick>'); ``` This encodes the UC-CTL-1 fix as a permanent ratchet guard — once the `<span onclick>` are replaced with `<button>`, this test prevents them from coming back. Must be added in the same commit as UC-CTL-1 / UC-DENS-1. --- ### GATE-UI-4 · Every tracker render function uses `emptyState()`, not inline `<span class="empty-state"` ```js const trackerFnBodies = extractFunctions(uiRenderSrc, ['renderCollectibles', 'renderLincolnMemorabilia', 'renderTraits', 'renderSkillBooks', 'renderMagazines']); assert(!/<span[^>]*class=["']empty-state["'][^>]*style=/.test(trackerFnBodies), 'Tracker render functions use emptyState() helper, not inline-styled empty state spans'); ``` --- ### GATE-UI-5 · Faction MINOR FACTIONS sub-panel template in `renderFactionRep()` uses `class="sub-panel"` + `data-sub-id` ```js // Extract renderFactionRep function body from ui-render.js const factionFnBody = extractFunction(uiRenderSrc, 'renderFactionRep'); assert(/class="sub-panel"[^>]*data-sub-id="factions_minor"/.test(factionFnBody) || /data-sub-id="factions_minor"[^>]*class="sub-panel"/.test(factionFnBody), 'renderFactionRep MINOR FACTIONS sub-panel has class="sub-panel" + data-sub-id'); ``` This encodes the UC-STR-2 fix as a ratchet guard. --- ### GATE-UI-6 · Faction `adjustFaction` call-sites use `5` not `50` as delta ```js // Both button click handlers should call adjustFaction('key','fame',5) not 50 const factionFnBody = extractFunction(uiRenderSrc, 'renderFactionRep'); assert(!/adjustFaction\([^)]+,\s*50\)/.test(factionFnBody), 'renderFactionRep faction buttons use ±5 delta, not ±50'); assert(!/±50/.test(factionFnBody), 'renderFactionRep helper text does not say ±50'); ``` This prevents the UC-CTL-2 bug from returning. --- ### GATE-UI-7 · `_updatePanelBadges()` includes Skill Books and Skill Magazines entries ```js // These were added in v2.5 — ensure they haven't been accidentally removed const badgesFnBody = extractFunction(uiCoreSrc, '_updatePanelBadges'); assert(badgesFnBody.includes('SKILL BOOKS'), '_updatePanelBadges has SKILL BOOKS entry'); assert(badgesFnBody.includes('SKILL MAGAZINES'), '_updatePanelBadges has SKILL MAGAZINES entry'); ``` --- ## SECTION 10 — Prioritized Build Backlog ### Tier 1 — High-leverage, non-breaking changes | ID | Finding | Severity | Fix | Files | |---|---|---|---|---| | UC-DENS-1 + UC-CTL-1 | Tracker rows: extract inline styles to `.tracker-row/.tracker-toggle`; `<span onclick>` → `<button>` | HIGH | New CSS classes; update 5 render functions | `terminal.css`, `ui-render.js` | | UC-STR-2 | Faction MINOR FACTIONS: `<details class="sub-panel" data-sub-id="factions_minor">` + persistence handler | MEDIUM | Render template + 10-line handler | `ui-render.js` | | UC-CTL-2 | Faction ±50 label bug → ±5 | MEDIUM | One string change | `ui-render.js` | | UC-ORDER-1 | Quest active-first sort | MEDIUM | 5-line sort before render | `ui-render.js` | ### Tier 2 — Structure and persistence | ID | Finding | Severity | Fix | Files | |---|---|---|---|---| | UC-STR-1 | `ammoSubPanel` add `data-sub-id="ammo_reserves"` | MEDIUM | 1-attribute change | `index.html` | | UC-BADGE-2 | Traits: update h3 via querySelector instead of header div | LOW | ~5 lines | `ui-render.js` | | UC-BADGE-1 | Skills books/magazines: `[n/total]` | LOW | 4 string changes | `ui-render.js` | | UC-CTL-3 | Lincoln select → `.tracker-select` CSS class | MEDIUM | 1 class + inline removal | `terminal.css`, `ui-render.js` | | UC-STR-3 | Audio Systems → `sub-panel` + `data-sub-id` | LOW | 2-line HTML change | `index.html` | | UC-STR-4 | Campaign Config sub-panels: `data-sub-id` on 3 elements | LOW | 3-attribute change | `index.html` | | UC-NAME-1 | Campaign Config summaries → `<h3>` headings | LOW | 6-line HTML change | `index.html` | ### Tier 3 — CSS and cosmetic refinements | ID | Finding | Severity | Fix | Files | |---|---|---|---|---| | UC-DENS-3 | Security panel: `.panel--alert` modifier class | LOW | New CSS class; remove 2 inline styles | `terminal.css`, `index.html` | | UC-ORDER-2 | Empty state ALL CAPS → sentence case | LOW | 4 string changes | `ui-render.js` | | UC-BADGE-4 | `renderCollectibles` empty state → `emptyState()` | LOW | 1-line change | `ui-render.js` | ### Spec-First backlog | ID | Finding | Note | |---|---|---| | UC-DENS-2 | Two density tiers (legacy 12px vs tracker 11px) | Formalise distinction in Protocol UI-2; no migration needed unless owner chooses Option A | | Gate guards | Suite 88 — UI Consistency (7 guards) | Add both runners same commit; Protocol 2a count sync | --- ## Constraint Checklist - **[GATE]** 1078 tests must pass · never `--no-verify` - **[CACHE]** Any change to `index.html`, `css/terminal.css`, or `js/ui-render.js` requires a `CACHE_NAME` bump per Protocol 1 - **[2A]** Every gate guard added increments the count in both runners + all 7 Protocol 2a sync locations - **[PARITY]** GATE-UI-1 through GATE-UI-7 must be added to BOTH runners in the same commit (Protocol 15) - **[A11Y]** UC-DENS-1 / UC-CTL-1 (`<span>` → `<button>`) must not regress contrast or focus states — verify `.tracker-toggle` inherits focus ring from the global `button:focus-visible` rule or adds its own (the current `button` global has `outline: none` — a dedicated `:focus-visible` rule is needed; cross-reference ACCESSIBILITY_AUDIT.md) - **[MOBILE]** `.tracker-toggle` must enforce `min-height: 28px; min-width: 28px` (Protocol 17); verify via Protocol 10 render check at 360/412px after UC-DENS-1 is applied - **[NEVER]** Never bump APP_VERSION for structure/CSS-only changes - **[BATCH]** UC-DENS-1 and UC-CTL-1 are the SAME code change — do not split them. UC-STR-1 through UC-STR-4 can batch together (all `index.html` HTML attribute changes, one cache bump). --- *End of UI_CONSISTENCY_AUDIT.md*
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).