[object Object]

← back to Dw Activation Calendar

calendar day columns expand to show all SKUs; every chip clickable (detail modal + Shopify admin/storefront links); fix latent chip-row overlap (aspect-ratio grid rows collapsed)

53bb7a5e560c2bcf52ee9b1fe462d5c9275a0cb4 · 2026-07-15 10:58:30 -0700 · Steve Abrams

Files touched

Diff

commit 53bb7a5e560c2bcf52ee9b1fe462d5c9275a0cb4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 15 10:58:30 2026 -0700

    calendar day columns expand to show all SKUs; every chip clickable (detail modal + Shopify admin/storefront links); fix latent chip-row overlap (aspect-ratio grid rows collapsed)
---
 public/index.html | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
 server.js         |  3 +-
 2 files changed, 84 insertions(+), 4 deletions(-)

diff --git a/public/index.html b/public/index.html
index 6b50f82..68828e0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -121,9 +121,19 @@
   .legend .sw{width:10px;height:10px;border-radius:3px;display:inline-block}
   .loading{padding:60px;text-align:center;color:var(--muted)}
 
-  /* SKU chips (image tiles) */
-  .scell .chips{display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));gap:5px;overflow-y:auto;align-content:start}
-  .chip{position:relative;border-radius:6px;overflow:hidden;background:var(--ground);border:1px solid var(--hairline);aspect-ratio:1/1}
+  /* SKU chips (image tiles). Flex-wrap with FIXED squares — aspect-ratio chips
+     inside an auto-row grid collapse the rows (derived height never reaches
+     track sizing) and every row overlaps the next. */
+  .scell .chips{display:flex;flex-wrap:wrap;gap:5px;align-content:flex-start;
+      max-height:178px;overflow:hidden}          /* collapsed preview: ~3 chip rows */
+  .cell.expanded .chips{max-height:none;overflow:visible}  /* expanded: the whole day column */
+  .chip{position:relative;border-radius:6px;overflow:hidden;background:var(--ground);border:1px solid var(--hairline);width:56px;height:56px;flex:0 0 auto;padding:0;cursor:pointer}
+  .chip:hover{border-color:var(--brass);box-shadow:0 0 0 1px var(--brass)}
+  .chip:focus-visible{outline:2px solid var(--plum);outline-offset:1px}
+  /* expand / collapse the day column */
+  .scell .more{margin-top:4px;align-self:stretch;font-size:10px;font-weight:600;letter-spacing:.4px;color:var(--muted);
+      background:var(--ground);border:1px dashed var(--hairline);border-radius:6px;padding:3px 6px}
+  .scell .more:hover{color:var(--ink);border-color:var(--faint)}
   .chip img{width:100%;height:100%;object-fit:cover;display:block}
   .chip .ph{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;color:#fff}
   .chip .vn{position:absolute;left:0;right:0;bottom:0;font-size:7px;line-height:1.15;background:rgba(31,27,23,.72);color:#f3ead7;padding:2px 3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:.2px}
@@ -183,6 +193,23 @@
   .mform .btn-save:hover{background:#4d2230}
   .mform .msg{font-size:12.5px;min-height:16px;margin-top:10px}
   .mform .msg.err{color:var(--red)}.mform .msg.ok{color:var(--green)}
+
+  /* ---- SKU detail modal ---- */
+  .skud .hero{width:100%;aspect-ratio:4/3;border-radius:12px;overflow:hidden;background:var(--ground);border:1px solid var(--hairline);margin-bottom:16px}
+  .skud .hero img{width:100%;height:100%;object-fit:cover;display:block}
+  .skud .hero .ph{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-family:var(--serif);font-size:34px;color:#fff}
+  .skud dl{display:grid;grid-template-columns:118px 1fr;gap:7px 14px;margin:0;font-size:13.5px}
+  .skud dt{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.6px;font-weight:600;align-self:center}
+  .skud dd{margin:0;color:var(--ink)}
+  .skud dd code{font-size:12.5px;background:var(--ground);border:1px solid var(--hairline);border-radius:6px;padding:2px 7px}
+  .skud .fxpill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:600;border-radius:999px;padding:3px 11px}
+  .skud .fxpill .d{width:7px;height:7px;border-radius:50%}
+  .skud .fxpill.s{background:var(--chip-green-bg);color:var(--chip-green-fg)}.skud .fxpill.s .d{background:var(--green)}
+  .skud .fxpill.r{background:var(--chip-brass-bg);color:var(--chip-brass-fg)}.skud .fxpill.r .d{background:var(--brass)}
+  .skud .links{display:flex;gap:10px;margin-top:20px;padding-top:18px;border-top:1px solid var(--hairline);flex-wrap:wrap}
+  .skud .links a{flex:1;min-width:150px;text-align:center;text-decoration:none;font-weight:600;font-size:13.5px;padding:10px 16px;border-radius:10px}
+  .skud .links a.adm{background:var(--plum);color:#fff}.skud .links a.adm:hover{background:#4d2230}
+  .skud .links a.sf{background:var(--surface);border:1px solid var(--hairline);color:var(--ink)}.skud .links a.sf:hover{border-color:var(--faint)}
 </style>
 </head>
 <body>
@@ -508,6 +535,38 @@ function slotLabel(h){ if(h==null) return ''; const ap=h<12?'AM':'PM'; let hh=h%
 function fmtGoLive(iso){ if(!iso) return '—'; try{ return new Date(iso).toLocaleString(undefined,{weekday:'short',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }catch(e){ return iso; } }
 function skuLabel(s){ return s.dw_sku || s.mfr_sku || (s.shopify_id?('#'+String(s.shopify_id).split('/').pop()):'(no sku)'); }
 
+/* ---- SKU detail modal (chip click) ---- */
+function openSkuModal(s){
+  const idNum = s.shopify_id ? String(s.shopify_id).split('/').pop() : null;
+  const adminUrl = idNum ? 'https://admin.shopify.com/store/designer-laboratory-sandbox/products/'+idNum : null;
+  const siteUrl  = s.handle ? 'https://www.designerwallcoverings.com/products/'+encodeURIComponent(s.handle) : null;
+  const fx = s.fix_type==='add-sample' ? 's' : 'r';
+  const hero = (s.image_url && /^https?:/.test(s.image_url))
+    ? `<img src="${esc(s.image_url)}" alt="${esc(s.title||skuLabel(s))}">`
+    : `<div class="ph" style="background:${vcolor(s.vendor)}">${esc(s.vendor.slice(0,3).toUpperCase())}</div>`;
+  const bodyHtml=`<div class="mform skud">
+    <div class="mhead">
+      <div class="mtitle">${esc(s.title || skuLabel(s))}</div>
+      <button type="button" class="mclose" id="sk_x" aria-label="Close">✕</button>
+    </div>
+    <div class="hero">${hero}</div>
+    <dl>
+      <dt>Vendor</dt><dd>${esc(s.vendor)}</dd>
+      ${s.dw_sku?`<dt>DW SKU</dt><dd><code>${esc(s.dw_sku)}</code></dd>`:''}
+      ${s.mfr_sku?`<dt>Mfr SKU</dt><dd><code>${esc(s.mfr_sku)}</code></dd>`:''}
+      <dt>Build step</dt><dd><span class="fxpill ${fx}"><span class="d"></span>${esc(s.fix_type)}</span></dd>
+      <dt>Goes active</dt><dd title="${esc(s.go_live_at||'')}">⏱ ${fmtGoLive(s.go_live_at)}${s.slot_hour!=null?' · '+slotLabel(s.slot_hour)+' slot':''}</dd>
+      ${idNum?`<dt>Shopify ID</dt><dd><code>${esc(idNum)}</code></dd>`:''}
+    </dl>
+    <div class="links">
+      ${adminUrl?`<a class="adm" href="${adminUrl}" target="_blank" rel="noopener noreferrer">Open in Shopify Admin</a>`:''}
+      ${siteUrl?`<a class="sf" href="${siteUrl}" target="_blank" rel="noopener noreferrer" title="staged items 404 on the storefront until they go active">View on site</a>`:''}
+    </div>
+  </div>`;
+  const M=ModalRig.open({key:'dwcal-sku',title:'SKU',bodyHtml,width:440,height:620,bg:'var(--surface)'});
+  M.body.querySelector('#sk_x').onclick=M.close;
+}
+
 /* Generic SKU-schedule month grid — shared by the SKU Activation tab AND the
    Google Calendar tab's not-connected fallback (renders into any container). */
 function renderSchedMonth(containerId, monthDate, data){
@@ -522,6 +581,7 @@ function renderSchedMonth(containerId, monthDate, data){
   const first=new Date(y,m,1), startDow=first.getDay(), days=new Date(y,m+1,0).getDate();
   const prevDays=new Date(y,m,0).getDate();
   const todayISO=new Date().toISOString().slice(0,10);
+  const expanders=[];
   for(let i=startDow-1;i>=0;i--){ grid.appendChild(outCell(prevDays-i)); }
   for(let dn=1;dn<=days;dn++){
     const iso=`${y}-${String(m+1).padStart(2,'0')}-${String(dn).padStart(2,'0')}`;
@@ -532,7 +592,11 @@ function renderSchedMonth(containerId, monthDate, data){
     if(day){
       const chips=document.createElement('div'); chips.className='chips';
       day.skus.forEach(s=>{
+        // div, not <button>: a button grid-item's aspect-ratio height doesn't
+        // feed the auto row track, so rows collapse and chips overlap.
         const el=document.createElement('div'); el.className='chip';
+        el.setAttribute('role','button'); el.tabIndex=0;
+        el.onkeydown=(e)=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();openSkuModal(s);}};
         el.title=`${skuLabel(s)} · ${s.vendor}${s.title?' · '+s.title:''} · ${s.fix_type}\n⏱ goes active ${fmtGoLive(s.go_live_at)}`;
         const fx=s.fix_type==='add-sample'?'s':'r';
         if(s.image_url && /^https?:/.test(s.image_url)){
@@ -540,14 +604,29 @@ function renderSchedMonth(containerId, monthDate, data){
         }else{
           el.innerHTML=`<span class="fx ${fx}"></span><div class="ph" style="background:${vcolor(s.vendor)}">${s.vendor.slice(0,3).toUpperCase()}</div><span class="vn">${s.vendor}</span>`;
         }
+        el.onclick=(e)=>{e.stopPropagation();openSkuModal(s);};
         chips.appendChild(el);
       });
       cell.appendChild(chips);
+      // expand / collapse the whole day column (hidden later if everything fits)
+      const more=document.createElement('button'); more.type='button'; more.className='more';
+      const closedLabel='▾ show all '+day.count;
+      more.textContent=closedLabel;
+      more.onclick=()=>{
+        const open=cell.classList.toggle('expanded');
+        more.textContent=open?'▴ collapse':closedLabel;
+      };
+      cell.appendChild(more);
+      expanders.push({chips,more});
     }
     grid.appendChild(cell);
   }
   const totalCells=startDow+days; const trail=(7-(totalCells%7))%7;
   for(let dn=1;dn<=trail;dn++){ grid.appendChild(outCell(dn)); }
+  // after layout, drop the expander on days whose chips already fit the preview
+  requestAnimationFrame(()=>expanders.forEach(({chips,more})=>{
+    if(chips.scrollHeight<=chips.clientHeight+2) more.remove();
+  }));
 }
 function renderMonth(){
   document.getElementById('mlabel').textContent=fmtMonth(viewMonth);
diff --git a/server.js b/server.js
index c9ef74d..dfca6bc 100644
--- a/server.js
+++ b/server.js
@@ -82,6 +82,7 @@ async function scheduleFromFile() {
       vendor: s.vendor, fix_type: s.fix_type,
       slot_hour: s.slot_hour, go_live_at: s.go_live_at,
       image_url: sp.image_url || null, title: sp.title || null,
+      shopify_id: s.shopify_id || null, handle: sp.handle || null,
     });
   }
   const days = [...byDate.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1)
@@ -110,7 +111,7 @@ async function loadStaged() {
   const c = await pool.connect();
   try {
     const r = await c.query(`
-      select w.dw_sku, w.vendor, w.fix_type, w.order_rank,
+      select w.dw_sku, w.vendor, w.fix_type, w.order_rank, w.shopify_id,
              sp.image_url, sp.handle, sp.title
         from bulk_fivefield_worklist w
         left join shopify_products sp on sp.shopify_id = w.shopify_id

← 6bb8427 auto-save: 2026-07-15T00:33:42 (1 files) — data/activation-s  ·  back to Dw Activation Calendar  ·  Calendar chips are real hrefs to the staged Shopify product 09e4fb6 →