[object Object]

← back to Dw Collections Viewer

viewer standard: add Grid/List toggle + per-field on/off menu to internal record-grid viewer

101776948f4920edf663da3325dd0cba4a5a5554 · 2026-08-12 08:53:16 -0700 · Steve

Files touched

Diff

commit 101776948f4920edf663da3325dd0cba4a5a5554
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 12 08:53:16 2026 -0700

    viewer standard: add Grid/List toggle + per-field on/off menu to internal record-grid viewer
---
 public/index.html | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 123 insertions(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index 436567b..8dbcf4b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -276,6 +276,38 @@ body.bulk .card.selected { border-color: var(--gold) }
 .pin-btn:hover { color: var(--ink); border-color: var(--gold) }
 
 @media (max-width: 1024px) { main { grid-template-columns: 1fr } aside { border-left: 0; border-top: 1px solid var(--rule) } }
+
+/* ── Grid / List toggle for main site grid ── */
+.vtoggle { display:flex; border:1px solid var(--rule); border-radius:3px; overflow:hidden; }
+.vtoggle button { background:var(--bg2); color:var(--muted); border:none; padding:5px 12px; font:10px/1 inherit; letter-spacing:.12em; text-transform:uppercase; cursor:pointer; font-weight:600; }
+.vtoggle button.on { background:var(--gold); color:var(--bg); }
+
+/* ── List view ── */
+.grid.list { display:block; }
+.grid.list .card { display:flex; flex-direction:row; align-items:center; margin-bottom:6px; gap:0; }
+.grid.list .card .hero { flex:0 0 90px; aspect-ratio:16/9; height:56px; }
+.grid.list .card .info { flex:1; display:flex; flex-direction:row; align-items:center; gap:16px; padding:8px 12px; border-top:none; border-left:1px solid var(--rule); }
+.grid.list .card .name { font-size:12px; flex:0 0 160px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+.grid.list .card .stats { flex:1; }
+
+/* ── Sort select for site grid ── */
+.site-sort-ctl { display:inline-flex; align-items:center; gap:6px; font-size:10px; color:var(--muted); letter-spacing:.12em; text-transform:uppercase; font-weight:600; }
+.site-sort-ctl select { background:var(--bg2); color:var(--ink); border:1px solid var(--rule); padding:4px 8px; font-size:10px; font-family:inherit; cursor:pointer; }
+
+/* ── Fields menu ── */
+.fieldmenu { position:relative; }
+.fbtn { background:var(--bg2); color:var(--muted); border:1px solid var(--rule); padding:5px 12px; font:10px/1 inherit; letter-spacing:.12em; text-transform:uppercase; cursor:pointer; font-weight:600; }
+.fbtn:hover { border-color:var(--gold); color:var(--ink); }
+.fpop { position:absolute; top:calc(100% + 6px); right:0; background:var(--bg2); border:1px solid var(--rule); box-shadow:0 10px 26px #0008; padding:8px 4px; z-index:30; min-width:186px; }
+.fpop[hidden] { display:none; }
+.fpop label { display:flex; align-items:center; gap:10px; padding:6px 14px; font-size:11px; color:var(--muted); cursor:pointer; white-space:nowrap; letter-spacing:.04em; }
+.fpop label:hover { color:var(--ink); }
+.fpop input { accent-color:var(--gold); }
+
+/* ── Field-hide rules for site cards (grid only) ── */
+.grid:not(.list).hf-hero   .card .hero   { display:none; }
+.grid:not(.list).hf-name   .card .name   { display:none; }
+.grid:not(.list).hf-stats  .card .stats  { display:none; }
 </style>
 </head>
 <body>
@@ -291,6 +323,31 @@ body.bulk .card.selected { border-color: var(--gold) }
   <div class="grid-wrap">
     <div class="toolbar">
       <input class="search-q" id="siteFilter" type="text" placeholder="filter sites by slug or domain…" oninput="filterSites(this.value)">
+      <div class="site-sort-ctl">
+        Sort
+        <select id="siteSort" onchange="onSiteSortChange()">
+          <option value="slug">Slug A→Z</option>
+          <option value="slug_desc">Slug Z→A</option>
+          <option value="products">Products: Most</option>
+          <option value="products_asc">Products: Fewest</option>
+          <option value="collections">Collections: Most</option>
+          <option value="domain">Domain A→Z</option>
+          <option value="stale">Stale first</option>
+          <option value="zero">Zero products first</option>
+        </select>
+      </div>
+      <div class="vtoggle">
+        <button id="vGrid" class="on" onclick="setDWCView('grid')">▦ Grid</button>
+        <button id="vList" onclick="setDWCView('list')">☰ List</button>
+      </div>
+      <div class="fieldmenu">
+        <button class="fbtn" onclick="toggleDWCFieldsMenu(event)">Fields ▾</button>
+        <div class="fpop" id="fpop" hidden>
+          <label><input type="checkbox" data-dwf="hero" checked> Hero image</label>
+          <label><input type="checkbox" data-dwf="name" checked> Slug / name</label>
+          <label><input type="checkbox" data-dwf="stats" checked> Stats / badges</label>
+        </div>
+      </div>
       <button class="mode-toggle" id="bulkToggle" onclick="toggleBulk()">☐ Bulk select</button>
       <button class="mode-toggle" onclick="autoSeed(true)" title="Preview without saving">★ Auto-seed (preview)</button>
       <button class="mode-toggle" onclick="autoSeed(false)" title="Assign top-suggested collection to every site that has zero">★ Auto-seed (apply)</button>
@@ -319,6 +376,7 @@ body.bulk .card.selected { border-color: var(--gold) }
 <script>
 let SITES = {}, COLLS = [], CURRENT = null, COLL_FILTER = '';
 let SITE_FILTER = '', BULK_MODE = false, BULK_SELECTED = new Set();
+let SITE_SORT = localStorage.getItem('dwc_site_sort') || 'slug';
 let SUGGESTIONS = {};  // slug → array of suggested collections
 // Per-site products grid state (right pane). PRODUCTS holds the most-recently
 // fetched, newest-first product list for CURRENT. PRODUCT_SELECTED is a Set of
@@ -349,8 +407,24 @@ async function loadSites() {
   renderGrid();
 }
 
+function sortSlugs(slugs) {
+  const s = SITE_SORT;
+  return [...slugs].sort((a, b) => {
+    const sa = SITES[a], sb = SITES[b];
+    if (s === 'slug') return a.localeCompare(b);
+    if (s === 'slug_desc') return b.localeCompare(a);
+    if (s === 'products') return (sb.product_count || 0) - (sa.product_count || 0);
+    if (s === 'products_asc') return (sa.product_count || 0) - (sb.product_count || 0);
+    if (s === 'collections') return ((sb.collections || []).length) - ((sa.collections || []).length);
+    if (s === 'domain') return (sa.domain || '').localeCompare(sb.domain || '');
+    if (s === 'stale') return (sb.is_stale ? 1 : 0) - (sa.is_stale ? 1 : 0) || a.localeCompare(b);
+    if (s === 'zero') return ((sa.product_count === 0 ? 0 : 1) - (sb.product_count === 0 ? 0 : 1)) || a.localeCompare(b);
+    return a.localeCompare(b);
+  });
+}
+
 function renderGrid() {
-  const slugs = Object.keys(SITES).sort();
+  const slugs = sortSlugs(Object.keys(SITES));
   const f = SITE_FILTER.toLowerCase();
   const visible = slugs.filter(s => !f || s.toLowerCase().includes(f) || (SITES[s].domain || '').toLowerCase().includes(f));
   document.getElementById('grid').innerHTML = visible.map(slug => {
@@ -386,6 +460,54 @@ function renderGrid() {
 
 function filterSites(v) { SITE_FILTER = v; renderGrid(); }
 
+// ── Site grid: sort ──
+function onSiteSortChange() {
+  SITE_SORT = document.getElementById('siteSort').value;
+  localStorage.setItem('dwc_site_sort', SITE_SORT);
+  renderGrid();
+}
+(function initSiteSort() {
+  const el = document.getElementById('siteSort');
+  if (el) el.value = SITE_SORT;
+})();
+
+// ── Grid / List view toggle ──
+const DWC_FIELD_DEFAULTS = { hero: true, name: true, stats: true };
+function setDWCView(v) {
+  const g = document.getElementById('grid');
+  g.classList.toggle('list', v === 'list');
+  document.getElementById('vGrid').classList.toggle('on', v !== 'list');
+  document.getElementById('vList').classList.toggle('on', v === 'list');
+  localStorage.setItem('dwc_site_view', v);
+}
+(function initDWCView() { setDWCView(localStorage.getItem('dwc_site_view') || 'grid'); })();
+
+// ── Fields menu ──
+function getDWCFields() {
+  try { return Object.assign({}, DWC_FIELD_DEFAULTS, JSON.parse(localStorage.getItem('dwc_site_fields') || '{}')); }
+  catch(e) { return Object.assign({}, DWC_FIELD_DEFAULTS); }
+}
+function applyDWCFields(prefs) {
+  const g = document.getElementById('grid');
+  for (const [k, on] of Object.entries(prefs)) g.classList.toggle('hf-' + k, !on);
+  document.querySelectorAll('#fpop [data-dwf]').forEach(cb => { cb.checked = !!prefs[cb.dataset.dwf]; });
+}
+function toggleDWCFieldsMenu(e) {
+  e.stopPropagation();
+  const p = document.getElementById('fpop');
+  p.hidden = !p.hidden;
+}
+document.addEventListener('click', () => { const p = document.getElementById('fpop'); if (p) p.hidden = true; });
+document.querySelectorAll('#fpop [data-dwf]').forEach(cb => {
+  cb.addEventListener('change', () => {
+    const prefs = getDWCFields();
+    prefs[cb.dataset.dwf] = cb.checked;
+    localStorage.setItem('dwc_site_fields', JSON.stringify(prefs));
+    applyDWCFields(prefs);
+  });
+});
+applyDWCFields(getDWCFields());
+
 function cardClick(e, slug) {
   if (BULK_MODE) {
     if (BULK_SELECTED.has(slug)) BULK_SELECTED.delete(slug); else BULK_SELECTED.add(slug);

← 48ca48c chore: v0.2.0 — similar-pattern search feature (session clos  ·  back to Dw Collections Viewer  ·  nav-agent: universal grid-controls drop-in on internal viewe a5d43e1 →