[object Object]

← back to Interiordesignershowroom

moodboard drawer: in-drawer project switcher + collapsible Project/Pieces/Create tabs (collapsed on load, state persisted)

81400f7e17bfb45c0fc38a54963741a2384b4305 · 2026-08-03 08:33:35 -0700 · Steve

Files touched

Diff

commit 81400f7e17bfb45c0fc38a54963741a2384b4305
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Aug 3 08:33:35 2026 -0700

    moodboard drawer: in-drawer project switcher + collapsible Project/Pieces/Create tabs (collapsed on load, state persisted)
---
 public/js/moodboard.js | 92 ++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 78 insertions(+), 14 deletions(-)

diff --git a/public/js/moodboard.js b/public/js/moodboard.js
index d144791..a41da81 100644
--- a/public/js/moodboard.js
+++ b/public/js/moodboard.js
@@ -67,6 +67,10 @@
     projectSel.innerHTML = opts + '<option value="__new__">+ New project…</option>';
   }
 
+  // drawer tab open/closed state (persisted; tabs start COLLAPSED on first load)
+  function tabState() { try { return JSON.parse(localStorage.getItem('ids_mb_tabs')) || {}; } catch (e) { return {}; } }
+  function setTab(id, open) { var t = tabState(); t[id] = open; localStorage.setItem('ids_mb_tabs', JSON.stringify(t)); }
+
   // load()/save() always operate on the ACTIVE project's item list
   function load() { var p = readProjects(); return p[getActive()] || []; }
   function save(b) { var p = readProjects(); p[getActive()] = b; writeProjects(p); sync(); }
@@ -139,6 +143,26 @@
     + '.mb-err{color:#8b3a3a;font-size:.8rem;padding:14px 2px;line-height:1.5}'
     + '.mb-back{background:none;border:0;color:#7a6038;font-size:.72rem;text-transform:uppercase;letter-spacing:.05em;cursor:pointer;padding:0;margin:4px 2px 10px}'
     + '.mb-back:hover{text-decoration:underline}'
+    // collapsible drawer tabs (Project / Pieces / Create) — start collapsed, mirror the left-rail pattern
+    + '.mb-tab{border-bottom:1px solid var(--line,#e8e2d9)}'
+    + '.mb-tab>summary{cursor:pointer;list-style:none;user-select:none;display:flex;justify-content:space-between;align-items:center;'
+    + 'padding:12px 4px;font:600 .68rem/1.2 var(--sans,sans-serif);letter-spacing:.1em;text-transform:uppercase;color:#7a7168}'
+    + '.mb-tab>summary::-webkit-details-marker{display:none}'
+    + '.mb-tab>summary::after{content:"+";font-size:1rem;font-weight:300;color:#b0a898}'
+    + '.mb-tab[open]>summary{color:#16130f}'
+    + '.mb-tab[open]>summary::after{content:"\\2013"}'
+    + '.mb-tabbody{padding:2px 2px 16px}'
+    + '.mb-tabbody.mb-foot{border-top:0;padding:2px 2px 16px}'
+    + '.mb-projects{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}'
+    + '.mb-proj{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%;text-align:left;'
+    + 'border:1px solid var(--line,#e8e2d9);background:#fff;border-radius:2px;padding:8px 10px;cursor:pointer;'
+    + 'font:.8rem var(--sans,sans-serif);color:#3a3530;transition:border-color .15s,background .15s}'
+    + '.mb-proj:hover{border-color:#9b7b50}'
+    + '.mb-proj.is-active{background:#16130f;color:#faf8f5;border-color:#16130f}'
+    + '.mb-proj-c{font-size:.7rem;opacity:.7;font-variant-numeric:tabular-nums}'
+    + '.mb-newproj{width:100%;border:1px dashed var(--line,#c9bfae);background:none;border-radius:2px;padding:8px;cursor:pointer;'
+    + 'font:.72rem var(--sans,sans-serif);letter-spacing:.05em;text-transform:uppercase;color:#7a6038}'
+    + '.mb-newproj:hover{border-color:#9b7b50;color:#9b7b50}'
     + '.mbdrawer[hidden],.mb-backdrop[hidden]{display:none}'
     + '@media(max-width:520px){.mbfab{left:14px;bottom:14px}.mb-grid{grid-template-columns:1fr 1fr}}';
   var st = document.createElement('style'); st.textContent = css; document.head.appendChild(st);
@@ -164,22 +188,56 @@
     if (head) head.textContent = getActive();
     if (lastRender) return renderResult(scroll);
     var b = load();
-    if (!b.length) { scroll.innerHTML = '<p class="mb-empty">Your mood board is empty.<br>While you shop, tap <strong>◇ Mood Board</strong> on any piece to collect it here, then turn the whole board into a room.</p>'; return; }
+    var ts = tabState();
     var roomOpts = ROOMS.map(function (r) { return '<option value="' + esc(r) + '">' + esc(r) + '</option>'; }).join('');
