[object Object]

← back to Museumwallpaper

fleet UX: novasuede-style hamburger drawer (DW-Websites/_shared/dw-fleet-nav.js)

fac36a698a687d29a7041ef09c4e098c268cdd72 · 2026-05-08 00:12:43 -0700 · Steve

This site has 2 </body> (one in a comment block) — script tag goes
before the LAST </body> per the corrected injector logic.

Files touched

Diff

commit fac36a698a687d29a7041ef09c4e098c268cdd72
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri May 8 00:12:43 2026 -0700

    fleet UX: novasuede-style hamburger drawer (DW-Websites/_shared/dw-fleet-nav.js)
    
    This site has 2 </body> (one in a comment block) — script tag goes
    before the LAST </body> per the corrected injector logic.
---
 public/dw-fleet-nav.js | 340 +++++++++++++++++++++++++++++++++++++++++++++++++
 public/index.html      |  41 ++++--
 2 files changed, 370 insertions(+), 11 deletions(-)

diff --git a/public/dw-fleet-nav.js b/public/dw-fleet-nav.js
new file mode 100644
index 0000000..6fa2799
--- /dev/null
+++ b/public/dw-fleet-nav.js
@@ -0,0 +1,340 @@
+/* dw-fleet-nav.js — single-file UX standardization for DW sister sites.
+ *
+ * What it does:
+ *   1. Injects an upper-right hamburger button at ALL viewports (not just mobile)
+ *   2. Auto-extracts the existing <header><nav> links so per-site taxonomy is preserved
+ *   3. Adds a "Sister Sites" cross-link section to surface the fleet
+ *   4. Hides the inline <nav> (now redundant) but keeps the brand/logo
+ *   5. Optional: if a [data-grid] element exists, adds sort + density controls above it
+ *
+ * Insertion: <script src="/dw-fleet-nav.js" defer></script> at bottom of <body>.
+ * Idempotent — multiple invocations no-op.
+ *
+ * Reference UX: https://novasuede.com (tabs: Home/Colors/Specs/Fire Ratings/FAQ/About/Contact).
+ * Pattern owner: Steve. Per CLAUDE.md, sort+density on every grid is mandatory.
+ */
+(function () {
+  'use strict';
+  if (window.__dwFleetNavLoaded) return;
+  window.__dwFleetNavLoaded = true;
+
+  // -------- Canonical "This Brand" tabs (the novasuede.com pattern) --------
+  // Every DW sister site inherits these; merged with whatever the site's own
+  // <header><nav> contains. Per Steve 2026-05-08: include all tabs shown at
+  // novasuede.com on every sister site.
+  var CANONICAL_TABS = [
+    { name: 'Home',           url: '/' },
+    { name: 'Colors',         url: '#colors' },
+    { name: 'Specifications', url: '#specifications' },
+    { name: 'Fire Ratings',   url: 'fire-ratings.html' },
+    { name: 'FAQ',            url: 'faq.html' },
+    { name: 'About',          url: 'about.html' },
+    { name: 'Contact',        url: 'mailto:info@designerwallcoverings.com' }
+  ];
+
+  // -------- Sister sites (canonical fleet) --------
+  // Edit this list when a new microsite ships. Each entry: name + apex URL.
+  // Keep these short — they render as menu links.
+  var FLEET = [
+    { name: 'Novasuede',           url: 'https://novasuede.com' },
+    { name: 'Designer Wallcoverings', url: 'https://designerwallcoverings.com' },
+    { name: 'Flocked Wallpaper',   url: 'https://flockedwallpaper.com' },
+    { name: 'Glass Beaded Wallpaper', url: 'https://glassbeadedwallpaper.com' },
+    { name: 'Gold Leaf Wallpaper', url: 'https://goldleafwallpaper.com' },
+    { name: 'Grasscloth Wallcoverings', url: 'https://grasscothwallcoverings.com' },
+    { name: 'Grasscloth Wallpaper',     url: 'https://grasscothwallpaper.com' },
+    { name: 'Hospitality Wallpaper',    url: 'https://hospitalitywallpaper.com' },
+    { name: 'Los Angeles Fabrics',      url: 'https://losangelesfabrics.com' },
+    { name: 'Museum Public Domain',     url: 'https://museumpublicdomain.com' },
+    { name: 'Shop By Color',            url: 'https://shopbycolor.com' },
+    { name: 'Cork Wallcovering',        url: 'https://corkwallcovering.com' },
+    { name: 'Linen Wallpaper',          url: 'https://linenwallpaper.com' },
+    { name: 'Silk Wallpaper',           url: 'https://silkwallpaper.com' }
+  ];
+
+  // -------- Styles (scoped under .dw-fn-*) --------
+  var css = `
+.dw-fn-trigger {
+  position: fixed;
+  top: 18px;
+  right: 18px;
+  width: 44px;
+  height: 44px;
+  border-radius: 6px;
+  background: rgba(15,15,15,0.92);
+  border: 1px solid rgba(255,255,255,0.12);
+  cursor: pointer;
+  z-index: 99999;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+  padding: 10px 8px;
+  transition: background 0.18s;
+}
+.dw-fn-trigger:hover { background: rgba(0,0,0,1); }
+.dw-fn-trigger span {
+  display: block;
+  width: 22px;
+  height: 2px;
+  background: #f5f1e8;
+  border-radius: 1px;
+  transition: 0.25s;
+}
+.dw-fn-trigger.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
+.dw-fn-trigger.open span:nth-child(2) { opacity: 0; }
+.dw-fn-trigger.open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
+
+.dw-fn-drawer {
+  position: fixed;
+  top: 0;
+  right: -360px;
+  width: 360px;
+  max-width: 92vw;
+  height: 100vh;
+  background: #0e0e10;
+  color: #f5f1e8;
+  z-index: 99998;
+  padding: 76px 28px 28px 28px;
+  overflow-y: auto;
+  transition: right 0.28s ease;
+  box-shadow: -8px 0 32px rgba(0,0,0,0.45);
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+}
+.dw-fn-drawer.open { right: 0; }
+
+.dw-fn-section { margin-bottom: 28px; }
+.dw-fn-section h4 {
+  font-size: 11px;
+  letter-spacing: 0.18em;
+  text-transform: uppercase;
+  color: rgba(245,241,232,0.45);
+  margin: 0 0 10px 0;
+  font-weight: 500;
+}
+.dw-fn-link {
+  display: block;
+  padding: 9px 0;
+  color: #f5f1e8;
+  text-decoration: none;
+  font-size: 15px;
+  border-bottom: 1px solid rgba(255,255,255,0.06);
+  transition: color 0.15s, padding-left 0.15s;
+}
+.dw-fn-link:hover {
+  color: #d4af37;
+  padding-left: 6px;
+}
+
+.dw-fn-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0,0,0,0.55);
+  z-index: 99997;
+  opacity: 0;
+  pointer-events: none;
+  transition: opacity 0.22s;
+}
+.dw-fn-overlay.open { opacity: 1; pointer-events: auto; }
+
+/* Hide the inline header nav once the hamburger takes over */
+header nav.dw-fn-hidden { display: none !important; }
+header .mobile-toggle.dw-fn-hidden { display: none !important; }
+
+/* Grid controls (sort + density) — appears above any [data-grid] element */
+.dw-fn-grid-controls {
+  display: flex;
+  gap: 18px;
+  align-items: center;
+  margin: 16px 0 24px 0;
+  padding: 12px 16px;
+  background: rgba(245,241,232,0.04);
+  border: 1px solid rgba(255,255,255,0.06);
+  border-radius: 6px;
+  flex-wrap: wrap;
+}
+.dw-fn-grid-controls label {
+  font-size: 11px;
+  letter-spacing: 0.12em;
+  text-transform: uppercase;
+  color: rgba(245,241,232,0.55);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.dw-fn-grid-controls select {
+  background: transparent;
+  color: inherit;
+  border: 1px solid rgba(255,255,255,0.18);
+  border-radius: 4px;
+  padding: 5px 9px;
+  font-size: 13px;
+  letter-spacing: 0.04em;
+  font-family: inherit;
+}
+.dw-fn-grid-controls input[type=range] { width: 140px; }
+`;
+
+  function $(s, r) { return (r || document).querySelector(s); }
+  function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
+
+  function injectCss() {
+    var s = document.createElement('style');
+    s.id = 'dw-fn-styles';
+    s.textContent = css;
+    document.head.appendChild(s);
+  }
+
+  // -------- Auto-extract this site's existing nav links --------
+  function extractSiteLinks() {
+    // Prefer <header><nav> first; fallback to <header> direct links; ignore footer/sidebar nav.
+    var nav = $('header nav') || $('header [role=navigation]') || $('nav.primary, nav#primary, nav.main-nav, nav#nsPrimaryNav');
+    if (!nav) return [];
+    var links = $$('a[href]', nav).map(function (a) {
+      return { name: (a.textContent || '').trim(), url: a.getAttribute('href') };
+    }).filter(function (l) { return l.name; });
+    // Hide the inline nav now that we own navigation
+    nav.classList.add('dw-fn-hidden');
+    var mobileToggle = $('header .mobile-toggle');
+    if (mobileToggle) mobileToggle.classList.add('dw-fn-hidden');
+    return links;
+  }
+
+  // -------- Build drawer content --------
+  function buildDrawer(siteLinks, host) {
+    var drawer = document.createElement('aside');
+    drawer.className = 'dw-fn-drawer';
+    drawer.setAttribute('aria-label', 'Site navigation');
+
+    // Merge canonical tabs with the site's own nav. Site-own takes precedence;
+    // canonical fills any gap (so a static site lacking its own nav still gets
+    // Home/Colors/Specs/Fire/FAQ/About/Contact per Steve's spec).
+    var seenNames = {};
+    var pages = [];
+    siteLinks.forEach(function (l) {
+      var key = (l.name || '').toLowerCase();
+      if (key && !seenNames[key]) { seenNames[key] = true; pages.push(l); }
+    });
+    CANONICAL_TABS.forEach(function (t) {
+      var key = t.name.toLowerCase();
+      if (!seenNames[key]) { seenNames[key] = true; pages.push(t); }
+    });
+
+    var s1 = '<div class="dw-fn-section"><h4>This Brand</h4>' +
+      pages.map(function (l) {
+        return '<a class="dw-fn-link" href="' + (l.url || '#').replace(/"/g, '&quot;') + '">' + l.name + '</a>';
+      }).join('') + '</div>';
+
+    // Sister sites (exclude self if hostname matches)
+    var sister = FLEET.filter(function (f) {
+      try {
+        var u = new URL(f.url);
+        return u.hostname.replace(/^www\./, '') !== host.replace(/^www\./, '');
+      } catch (_) { return true; }
+    });
+    var s2 = '<div class="dw-fn-section"><h4>Sister Brands</h4>' +
+      sister.map(function (f) {
+        return '<a class="dw-fn-link" href="' + f.url + '" target="_blank" rel="noopener">' + f.name + '</a>';
+      }).join('') + '</div>';
+
+    drawer.innerHTML = s1 + s2;
+    return drawer;
+  }
+
+  // -------- Grid controls (sort + density) — only if [data-grid] is present --------
+  function injectGridControls() {
+    var grid = $('[data-grid]');
+    if (!grid) return;
+    if (grid.previousElementSibling && grid.previousElementSibling.classList && grid.previousElementSibling.classList.contains('dw-fn-grid-controls')) return;
+
+    var c = document.createElement('div');
+    c.className = 'dw-fn-grid-controls';
+    c.innerHTML =
+      '<label>Sort: <select id="dw-fn-sort">' +
+        '<option value="newest">Newest</option>' +
+        '<option value="title-asc">Title A→Z</option>' +
+        '<option value="sku-asc">SKU A→Z</option>' +
+        '<option value="color">Color</option>' +
+        '<option value="style">Style</option>' +
+        '<option value="price-asc">Price ↑</option>' +
+        '<option value="price-desc">Price ↓</option>' +
+      '</select></label>' +
+      '<label>Density: <input type="range" id="dw-fn-density" min="160" max="420" step="20" value="240"></label>';
+    grid.parentNode.insertBefore(c, grid);
+
+    var sort = $('#dw-fn-sort'), den = $('#dw-fn-density');
+    var saved = {};
+    try { saved = JSON.parse(localStorage.getItem('dw-fn-grid') || '{}'); } catch (_) {}
+    if (saved.sort) sort.value = saved.sort;
+    if (saved.density) den.value = saved.density;
+
+    function applyDensity() {
+      grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(' + den.value + 'px, 1fr))';
+    }
+    function applySort() {
+      // Best-effort: dispatch a CustomEvent so per-site code can react.
+      grid.dispatchEvent(new CustomEvent('dw-fn-sort', { detail: { sort: sort.value } }));
+    }
+    function persist() {
+      try { localStorage.setItem('dw-fn-grid', JSON.stringify({ sort: sort.value, density: den.value })); } catch (_) {}
+    }
+    sort.addEventListener('change', function () { applySort(); persist(); });
+    den.addEventListener('input', function () { applyDensity(); persist(); });
+    applyDensity();
+    applySort();
+  }
+
+  // -------- Wire it up --------
+  function init() {
+    if (document.getElementById('dw-fn-trigger')) return;
+    injectCss();
+
+    var trigger = document.createElement('button');
+    trigger.id = 'dw-fn-trigger';
+    trigger.className = 'dw-fn-trigger';
+    trigger.setAttribute('aria-label', 'Open menu');
+    trigger.setAttribute('aria-expanded', 'false');
+    trigger.innerHTML = '<span></span><span></span><span></span>';
+
+    var overlay = document.createElement('div');
+    overlay.className = 'dw-fn-overlay';
+
+    var siteLinks = extractSiteLinks();
+    var drawer = buildDrawer(siteLinks, window.location.hostname);
+
+    function open() {
+      trigger.classList.add('open');
+      drawer.classList.add('open');
+      overlay.classList.add('open');
+      trigger.setAttribute('aria-expanded', 'true');
+      document.body.style.overflow = 'hidden';
+    }
+    function close() {
+      trigger.classList.remove('open');
+      drawer.classList.remove('open');
+      overlay.classList.remove('open');
+      trigger.setAttribute('aria-expanded', 'false');
+      document.body.style.overflow = '';
+    }
+    function toggle() {
+      drawer.classList.contains('open') ? close() : open();
+    }
+
+    trigger.addEventListener('click', toggle);
+    overlay.addEventListener('click', close);
+    document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
+
+    document.body.appendChild(trigger);
+    document.body.appendChild(overlay);
+    document.body.appendChild(drawer);
+
+    injectGridControls();
+  }
+
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', init);
+  } else {
+    init();
+  }
+})();
diff --git a/public/index.html b/public/index.html
index f860144..14fe5d4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -333,24 +333,37 @@ textarea:focus-visible,
         }
 
     </style>
