← back to Ticket Action Viewer
Refresh nav-agent to v1.2 (adds kv-list compact List view, default-off)
b5c4aba34315b5b722e49e764d3607474c2cd078 · 2026-08-19 14:05:24 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/nav-agent/nav-agent.cssM public/nav-agent/nav-agent.js
Diff
commit b5c4aba34315b5b722e49e764d3607474c2cd078
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 19 14:05:24 2026 -0700
Refresh nav-agent to v1.2 (adds kv-list compact List view, default-off)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/nav-agent/nav-agent.css | 22 ++++++++++++-
public/nav-agent/nav-agent.js | 75 +++++++++++++++++++++++++++++++++++++++---
2 files changed, 92 insertions(+), 5 deletions(-)
diff --git a/public/nav-agent/nav-agent.css b/public/nav-agent/nav-agent.css
index 844d787..466a1ab 100644
--- a/public/nav-agent/nav-agent.css
+++ b/public/nav-agent/nav-agent.css
@@ -97,4 +97,24 @@ table .na-group-h td{padding:8px 6px;color:var(--na-acc);font-weight:600;border-
.na-merge .na-row label{color:var(--na-mut);min-width:48px}
.na-merge select{background:var(--na-bg2);border:1px solid var(--na-line);color:var(--na-fg);border-radius:6px;padding:6px 8px;font-size:13px;flex:1}
-@media (max-width:640px){.na-panel{width:86vw}}
+/* ---- Grid / List view toggle ----------------------------------------- */
+.na-viewseg{display:inline-flex;margin:4px 0 10px;border:1px solid var(--na-line);border-radius:6px;overflow:hidden}
+.na-viewseg button{background:var(--na-bg2);color:var(--na-mut);border:0;border-right:1px solid var(--na-line);
+ padding:6px 12px;font-size:12px;cursor:pointer}
+.na-viewseg button:last-child{border-right:0}
+.na-viewseg button.on{background:var(--na-acc);color:#fff}
+
+/* ---- compact LIST view (the kv-list "third view") --------------------- */
+.na-list{width:100%}
+.na-lrows{display:flex;flex-direction:column}
+.na-lrow{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;padding:10px 14px;
+ border-bottom:1px solid var(--na-line)}
+.na-lrow:hover{background:rgba(255,255,255,.04)}
+.na-lmain{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;min-width:210px;flex:0 0 auto}
+.na-la{font-weight:600;color:var(--na-fg)}
+.na-lmeta{display:flex;flex-wrap:wrap;gap:6px 16px;flex:1 1 320px;min-width:0}
+.na-lm{font-size:12.5px;color:var(--na-fg);white-space:nowrap}
+.na-lmk{color:var(--na-mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;margin-right:5px}
+.na-lempty{padding:40px;text-align:center;color:var(--na-mut)}
+
+@media (max-width:640px){.na-panel{width:86vw}.na-lmain{min-width:100%}}
diff --git a/public/nav-agent/nav-agent.js b/public/nav-agent/nav-agent.js
index 991a82f..4c16ce8 100644
--- a/public/nav-agent/nav-agent.js
+++ b/public/nav-agent/nav-agent.js
@@ -1,11 +1,14 @@
/* ============================================================================
- * Nav Agent — universal grid-controls drop-in (v1.0)
+ * Nav Agent — universal grid-controls drop-in (v1.2)
*
* Drop one <script src=".../nav-agent.js" defer></script> (+ the CSS) onto any
* page. It auto-detects the primary data grid (a <table> or a .grid/.card list),
* then injects:
* • a LEFT collapsible panel: text search, per-FIELD show/hide toggles,
* sort-by-any-field (asc/desc), group-by-any-field, density slider
+ * • a Grid / List view toggle — List is the compact "kv-list" third view:
+ * one scannable row per record, first visible field as the identity + the
+ * rest as inline LABEL·value chips (default OFF; the native grid is default)
* • movable / resizable / collapsible / removable / maximizable panel chrome
* on the grid itself
* All state persists to localStorage, namespaced per page path, so every
@@ -170,7 +173,7 @@
});
}
- function applyAll() { fieldStyleTag(); applySort(); applySearch(); }
+ function applyAll() { fieldStyleTag(); applySort(); applySearch(); if (self.renderList && st.view === 'list') self.renderList(); }
/* re-apply after host re-renders (debounced, ignores our own mutations).
If the FIELD SET changed (async data arrived, filter repopulated), tell
@@ -199,6 +202,15 @@
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }
+ /* -- Grid / List view toggle (shared by standalone + merged panels) ---- */
+ function viewToggle(agent) {
+ var st = agent.state, wrap = el('div', 'na-viewseg');
+ wrap.innerHTML = '<button data-v="grid">▦ Grid</button><button data-v="list">☰ List</button>';
+ function mark() { [].slice.call(wrap.children).forEach(function (b) { b.classList.toggle('on', (st.view || 'grid') === b.getAttribute('data-v')); }); }
+ [].slice.call(wrap.children).forEach(function (b) { b.onclick = function () { agent.setView(b.getAttribute('data-v')); mark(); }; });
+ mark(); return wrap;
+ }
+
/* -- panel UI ---------------------------------------------------------- */
function buildPanel(agent, opt) {
var M = agent.model, st = agent.state;
@@ -210,6 +222,9 @@
'<button class="na-x" title="Close">✕</button>'));
var body = el('div', 'na-body'); panel.appendChild(body);
+ // Grid / List view toggle (only meaningful when we have fields to lay out)
+ if (M.fields && M.fields.length) body.appendChild(viewToggle(agent));
+
// search
var search = el('input', 'na-search'); search.type = 'search'; search.placeholder = 'Search all fields…';
search.value = st.q || '';
@@ -361,7 +376,10 @@
if (g.float) { wrap.classList.add('na-float'); ['left', 'top', 'width', 'height'].forEach(function (k) { if (g[k]) wrap.style[k] = g[k]; }); }
if (g.max) wrap.classList.add('na-max');
if (g.collapsed) wrap.classList.add('na-collapsed');
- if (g.removed) { wrap.classList.add('na-removed'); addRestore(); }
+ // ROOT FIX (Steve-approved 2026-08-17): a "removed" grid must NEVER stay hidden across reloads —
+ // that trap has no recovery and blanked whole pages (CRCP mls.html). Do NOT restore `removed` on
+ // load: the grid always reappears; in-session Remove still works, it just no longer persist-hides.
+ // (Float / max / collapsed still persist — those are safe, recoverable layout choices.)
}
/* -- merged mode: fold into an existing house panel ("2 into 1") ------- */
@@ -400,6 +418,7 @@
gsel.value = st.group || ''; gsel.onchange = function () { st.group = gsel.value; agent.save(); agent.applyGroup(); };
grow.appendChild(gsel); box.appendChild(grow);
}
+ if (M.fields && M.fields.length) box.appendChild(viewToggle(agent));
box.appendChild(el('div', 'na-note', 'Drag the grid’s ▤ bar to float · ▁ collapse · ⤢ maximize · ✕ remove.'));
house.appendChild(box);
return box;
@@ -419,6 +438,42 @@
else target.setAttribute('data-na-sel', target.id ? '#' + target.id : '.' + (target.className.split(/\s+/)[0] || 'grid'));
var agent = Agent(target, opt);
target.__naAgent = agent;
+
+ // ---- compact LIST view (the kv-list "third view", v1.2) -----------
+ // Renders each live grid row as one scannable line: first visible field
+ // as the identity, the rest as inline LABEL·value chips. DOM-based (reads
+ // M.cellText), so sort/search/field-toggle all flow through for free.
+ // Default OFF — the page's native grid stays the default view.
+ function ensureListEl() {
+ if (agent._listEl) return agent._listEl;
+ var lel = el('div', 'na-list');
+ (target.parentNode || target).insertBefore(lel, target.nextSibling);
+ agent._listEl = lel; return lel;
+ }
+ agent.renderList = function () {
+ var lel = agent._listEl; if (!lel) return;
+ var M2 = agent.model, hidden = agent.state.hidden || [];
+ var vis = (M2.fields || []).filter(function (f) { return hidden.indexOf(f.key) < 0; });
+ if (!vis.length) vis = (M2.fields || []).slice();
+ if (!vis.length) { lel.innerHTML = '<div class="na-lempty">No structured fields detected on this grid.</div>'; return; }
+ var titleF = vis[0], chipF = vis.slice(1);
+ var rows = M2.rows().filter(function (r) { return r.style.display !== 'none'; });
+ if (!rows.length) { lel.innerHTML = '<div class="na-lempty">No matching rows.</div>'; return; }
+ lel.innerHTML = '<div class="na-lrows">' + rows.map(function (r) {
+ var tt = esc(M2.cellText(r, titleF) || '—');
+ var chips = chipF.map(function (f) {
+ var v = M2.cellText(r, f); if (v === '' || v == null) return '';
+ return '<span class="na-lm"><span class="na-lmk">' + esc(f.label) + '</span>' + esc(v) + '</span>';
+ }).join('');
+ return '<div class="na-lrow"><div class="na-lmain"><span class="na-la">' + tt + '</span></div>' +
+ '<div class="na-lmeta">' + chips + '</div></div>';
+ }).join('') + '</div>';
+ };
+ agent.setView = function (v) {
+ agent.state.view = (v === 'list') ? 'list' : 'grid'; agent.save();
+ if (agent.state.view === 'list') { ensureListEl(); target.style.display = 'none'; agent._listEl.style.display = ''; agent.renderList(); }
+ else { if (agent._listEl) agent._listEl.style.display = 'none'; target.style.display = ''; }
+ };
// "2 into 1": if the page already has a house control panel, fold into it
var house = opt.merge === false ? null : findHousePanel(target);
// (re)build the controls — called again by the observer when fields change
@@ -434,6 +489,8 @@
makePanel();
chrome(agent, opt);
agent.applyAll();
+ // restore a persisted List-view choice (default is Grid — nothing changes)
+ if (agent.state.view === 'list') agent.setView('list');
// standalone launcher only when NOT merged into a house panel
if (!house && !document.querySelector('.na-launch')) {
@@ -468,7 +525,17 @@
populated = !!t.querySelector('.card') || kids.length >= 2;
}
}
- if (populated || tries++ > 32) { API.mount(); return; } // mount anyway after ~8s; observer fills fields when data arrives
+ if (populated || tries++ > 32) {
+ var _agent = API.mount(); // mount anyway after ~8s; observer fills fields when data arrives
+ // diagnosability (DTD C, 2026-08-13): a null mount is otherwise SILENT — md5/marker/200
+ // all pass while no panel/launcher appears. Emit ONE passive console.info (never warn/error,
+ // never throw, never alter flow) so a future silent-null-mount is greppable, not invisible.
+ // Only nudge when there is GENUINELY no target. mount() also returns null when it
+ // THREW mid-mount on a real grid (its own console.warn already fired) — re-check
+ // findTarget() so we never emit a false "no grid" on a page that actually has one.
+ if (!_agent) { try { if (findTarget() === null && window.console && console.info) console.info('[nav-agent] no grid target found on this marker-bearing page — add data-nav-grid to the real grid container, or data-nav-agent="off" to opt out (see _shared/nav-agent/README.md).'); } catch (e) {} }
+ return;
+ }
setTimeout(attempt, 250);
})();
} catch (e) {}
← 917906c Move default PORT 9971->10075 (9971 collided with gmc-viewer
·
back to Ticket Action Viewer
·
nav-agent: List data full-width tight columns (grid) — layou 441bd72 →