← back to Quadrille Showroom
auto-save: 2026-07-02T10:47:21 (3 files) — package-lock.json public/js/dock.js public/showroom.html
b657af00c9811d16309d230a54f37da70263eb5a · 2026-07-02 10:47:24 -0700 · Steve Abrams
Files touched
M package-lock.jsonM public/js/dock.jsM public/showroom.html
Diff
commit b657af00c9811d16309d230a54f37da70263eb5a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 2 10:47:24 2026 -0700
auto-save: 2026-07-02T10:47:21 (3 files) — package-lock.json public/js/dock.js public/showroom.html
---
package-lock.json | 4 +-
public/js/dock.js | 437 ++++++++++++++++++++++++++++++---------------------
public/showroom.html | 36 +++--
3 files changed, 281 insertions(+), 196 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 0257f15..154fb7e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "quadrille-showroom",
- "version": "1.3.1",
+ "version": "1.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "quadrille-showroom",
- "version": "1.3.1",
+ "version": "1.3.2",
"license": "ISC",
"dependencies": {
"compression": "^1.8.1",
diff --git a/public/js/dock.js b/public/js/dock.js
index 13408cf..db383a2 100644
--- a/public/js/dock.js
+++ b/public/js/dock.js
@@ -1,54 +1,67 @@
/* ============================================================================
- * dock.js — The Quadrille House: HAMBURGER DOCK.
+ * dock.js — The Quadrille House: 4-CORNER HAMBURGER DOCK.
*
- * Standing ask (Steve, port 7690): on load the showroom is a bare 3D view with
- * ONE hamburger in the corner. Every control surface — Versions, View & Theme,
- * the bottom Controls bar, the Navigate bar, the Collection list — and every
- * top-toolbar tool (Peruse, Wings/Overview, Explore, Music, Lighting, Age View,
- * Fullscreen) is STOWED on load and re-launches from the hamburger as its own
- * draggable / resizable / position-persisted modal.
+ * Standing ask (Steve, port 7690, 2026-07-02): the single top-right hamburger is
+ * replaced by FOUR corner hamburgers — one in each screen corner — and every
+ * control function is SPLIT across the corners by theme. Everything is COLLAPSED
+ * and clean on load (DTD verdict A); each corner's hamburger expands only its own
+ * group. Controls scale with viewport width (clamp) and the bottom pager always
+ * shows cleanly.
+ *
+ * TOP-LEFT → Collection (vendor list) + Wings / Overview / Peruse
+ * TOP-RIGHT → View & Theme + display tools (Explore, Open Board, Walls,
+ * Lighting, Music, Fullscreen)
+ * BOTTOM-LEFT→ Furniture Style (the style tray, now collapsible)
+ * BOTTOM-RIGHT→ Versions + Controls (sort, density, wing sliders, room size,
+ * window pager)
*
* Non-destructive by design: the original panels keep their IDs and ALL of their
- * existing wiring (showroom.js / viewmodes.js / versions.js bind by ID). We only
- * (a) hide them on load (`.qh-stowed`), and
- * (b) on launch, wrap each in a `.qh-modal` shell + ModalRig.rig() the shell —
- * so the panel's own flex/transform layout is never fought.
+ * existing wiring (showroom.js / viewmodes.js / versions.js / furniture-styles.js
+ * bind by ID). We only (a) stow them on load (`.qh-stowed`), and (b) on launch
+ * wrap each in a `.qh-modal` shell (ModalRig-rigged), seeded to open ANCHORED to
+ * its owning corner. Panels carry a collapse (–) toggle so any element can be
+ * folded to its title bar.
*
* Depends on /js/modal-rig.js (window.ModalRig). Loaded LAST so every panel the
- * other bolt-ons inject already exists; a MutationObserver mops up late arrivals
- * (e.g. ageview's #btn-ageview, which is added on DOMContentLoaded).
+ * other bolt-ons inject already exists; a MutationObserver + a dedicated poller
+ * mop up late arrivals (ageview's #btn-ageview; furniture-styles' #qh-style-tray,
+ * which is built async once the furniture API is live).
* ========================================================================== */
(function () {
'use strict';
- // --- LAUNCHABLE PANELS: each becomes a movable modal -----------------------
- // sel = the existing element; label = menu/header text; w/h = first-open size.
- var PANELS = [
- // #guided-bar (◀ Previous / Next ▶ pager) is intentionally NOT docked — Steve wants it
- // ALWAYS visible while walking in book mode, so it stays a permanent bottom-centre fixture.
- { sel: '#window-bar', label: 'Controls', w: 760, h: 240 },
- { sel: '#vm-panel', label: 'View & Theme', w: 300, h: 380 },
- { sel: '#ver-rail', label: 'Versions', w: 250, h: 440 },
- { sel: '#vendor-sidebar', label: 'Collection', w: 250, h: 420 }
- ];
-
- // --- TOP-BAR TOOLS: menu items that just proxy-click the original button ---
- // (the buttons stay in the DOM, only their containers are hidden)
- var TOOLS = [
- { sel: '#btn-peruse', label: '▷ Peruse Collection' },
- { sel: '#top-bar [data-section="wings"]', label: 'Wing Boards' },
- { sel: '#top-bar [data-section="overview"]', label: 'Overview' },
- { sel: '#btn-explore', label: 'Explore (WASD)' },
- { sel: '#btn-bookmatch', label: 'Open Board' },
- { sel: '#btn-walls', label: 'Wallpaper Walls' },
- { sel: '#btn-music', label: 'Music' },
- { sel: '#btn-lighting', label: 'Lighting' },
- { sel: '#btn-fullscreen', label: 'Fullscreen' }
+ // --- CORNERS: each owns a themed group of panels + tool proxy-clicks --------
+ // panels: existing elements launched as movable modals.
+ // tools : menu items that proxy-click an existing button (button stays in DOM).
+ var CORNERS = [
+ { id: 'tl', pos: 'top-left', label: 'Collection & Tour',
+ panels: [ { sel: '#vendor-sidebar', label: 'Collection', w: 250, h: 420 } ],
+ tools: [ { sel: '#top-bar [data-section="wings"]', label: 'Wing Boards' },
+ { sel: '#top-bar [data-section="overview"]', label: 'Overview' },
+ { sel: '#btn-peruse', label: '▷ Peruse Collection' } ] },
+
+ { id: 'tr', pos: 'top-right', label: 'View & Tools',
+ panels: [ { sel: '#vm-panel', label: 'View & Theme', w: 300, h: 380 } ],
+ tools: [ { sel: '#btn-explore', label: 'Explore (WASD)' },
+ { sel: '#btn-bookmatch', label: 'Open Board' },
+ { sel: '#btn-walls', label: 'Wallpaper Walls' },
+ { sel: '#btn-lighting', label: 'Lighting' },
+ { sel: '#btn-music', label: 'Music' },
+ { sel: '#btn-fullscreen', label: 'Fullscreen' } ] },
+
+ { id: 'bl', pos: 'bottom-left', label: 'Furniture Style',
+ panels: [ { sel: '#qh-style-tray', label: 'Furniture Style', w: 260, h: 200 } ],
+ tools: [] },
+
+ { id: 'br', pos: 'bottom-right', label: 'Versions & Controls',
+ panels: [ { sel: '#ver-rail', label: 'Versions', w: 250, h: 440 },
+ { sel: '#window-bar', label: 'Controls', w: 760, h: 260 } ],
+ tools: [] }
];
- // --- STOW-ONLY: passive chrome hidden for a clean "just a hamburger" screen --
+ // --- STOW-ONLY: passive chrome hidden for a clean "just hamburgers" screen ---
var STOW_ONLY = [
- '#top-bar', // whole top toolbar (tools folded into menu)
+ '#top-bar', // whole top toolbar (tools folded into corner menus)
'#guided-instruction', '#guided-title', // guided-mode overlays
'#bottom-bar', '#now-viewing', // bottom navigator chatter (caption stays — book HUD)
'#minimap', // spatial minimap (explore aid)
@@ -57,8 +70,15 @@
];
var Z_BASE = 5000, zTop = Z_BASE; // modal stacking
- var openOrder = []; // selectors, in focus order (last = top)
- var LS_OPEN = 'qh_dock_open';
+ var openOrder = []; // panel selectors, in focus order (last = top)
+ var LS_OPEN = 'qh_dock_open_v2';
+
+ // flat index of every panel across corners (sel -> {panel, corner})
+ var PANEL_INDEX = {};
+ CORNERS.forEach(function (c) {
+ c.panels.forEach(function (p) { PANEL_INDEX[p.sel] = { p: p, corner: c }; });
+ });
+ function allPanels() { return CORNERS.reduce(function (a, c) { return a.concat(c.panels); }, []); }
// ---------------------------------------------------------------- styles ----
function injectStyles() {
@@ -67,37 +87,42 @@
st.id = 'qh-dock-styles';
st.textContent = [
'.qh-stowed{display:none !important;}',
- /* hamburger — top-right corner, always on top */
- '#qh-burger{position:fixed;top:14px;right:16px;z-index:9000;width:48px;height:48px;',
- 'display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;',
+ /* corner hamburgers — one per corner, responsive size */
+ '.qh-cburger{position:fixed;z-index:9000;',
+ 'width:clamp(40px,5.2vw,48px);height:clamp(40px,5.2vw,48px);',
+ 'display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(4px,0.6vw,5px);',
'background:rgba(12,12,17,0.86);border:1.5px solid rgba(201,169,110,0.5);border-radius:13px;',
'cursor:pointer;backdrop-filter:blur(12px);box-shadow:0 10px 30px -8px rgba(0,0,0,0.7);',
'transition:border-color .15s, transform .1s;}',
- '#qh-burger:hover{border-color:#c9a96e;}',
- '#qh-burger:active{transform:translateY(1px);}',
- '#qh-burger span{display:block;width:22px;height:2.5px;border-radius:2px;background:#c9a96e;transition:.2s;}',
- '#qh-burger.open span:nth-child(1){transform:translateY(7.5px) rotate(45deg);}',
- '#qh-burger.open span:nth-child(2){opacity:0;}',
- '#qh-burger.open span:nth-child(3){transform:translateY(-7.5px) rotate(-45deg);}',
- /* first-visit discoverability: gentle attention pulse + hint pill pointing at the ☰ */
- '#qh-burger.qh-attn{animation:qh-attn-pulse 1.9s ease-out infinite;}',
+ '.qh-cburger:hover{border-color:#c9a96e;}',
+ '.qh-cburger:active{transform:translateY(1px);}',
+ '.qh-cburger span{display:block;width:clamp(18px,2.4vw,22px);height:2.5px;border-radius:2px;',
+ 'background:#c9a96e;transition:.2s;}',
+ '.qh-cburger.open span:nth-child(1){transform:translateY(7.5px) rotate(45deg);}',
+ '.qh-cburger.open span:nth-child(2){opacity:0;}',
+ '.qh-cburger.open span:nth-child(3){transform:translateY(-7.5px) rotate(-45deg);}',
+ /* corner placement */
+ '#qh-cb-tl{top:14px;left:16px;} #qh-cb-tr{top:14px;right:16px;}',
+ '#qh-cb-bl{bottom:16px;left:16px;} #qh-cb-br{bottom:16px;right:16px;}',
+ /* first-visit attention pulse (one-time) */
+ '.qh-cburger.qh-attn{animation:qh-attn-pulse 1.9s ease-out infinite;}',
'@keyframes qh-attn-pulse{0%{box-shadow:0 10px 30px -8px rgba(0,0,0,0.7),0 0 0 0 rgba(201,169,110,0.5);}' +
- '70%{box-shadow:0 10px 30px -8px rgba(0,0,0,0.7),0 0 0 15px rgba(201,169,110,0);}' +
+ '70%{box-shadow:0 10px 30px -8px rgba(0,0,0,0.7),0 0 0 13px rgba(201,169,110,0);}' +
'100%{box-shadow:0 10px 30px -8px rgba(0,0,0,0.7),0 0 0 0 rgba(201,169,110,0);}}',
- '#qh-burger-hint{position:fixed;top:25px;right:74px;z-index:9000;pointer-events:none;',
- 'font-family:Georgia,serif;font-size:13px;letter-spacing:.4px;color:#f0e6cf;',
- 'background:rgba(12,12,17,0.88);border:1px solid rgba(201,169,110,0.5);',
- 'padding:6px 13px;border-radius:9px;backdrop-filter:blur(12px);white-space:nowrap;',
- 'box-shadow:0 10px 30px -8px rgba(0,0,0,0.7);transition:opacity .4s, transform .4s;}',
- '#qh-burger-hint::after{content:"";position:absolute;top:50%;right:-6px;transform:translateY(-50%);',
- 'border-top:6px solid transparent;border-bottom:6px solid transparent;border-left:6px solid rgba(201,169,110,0.5);}',
- '#qh-burger-hint.qh-hint-gone{opacity:0;transform:translateX(10px);}',
- /* launcher menu */
- '#qh-menu{position:fixed;top:70px;right:16px;z-index:8999;width:236px;max-height:78vh;overflow:auto;',
+ /* corner labels (tiny eyebrow under/over each burger, shown on hover) */
+ '.qh-clabel{position:fixed;z-index:8998;pointer-events:none;font-family:Georgia,serif;',
+ 'font-size:10px;letter-spacing:1px;text-transform:uppercase;color:#c9b98a;',
+ 'background:rgba(12,12,17,0.8);border:1px solid rgba(201,169,110,0.3);',
+ 'padding:3px 8px;border-radius:7px;opacity:0;transition:opacity .18s;white-space:nowrap;}',
+ '.qh-cburger:hover + .qh-clabel{opacity:1;}',
+ /* per-corner launcher menu */
+ '.qh-cmenu{position:fixed;z-index:8999;width:min(230px,78vw);max-height:70vh;overflow:auto;',
'background:rgba(14,14,20,0.94);border:1px solid rgba(201,169,110,0.3);border-radius:14px;',
'padding:8px;backdrop-filter:blur(14px);box-shadow:0 18px 50px -12px rgba(0,0,0,0.8);',
'display:none;font-family:inherit;}',
- '#qh-menu.open{display:block;}',
+ '.qh-cmenu.open{display:block;}',
+ '#qh-cm-tl{top:70px;left:16px;} #qh-cm-tr{top:70px;right:16px;}',
+ '#qh-cm-bl{bottom:74px;left:16px;} #qh-cm-br{bottom:74px;right:16px;}',
'.qh-menu-eyebrow{font-size:10px;letter-spacing:1.5px;text-transform:uppercase;color:#8c8678;',
'padding:8px 10px 4px;font-weight:700;}',
'.qh-menu-item{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;',
@@ -110,18 +135,21 @@
'.qh-menu-item.active{border-color:rgba(201,169,110,0.55);}',
/* modal shell */
'.qh-modal{position:absolute;z-index:5000;display:flex;flex-direction:column;',
- 'min-width:200px;min-height:90px;overflow:hidden;border-radius:13px;',
+ 'min-width:200px;min-height:44px;overflow:hidden;border-radius:13px;',
+ 'max-width:96vw;',
'background:rgba(12,12,17,0.92);border:1px solid rgba(201,169,110,0.34);',
'box-shadow:0 22px 60px -14px rgba(0,0,0,0.8);backdrop-filter:blur(10px);}',
+ '.qh-modal.qh-collapsed{height:auto !important;resize:none !important;}',
+ '.qh-modal.qh-collapsed > .qh-pane-host{display:none !important;}',
'.qh-modal-head{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:8px;',
'padding:8px 12px;cursor:move;user-select:none;border-bottom:1px solid rgba(201,169,110,0.22);',
'background:rgba(201,169,110,0.10);}',
'.qh-modal-title{font-family:Georgia,serif;font-size:14px;font-weight:600;color:#f0e6cf;letter-spacing:.4px;',
'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
- '.qh-modal-tools{flex:0 0 auto;display:flex;align-items:center;gap:2px;color:#c9a96e;}',
- '.qh-modal-x{cursor:pointer;opacity:.75;font-size:19px;line-height:1;padding:0 4px;}',
- '.qh-modal-x:hover{opacity:1;color:#fff;}',
- /* the original panel, neutralized, fills the shell body */
+ '.qh-modal-tools{flex:0 0 auto;display:flex;align-items:center;gap:6px;color:#c9a96e;}',
+ '.qh-modal-min,.qh-modal-x{cursor:pointer;opacity:.75;line-height:1;padding:0 2px;}',
+ '.qh-modal-min{font-size:17px;} .qh-modal-x{font-size:19px;}',
+ '.qh-modal-min:hover,.qh-modal-x:hover{opacity:1;color:#fff;}',
'.qh-modal > .qh-pane-host{flex:1 1 auto;overflow:auto;min-height:0;}'
].join('\n');
document.head.appendChild(st);
@@ -135,20 +163,24 @@
if (el) el.classList.add('qh-stowed');
}
- // P2: give each panel a staggered FIRST-OPEN position so launching several
- // doesn't pile them centered on top of each other. We pre-seed ModalRig's
- // saved-rect key only when none exists yet — so a position the user has
- // dragged (which ModalRig.saveRect writes to the same key) always wins on
- // subsequent opens. Cascade stays clamped inside the viewport.
- function seedCascadeRect(p) {
- var key = 'modalrig:qhdock:' + p.sel;
+ // Seed a panel's FIRST-OPEN rect anchored to its owning corner (so it reads as
+ // "belonging" to the hamburger it launched from). A position the user later
+ // drags wins on subsequent opens (ModalRig.saveRect writes the same key).
+ function seedCornerRect(p, corner) {
+ var key = 'modalrig:qhdock2:' + p.sel;
try { if (localStorage.getItem(key)) return; } catch (e) {}
- var i = PANELS.findIndex(function (x) { return x.sel === p.sel; });
- if (i < 0) i = openOrder.length;
- var step = 36, baseL = 90, baseT = 84;
- var w = p.w || 760, h = p.h || 420;
- var left = Math.max(0, Math.min(baseL + i * step, window.innerWidth - w - 16, window.innerWidth - 80));
- var top = Math.max(0, Math.min(baseT + i * step, window.innerHeight - h - 16, window.innerHeight - 40));
+ var w = p.w || 320, h = p.h || 360;
+ var m = 16, burgerZone = 70; // clear the corner burger
+ var vw = window.innerWidth, vh = window.innerHeight;
+ // stagger multiple panels in the same corner so they don't stack
+ var siblings = corner.panels;
+ var idx = siblings.findIndex(function (x) { return x.sel === p.sel; });
+ var off = (idx < 0 ? 0 : idx) * 34;
+ var left, top;
+ if (corner.pos.indexOf('left') > -1) left = m + off; else left = Math.max(m, vw - w - m - off);
+ if (corner.pos.indexOf('top') > -1) top = burgerZone + off; else top = Math.max(m, vh - h - m - off);
+ left = Math.max(0, Math.min(left, vw - 80));
+ top = Math.max(0, Math.min(top, vh - 40));
try { localStorage.setItem(key, JSON.stringify({ left: left, top: top, w: w, h: h })); } catch (e) {}
}
@@ -156,16 +188,20 @@
function launch(p) {
var el = $(p.sel);
if (!el) return;
+ var meta = PANEL_INDEX[p.sel];
+ var corner = meta ? meta.corner : CORNERS[0];
var wrap = el.closest('.qh-modal');
if (!wrap) {
- // first launch: build the shell, move the panel inside, neutralize it
wrap = document.createElement('div');
wrap.className = 'qh-modal';
wrap.setAttribute('data-qh-key', p.sel);
var head = document.createElement('div');
head.className = 'qh-modal-head';
head.innerHTML = '<span class="qh-modal-title"></span>' +
- '<span class="qh-modal-tools"><span class="qh-modal-x" title="Close (Esc)">×</span></span>';
+ '<span class="qh-modal-tools">' +
+ '<span class="qh-modal-min" title="Collapse">–</span>' +
+ '<span class="qh-modal-x" title="Close (Esc)">×</span>' +
+ '</span>';
head.querySelector('.qh-modal-title').textContent = p.label;
el.parentNode.insertBefore(wrap, el);
wrap.appendChild(head);
@@ -179,25 +215,31 @@
s.left = s.right = s.top = s.bottom = 'auto';
s.transform = 'none'; s.margin = '0';
s.width = '100%'; s.maxWidth = 'none'; s.maxHeight = 'none';
- s.display = ''; // clear any inline display:none from native collapse logic
-
- head.querySelector('.qh-modal-x').addEventListener('click', function (e) {
- e.stopPropagation(); hide(p);
+ s.display = '';
+
+ var xBtn = head.querySelector('.qh-modal-x');
+ xBtn.setAttribute('data-rig-btn', '1');
+ xBtn.addEventListener('click', function (e) { e.stopPropagation(); hide(p); });
+
+ var minBtn = head.querySelector('.qh-modal-min');
+ minBtn.setAttribute('data-rig-btn', '1');
+ minBtn.addEventListener('click', function (e) {
+ e.stopPropagation();
+ var collapsed = wrap.classList.toggle('qh-collapsed');
+ minBtn.textContent = collapsed ? '▸' : '–'; // ▸ when folded
+ minBtn.title = collapsed ? 'Expand' : 'Collapse';
});
- // keep the close × from initiating a drag
- head.querySelector('.qh-modal-x').setAttribute('data-rig-btn', '1');
+
wrap.addEventListener('mousedown', function () { bringToFront(p.sel, wrap); });
- seedCascadeRect(p); // P2: stagger first-open position so panels don't all stack centered
- ModalRig.rig(wrap, { key: 'qhdock:' + p.sel, handle: head, width: p.w, height: p.h });
+ seedCornerRect(p, corner);
+ ModalRig.rig(wrap, { key: 'qhdock2:' + p.sel, handle: head, width: p.w, height: p.h });
}
wrap.classList.remove('qh-stowed');
- el.classList.remove('qh-stowed'); // the PANEL itself was stowed (display:none!important) on boot;
- // removing it from the wrap alone left every launched panel's
- // CONTENT invisible/0×0 — only the header showed. (Phase-5 /5x.)
+ el.classList.remove('qh-stowed');
bringToFront(p.sel, wrap);
if (openOrder.indexOf(p.sel) === -1) openOrder.push(p.sel);
- saveOpen(); reflectMenu();
+ saveOpen(); reflectMenus();
}
function hide(p) {
@@ -205,7 +247,7 @@
var wrap = el.closest('.qh-modal');
if (wrap) wrap.classList.add('qh-stowed');
var i = openOrder.indexOf(p.sel); if (i > -1) openOrder.splice(i, 1);
- saveOpen(); reflectMenu();
+ saveOpen(); reflectMenus();
}
function isOpen(p) {
@@ -226,107 +268,132 @@
try { localStorage.setItem(LS_OPEN, JSON.stringify(openOrder)); } catch (e) {}
}
- // ----------------------------------------------------------------- menu -----
- var burger, menu;
+ // ----------------------------------------------------------------- menus ----
+ var burgers = {}, menus = {};
- function closeMenu() {
- if (menu) menu.classList.remove('open');
- if (burger) burger.classList.remove('open');
+ function closeAllMenus(except) {
+ CORNERS.forEach(function (c) {
+ if (c.id === except) return;
+ if (menus[c.id]) menus[c.id].classList.remove('open');
+ if (burgers[c.id]) burgers[c.id].classList.remove('open');
+ });
}
- // First-visit hint: stop the pulse, fade the pill out, and never show it again.
- function dismissHint() {
- if (burger) burger.classList.remove('qh-attn');
- var h = document.getElementById('qh-burger-hint');
- if (h) { h.classList.add('qh-hint-gone'); setTimeout(function () { if (h.parentNode) h.remove(); }, 450); }
- try { localStorage.setItem('qh_burger_seen', '1'); } catch (e) {}
+ function dismissAttn() {
+ CORNERS.forEach(function (c) { if (burgers[c.id]) burgers[c.id].classList.remove('qh-attn'); });
+ try { localStorage.setItem('qh_corners_seen', '1'); } catch (e) {}
}
- function buildMenu() {
- burger = document.createElement('div');
- burger.id = 'qh-burger';
- burger.title = 'Showroom controls';
- burger.innerHTML = '<span></span><span></span><span></span>';
- document.body.appendChild(burger);
-
- // First-visit discoverability: pulse the ☰ + show a labeled hint pill pointing at it.
- // Both are one-time — dismissed on first interaction and remembered so it never nags again.
- var seenBurger = false;
- try { seenBurger = !!localStorage.getItem('qh_burger_seen'); } catch (e) {}
- if (!seenBurger) {
- burger.classList.add('qh-attn');
- var hint = document.createElement('div');
- hint.id = 'qh-burger-hint';
- hint.textContent = 'Controls & Views';
- document.body.appendChild(hint);
+ function buildCorner(c) {
+ var b = document.createElement('div');
+ b.className = 'qh-cburger';
+ b.id = 'qh-cb-' + c.id;
+ b.title = c.label;
+ b.innerHTML = '<span></span><span></span><span></span>';
+ document.body.appendChild(b);
+ burgers[c.id] = b;
+
+ var lab = document.createElement('div');
+ lab.className = 'qh-clabel';
+ lab.textContent = c.label;
+ // position the label just inboard of the burger
+ var m = 16, gap = 70;
+ if (c.pos === 'top-left') { lab.style.top = '22px'; lab.style.left = gap + 'px'; }
+ else if (c.pos === 'top-right') { lab.style.top = '22px'; lab.style.right = gap + 'px'; }
+ else if (c.pos === 'bottom-left') { lab.style.bottom = '24px'; lab.style.left = gap + 'px'; }
+ else { lab.style.bottom = '24px'; lab.style.right = gap + 'px'; }
+ document.body.appendChild(lab);
+
+ var menu = document.createElement('div');
+ menu.className = 'qh-cmenu';
+ menu.id = 'qh-cm-' + c.id;
+ var html = '';
+ if (c.panels.length) {
+ html += '<div class="qh-menu-eyebrow">Panels</div>';
+ c.panels.forEach(function (p) {
+ html += '<button class="qh-menu-item" data-panel="' + p.sel + '">' +
+ '<span>' + p.label + '</span><span class="qh-dot">●</span></button>';
+ });
+ }
+ if (c.tools.length) {
+ html += '<div class="qh-menu-eyebrow">Tools</div>';
+ c.tools.forEach(function (t, i) {
+ html += '<button class="qh-menu-item" data-tool-idx="' + i + '">' +
+ '<span>' + t.label + '</span><span class="qh-dot">●</span></button>';
+ });
}
-
- menu = document.createElement('div');
- menu.id = 'qh-menu';
- document.body.appendChild(menu);
-
- var html = '<div class="qh-menu-eyebrow">Panels</div>';
- PANELS.forEach(function (p) {
- html += '<button class="qh-menu-item" data-panel="' + p.sel + '">' +
- '<span>' + p.label + '</span><span class="qh-dot">●</span></button>';
- });
- html += '<div class="qh-menu-eyebrow">Tools</div>';
- // NOTE: a TOOL selector may contain quotes/spaces (e.g. #top-bar [data-section="wings"]).
- // Storing the raw selector in a data-* attribute breaks the HTML (inner quotes close the
- // attribute early) AND breaks querySelector('[data-tool="<sel>"]') ("is not a valid
- // selector"). So we key tools by INDEX and resolve TOOLS[idx].sel in JS. (Phase-5 /5x.)
- TOOLS.forEach(function (t, i) {
- html += '<button class="qh-menu-item" data-tool-idx="' + i + '">' +
- '<span>' + t.label + '</span><span class="qh-dot">●</span></button>';
- });
menu.innerHTML = html;
+ document.body.appendChild(menu);
+ menus[c.id] = menu;
menu.addEventListener('click', function (e) {
var item = e.target.closest('.qh-menu-item'); if (!item) return;
var pSel = item.getAttribute('data-panel');
var tIdx = item.getAttribute('data-tool-idx');
if (pSel) {
- var p = PANELS.filter(function (x) { return x.sel === pSel; })[0];
+ var p = c.panels.filter(function (x) { return x.sel === pSel; })[0];
if (p) toggle(p);
- closeMenu(); // P3 (DTD A): launching a panel auto-closes the menu so the modal is unobstructed
+ closeAllMenus();
} else if (tIdx !== null) {
- var t = TOOLS[+tIdx];
+ var t = c.tools[+tIdx];
var btn = t && $(t.sel);
- if (btn) { btn.click(); setTimeout(reflectMenu, 30); }
- closeMenu();
+ if (btn) { btn.click(); setTimeout(reflectMenus, 30); }
+ closeAllMenus();
}
});
- burger.addEventListener('click', function () {
- dismissHint();
+ b.addEventListener('click', function () {
+ dismissAttn();
+ // single-item corner (1 panel, no tools) → toggle it directly, no menu
+ if (c.panels.length === 1 && c.tools.length === 0) {
+ toggle(c.panels[0]);
+ b.classList.toggle('open', isOpen(c.panels[0]));
+ closeAllMenus();
+ return;
+ }
var open = menu.classList.toggle('open');
- burger.classList.toggle('open', open);
- if (open) reflectMenu();
+ b.classList.toggle('open', open);
+ closeAllMenus(c.id);
+ if (open) reflectMenus();
});
- // click-away closes the menu (but not the modals)
+ }
+
+ function buildCorners() {
+ CORNERS.forEach(buildCorner);
+
+ var seen = false;
+ try { seen = !!localStorage.getItem('qh_corners_seen'); } catch (e) {}
+ if (!seen) CORNERS.forEach(function (c) { if (burgers[c.id]) burgers[c.id].classList.add('qh-attn'); });
+
+ // click-away closes menus (never the modals)
document.addEventListener('mousedown', function (e) {
- if (!menu.classList.contains('open')) return;
- if (e.target.closest('#qh-menu') || e.target.closest('#qh-burger')) return;
- menu.classList.remove('open'); burger.classList.remove('open');
+ if (e.target.closest('.qh-cmenu') || e.target.closest('.qh-cburger')) return;
+ closeAllMenus();
});
}
- function reflectMenu() {
- if (!menu) return;
- PANELS.forEach(function (p) {
- var it = menu.querySelector('[data-panel="' + p.sel + '"]');
- if (it) it.classList.toggle('active', isOpen(p));
- });
- TOOLS.forEach(function (t, i) {
- var it = menu.querySelector('[data-tool-idx="' + i + '"]'); // index key — selector may contain quotes
- var btn = $(t.sel);
- if (it) it.classList.toggle('active', !!btn && btn.classList.contains('active'));
+ function reflectMenus() {
+ CORNERS.forEach(function (c) {
+ var menu = menus[c.id]; if (!menu) return;
+ c.panels.forEach(function (p) {
+ var it = menu.querySelector('[data-panel="' + p.sel + '"]');
+ if (it) it.classList.toggle('active', isOpen(p));
+ });
+ c.tools.forEach(function (t, i) {
+ var it = menu.querySelector('[data-tool-idx="' + i + '"]');
+ var btn = $(t.sel);
+ if (it) it.classList.toggle('active', !!btn && btn.classList.contains('active'));
+ });
+ // single-item corner: reflect open-state on the burger itself
+ if (c.panels.length === 1 && c.tools.length === 0 && burgers[c.id]) {
+ burgers[c.id].classList.toggle('open', isOpen(c.panels[0]));
+ }
});
}
// ------------------------------------------------------------- stow + boot --
function stowAll() {
- PANELS.forEach(function (p) {
+ allPanels().forEach(function (p) {
var el = $(p.sel);
if (el && !el.closest('.qh-modal')) stow(el); // don't re-stow a launched modal's pane
});
@@ -337,33 +404,47 @@
var saved = [];
try { saved = JSON.parse(localStorage.getItem(LS_OPEN) || '[]') || []; } catch (e) {}
saved.forEach(function (sel) {
- var p = PANELS.filter(function (x) { return x.sel === sel; })[0];
- if (p) launch(p);
+ var meta = PANEL_INDEX[sel];
+ if (meta) launch(meta.p);
});
}
- function boot() {
- if (!window.ModalRig) { // modal-rig.js must load first
- return setTimeout(boot, 60);
+ // The furniture-style tray is built async (furniture-styles.js waits for the
+ // furniture API). Poll for it, stow it on arrival, and if it was open last
+ // session re-launch it.
+ function ownStyleTray(tries) {
+ tries = tries || 0;
+ var el = $('#qh-style-tray');
+ if (el) {
+ if (!el.closest('.qh-modal')) stow(el);
+ var saved = [];
+ try { saved = JSON.parse(localStorage.getItem(LS_OPEN) || '[]') || []; } catch (e) {}
+ if (saved.indexOf('#qh-style-tray') > -1) launch(PANEL_INDEX['#qh-style-tray'].p);
+ return;
}
+ if (tries > 240) return; // ~14s
+ setTimeout(function () { ownStyleTray(tries + 1); }, 60);
+ }
+
+ function boot() {
+ if (!window.ModalRig) return setTimeout(boot, 60);
injectStyles();
stowAll();
- buildMenu();
+ buildCorners();
- // Late-arriving nodes (ageview's #btn-ageview, or any panel injected on a
- // later tick) — keep them stowed until first launch.
var mo = new MutationObserver(function () { stowAll(); });
mo.observe(document.body, { childList: true, subtree: true });
- setTimeout(function () { mo.disconnect(); }, 4000); // settle window
+ setTimeout(function () { mo.disconnect(); }, 4000);
restoreOpen();
+ ownStyleTray(0);
// Esc closes the top-most open modal.
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape' || !openOrder.length) return;
var sel = openOrder[openOrder.length - 1];
- var p = PANELS.filter(function (x) { return x.sel === sel; })[0];
- if (p) { hide(p); e.stopPropagation(); }
+ var meta = PANEL_INDEX[sel];
+ if (meta) { hide(meta.p); e.stopPropagation(); }
}, true);
}
diff --git a/public/showroom.html b/public/showroom.html
index 3fafdbb..dc225db 100644
--- a/public/showroom.html
+++ b/public/showroom.html
@@ -18,36 +18,42 @@
Big, high-contrast, word-labelled controls at bottom-centre so a
65-year-old needs NO keyboard and NO 3D walking. Tap targets >=56px tall.
============================================================ */
+ /* Bottom pager — smaller + viewport-responsive (Steve 2026-07-02: reduce the
+ button size, scale with website width, always show cleanly at the bottom).
+ Sits centred between the bottom-left/right corner hamburgers with margin to
+ spare; wraps on narrow screens via the media query below. */
#guided-bar {
- position:fixed; left:50%; bottom:22px; transform:translateX(-50%);
- z-index:40; display:flex; align-items:center; gap:14px;
+ position:fixed; left:50%; bottom:16px; transform:translateX(-50%);
+ z-index:40; display:flex; align-items:center; gap:clamp(6px,1vw,12px);
+ max-width:calc(100vw - 148px); /* clear the ~64px corner burgers on each side */
background:rgba(12,12,17,0.90); border:1.5px solid rgba(201,169,110,0.45);
- border-radius:18px; padding:12px 16px; backdrop-filter:blur(14px);
- box-shadow:0 18px 50px -12px rgba(0,0,0,0.75);
+ border-radius:16px; padding:clamp(6px,0.9vw,10px) clamp(8px,1.2vw,14px);
+ backdrop-filter:blur(14px); box-shadow:0 18px 50px -12px rgba(0,0,0,0.75);
}
#guided-bar.hidden { display:none; }
.g-btn {
- min-height:60px; min-width:140px; padding:0 24px;
- display:inline-flex; align-items:center; justify-content:center; gap:8px;
- font-size:20px; font-weight:700; letter-spacing:.5px; cursor:pointer;
- border-radius:12px; border:2px solid rgba(201,169,110,0.55);
- background:rgba(255,255,255,0.06); color:#f0e6cf;
+ min-height:clamp(38px,5vw,48px); min-width:clamp(82px,11vw,124px);
+ padding:0 clamp(10px,1.5vw,18px);
+ display:inline-flex; align-items:center; justify-content:center; gap:6px;
+ font-size:clamp(12px,1.45vw,16px); font-weight:700; letter-spacing:.4px; cursor:pointer;
+ border-radius:11px; border:2px solid rgba(201,169,110,0.55);
+ background:rgba(255,255,255,0.06); color:#f0e6cf; white-space:nowrap;
transition:background .15s, transform .1s, border-color .15s;
-webkit-user-select:none; user-select:none;
}
.g-btn:hover { background:rgba(201,169,110,0.22); border-color:#c9a96e; }
.g-btn:active { transform:translateY(1px); }
.g-btn:disabled { opacity:.4; cursor:default; }
- /* The big primary Auto-Tour button — brass fill so it's the obvious hero action */
+ /* The primary Auto-Tour button — brass fill so it's the obvious hero action */
#g-tour {
- min-width:200px; font-size:21px;
+ min-width:clamp(120px,15vw,168px); font-size:clamp(13px,1.5vw,17px);
background:linear-gradient(180deg,#d8bd84,#b9933f); color:#1a1206;
border-color:#b9933f;
}
#g-tour:hover { background:linear-gradient(180deg,#e3cb95,#c5a04a); }
#g-tour.playing { background:linear-gradient(180deg,#d06a5a,#a8392b); color:#fff; border-color:#a8392b; }
/* The 'All Designs' grid button */
- #g-grid { min-width:130px; }
+ #g-grid { min-width:clamp(88px,11vw,118px); }
/* Always-on plain instruction strip above the guided bar */
#guided-instruction {
position:fixed; left:50%; bottom:96px; transform:translateX(-50%);
@@ -130,11 +136,9 @@
so paging one design at a time through all 883 is always available. Placed AFTER the
explore-mode hide so equal-specificity !important resolves in book mode's favour. */
body.book-mode #guided-bar { display:flex !important; }
- /* On small screens the big guided bar wraps; let it */
+ /* On small screens the pager wraps; clamp() already handles button sizing */
@media (max-width:720px){
- #guided-bar { flex-wrap:wrap; width:92vw; justify-content:center; }
- .g-btn { min-width:120px; font-size:17px; }
- #g-tour { min-width:160px; }
+ #guided-bar { flex-wrap:wrap; max-width:calc(100vw - 24px); justify-content:center; }
#guided-title .gt-name { font-size:23px; }
}
← bf8db8a chore: v1.3.2 (session close — furniture style selector + ar
·
back to Quadrille Showroom
·
5x sweep 1: keep bottom-corner panels clear of the centre pa a156052 →