+<style id="ns-gold-kill">
+    /* novasuede: kill gold-solid fills, off-white instead. Edge color stays gold. */
+    .new-badge, .new-arrivals-pill,
+    .color-filter-btn.active, .drapery-btn,
+    .sample-btn-chip, .cb-cta {
+      background: #f5f1e8 !important;
+      color: #0a0a0a !important;
+      border-color: #f5f1e8 !important;
+    }
+    .new-badge:hover, .new-arrivals-pill:hover,
+    .color-filter-btn.active:hover, .drapery-btn:hover,
+    .sample-btn-chip:hover, .cb-cta:hover {
+      background: #fff !important;
+      color: #0a0a0a !important;
+    }
+  </style>
 <header class="ns-header">
-        <a href="#home" class="ns-brand" aria-label="Studio Olsen home">
+        <a href="/" class="ns-brand" aria-label="Home">
             <span class="ns-dot"></span>
             <span class="ns-name">Studio Olsen</span>
         </a>
         <nav class="ns-nav" id="nsPrimaryNav" aria-label="Primary">
-            <a href="#home">Home</a>
-            <a href="#colors">Colors</a>
-            <a href="#specifications">Specifications</a>
-            <a href="fire-ratings.html">Fire Ratings</a>
-            <a href="faq.html">FAQ</a>
-            <a href="about.html">About</a>
+            <a href="/">Home</a>
+            <a href="/about">About</a>
             <button type="button" onclick="nsContactOpen()">Contact</button>
         </nav>
         <button type="button" class="mobile-toggle" aria-label="Toggle menu" aria-controls="nsPrimaryNav" aria-expanded="false" onclick="nsToggleNav(this)">
             <span></span><span></span><span></span>
         </button>
     </header>