-    scroll.innerHTML =
-      '<div class="mb-grid">' + b.map(function (x) {
-        return '<div class="mb-tile' + (x.primary ? ' is-primary' : '') + '" data-id="' + esc(x.id) + '">'
-          + '<button class="mb-star" type="button" data-star="' + esc(x.id) + '" aria-label="Make this the primary piece">' + (x.primary ? '★ Primary' : '☆ Set primary') + '</button>'
-          + '<button class="mb-tile-rm" type="button" data-rm="' + esc(x.id) + '" aria-label="Remove from mood board">✕</button>'
-          + '<img src="' + esc(x.image) + '" alt="" onerror="this.style.visibility=\'hidden\'">'
-          + '<div class="mb-tile-t">' + esc(x.title) + '</div></div>';
-      }).join('') + '</div>'
-      + '<div class="mb-foot">'
+
+    // ---- Project tab: switch/create the active mood-board project (in-drawer twin of the toolbar select) ----
+    var p = readProjects(), active = getActive();
+    var projRows = Object.keys(p).map(function (n) {
+      return '<button class="mb-proj' + (n === active ? ' is-active' : '') + '" type="button" data-proj="' + esc(n) + '">'
+        + '<span class="mb-proj-n">' + esc(n) + '</span><span class="mb-proj-c">' + p[n].length + '</span></button>';
+    }).join('');
+    var projectTab =
+      '<details class="mb-tab" data-tab="project"' + (ts.project ? ' open' : '') + '>'
+      + '<summary>Project · ' + esc(active) + '</summary>'
+      + '<div class="mb-tabbody"><div class="mb-projects">' + projRows + '</div>'
+      + '<button class="mb-newproj" type="button">+ New project</button></div></details>';
+
+    // ---- Pieces tab: the collected tiles (or empty state) ----
+    var piecesInner = b.length
+      ? '<div class="mb-grid">' + b.map(function (x) {
+          return '<div class="mb-tile' + (x.primary ? ' is-primary' : '') + '" data-id="' + esc(x.id) + '">'
+            + '<button class="mb-star" type="button" data-star="' + esc(x.id) + '" aria-label="Make this the primary piece">' + (x.primary ? '★ Primary' : '☆ Set primary') + '</button>'
+            + '<button class="mb-tile-rm" type="button" data-rm="' + esc(x.id) + '" aria-label="Remove from mood board">✕</button>'
+            + '<img src="' + esc(x.image) + '" alt="" onerror="this.style.visibility=\'hidden\'">'
+            + '<div class="mb-tile-t">' + esc(x.title) + '</div></div>';
+        }).join('') + '</div>'
+      : '<p class="mb-empty">This project is empty.<br>While you shop, tap <strong>◇ Mood Board</strong> on any piece to collect it here.</p>';
+    var piecesTab =
+      '<details class="mb-tab" data-tab="pieces"' + (ts.pieces ? ' open' : '') + '>'
+      + '<summary>Pieces (' + b.length + ')</summary>'
+      + '<div class="mb-tabbody">' + piecesInner + '</div></details>';
+
+    // ---- Create tab: room-type + generate the room setting ----
+    var createTab =
+      '<details class="mb-tab" data-tab="create"' + (ts.create ? ' open' : '') + '>'
+      + '<summary>Create room setting</summary>'
+      + '<div class="mb-tabbody mb-foot">'
       + '<select class="mb-room" aria-label="Room type for the setting"><option value="">Room type (optional)…</option>' + roomOpts + '</select>'
-      + '<button class="mb-make" type="button">Create Room Setting</button>'
-      + '<p class="mb-note">Composes your ' + b.length + ' piece' + (b.length === 1 ? '' : 's') + ' into one photoreal room — your ★ primary piece anchors it. Est. cost ~$' + COST.toFixed(3) + ' per room.</p>'
-      + '<button class="mb-clear" type="button">Clear board</button>'
-      + '</div>';
+      + '<button class="mb-make" type="button"' + (b.length ? '' : ' disabled') + '>Create Room Setting</button>'
+      + '<p class="mb-note">' + (b.length
+          ? 'Composes your ' + b.length + ' piece' + (b.length === 1 ? '' : 's') + ' into one photoreal room — your ★ primary piece anchors it. Est. cost ~$' + COST.toFixed(3) + ' per room.'
+          : 'Add pieces to this project first, then generate a room from them.') + '</p>'
+      + (b.length ? '<button class="mb-clear" type="button">Clear this project</button>' : '')
+      + '</div></details>';
+
+    scroll.innerHTML = projectTab + piecesTab + createTab;
+
+    // Persist each tab's open/closed state as the user toggles it. The `toggle`
+    // event doesn't bubble, so bind per-<details> (re-bound on every re-render).
+    scroll.querySelectorAll('.mb-tab').forEach(function (d) {
+      d.addEventListener('toggle', function () { setTab(d.getAttribute('data-tab'), d.open); });
+    });
   }
 
   function renderResult(scroll) {
@@ -305,6 +363,12 @@
     var mk = e.target.closest('.mb-make');
     if (mk && e.target.closest('.mb-foot') && !e.target.closest('.mb-result')) return armOrFire(mk);
     if (e.target.closest('.mb-clear')) { lastRender = null; save([]); return; }
+    var proj = e.target.closest('.mb-proj'); if (proj) return setActive(proj.getAttribute('data-proj'));
+    if (e.target.closest('.mb-newproj')) {
+      var nm = window.prompt('Name your new mood board project:', '');
+      if (nm && nm.trim()) createProject(nm);
+      return;
+    }
     var star = e.target.closest('[data-star]'); if (star) return setPrimary(star.getAttribute('data-star'));
     var rm = e.target.closest('[data-rm]'); if (rm) return removeItem(rm.getAttribute('data-rm'));
   });

← 590fca7 shop: collapse facet rail on load, 4-across grid + columns s  ·  back to Interiordesignershowroom  ·  Switch Honiture affiliate OFF (kept, hidden) + drop its inge d44578c →