← 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
M public/index.htmlM server.js
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 →