[object Object]

← back to Astek Import Viewer

Refresh nav-agent to v1.2 (adds kv-list compact List view, default-off)

ff27346b91641130442ec1a42a01f8ed9ccd5ce8 · 2026-08-19 14:05:20 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit ff27346b91641130442ec1a42a01f8ed9ccd5ce8
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 19 14:05:20 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 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[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) {}

← 4d4810a nav-agent: v1.1 merge-mode asset refresh  ·  back to Astek Import Viewer  ·  nav-agent: List data full-width tight columns (grid) — layou 3316e05 →