← back to Ffepurchasing
public/dw-fleet-nav.js
341 lines
/* 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, '"') + '">' + 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 noreferrer">' + 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();
}
})();