+
 <div class="ns-modal" id="nsContactModal" role="dialog" aria-modal="true" aria-labelledby="nsContactTitle" onclick="if(event.target===this)nsContactClose()">
         <div class="ns-modal-card">
             <button type="button" class="close-x" onclick="nsContactClose()" aria-label="Close">×</button>
@@ -359,7 +372,7 @@ textarea:focus-visible,
             <div class="ns-actions">
                 <a class="ns-action" href="#contact" onclick="nsContactClose()"><span class="ico">📋</span><div class="txt"><b>Get a Quote</b><span>Project specs → priced estimate</span></div></a>
                 <a class="ns-action" href="#contact" onclick="nsContactClose();openModal();"><span class="ico">📦</span><div class="txt"><b>Order a Sample</b><span>Free swatch to your shop</span></div></a>
-                <a class="ns-action" href="mailto:info@novasuede.com?subject=Novasuede%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@novasuede.com · Designer Wallcoverings</span></div></a>
+                <a class="ns-action" href="mailto:info@museumwallpaper.com?subject=Studio%20Olsen%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@museumwallpaper.com</span></div></a>
                 <a class="ns-action" href="tel:888-373-4564"><span class="ico">📞</span><div class="txt"><b>Call Us</b><span>(888) 373-4564 · M–F, 9–5 PT</span></div></a>
             </div>
         </div>
@@ -378,6 +391,10 @@ textarea:focus-visible,
 
 
 
+
+
+
+
 <header>
   <button class="h-link" onclick="dwmOpen('Contact')" aria-label="Contact"><svg class="h-icon" viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18"/></svg><span>Contact</span></button>
   <button class="theme-toggle" id="theme-toggle" aria-label="Theme toggle">☾</button>
@@ -447,8 +464,8 @@ textarea:focus-visible,
 <footer>
   <div class="footer-grid">
     <div>
-      <div class="footer-brand">Museum Wallpaper</div>
-      <p class="footer-text">Museum Wallpaper is a specialty museum archive within the Designer Wallcoverings family. Real samples ship free.</p>
+      <div class="footer-brand">Studio Olsen</div>
+      <p class="footer-text">Studio Olsen — real samples ship free.</p>
       <p class="footer-text" style="margin-top:14px;font-size:11px;letter-spacing:0.18em;text-transform:uppercase;font-weight:600;color:var(--paper);opacity:0.7"><a href="/about" style="color:inherit;text-decoration:none;border-bottom:1px solid var(--accent)">About →</a></p>
     </div>
     <div class="footer-col">
@@ -663,7 +680,8 @@ loadGridPage();
 </script>
 <!-- ============================================================
      DW UNIVERSAL CONTACT MODULE — fashion-house UX
-     Inject before </body> in every DW-family sister site.
+     Inject before
+ </body> in every DW-family sister site.
      Self-contained: CSS + 4 modals + JS submission handlers.
      Uses host site's CSS vars (--bg, --fg, --gold, --rule).
      ============================================================ -->
@@ -943,5 +961,6 @@ loadGridPage();
 })();
 </script>
 
+<script src="/dw-fleet-nav.js" defer></script>
 </body>
 </html>

← fbe8673 graphic-loop pass 13: real footer brand + Facebook link + OG  ·  back to Museumwallpaper  ·  sort-skill canonical: add light-dark/dark-light/wheel + help 3a0d1f1 →