← back to 1890swallpaper
public/ux-primitives.bundle.js
1359 lines
/* ux-primitives.bundle.js — canonical Bundle-B UX primitives (generated 2026-09-23T00:57:00Z)
7 zero-dependency IIFE primitives (window globals Toast, Skeleton, EmptyState,
Copy, CommandPalette, ModalRig, Bento) + ONE idempotent declarative auto-init
(UXPrimitives.init). No-ops on a page using none of them. Globals are distinct
from corner-nav (CornerNavConfig) + hero-4grid (Hero4GridConfig).
Regenerate via /Users/macstudio3/Projects/_shared/scripts/build-ux-primitives-bundle.sh */
/* ===== toast-notify.js ===== */
/* toast-notify.js — transient toast/notification stack. Zero dependencies.
*
* Part of the UX build-feature set (shares ux-primitives.css tokens).
*
* Toast.show({ message, title?, variant, duration?, position?, dismissible?, onClose? }) -> handle
* Toast.success(message, opts?) Toast.error(...) Toast.info(...) Toast.warn(...)
* Toast.setPosition('top-right') // persists per-viewer in localStorage
* Toast.clear() // dismiss everything
*
* variant : 'success' | 'error' | 'info' | 'warn' (default 'info')
* duration : ms before auto-dismiss; 0 = sticky (default 4500; errors 7000)
* position : 'top-right'|'top-left'|'top-center'|'bottom-right'|'bottom-left'|'bottom-center'
* handle : { close() }
*
* a11y: each position has ONE ARIA live region. success/info are polite
* (role=status), error/warn are assertive (role=alert). Auto-dismiss pauses on
* hover/focus. Manual close button is keyboard reachable and labelled.
*/
(function (global) {
var LS_KEY = 'ux:toast:pos';
var DEFAULT_POS = 'top-right';
var regions = {}; // pos -> element
var ICONS = {
success: '✓', // ✓
error: '✕', // ✕
warn: '⚠', // ⚠
info: 'ℹ' // ℹ
};
function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
function prefPos() { return lsGet(LS_KEY) || DEFAULT_POS; }
function reduceMotion() {
try { return matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (e) { return false; }
}
function region(pos) {
if (regions[pos] && document.body.contains(regions[pos])) return regions[pos];
var el = document.createElement('div');
el.className = 'ux-toast-region';
el.setAttribute('data-pos', pos);
// polite live region; assertive toasts set their own role=alert which overrides
el.setAttribute('aria-live', 'polite');
el.setAttribute('aria-atomic', 'false');
document.body.appendChild(el);
regions[pos] = el;
return el;
}
function show(opts) {
opts = opts || {};
if (typeof opts === 'string') opts = { message: opts };
var variant = opts.variant || 'info';
var pos = opts.position || prefPos();
var assertive = (variant === 'error' || variant === 'warn');
var duration = opts.duration == null
? (variant === 'error' ? 7000 : 4500)
: opts.duration;
var dismissible = opts.dismissible !== false;
var reg = region(pos);
var t = document.createElement('div');
t.className = 'ux-toast ux-toast--' + variant;
t.setAttribute('role', assertive ? 'alert' : 'status');
t.setAttribute('data-state', 'enter');
var icon = document.createElement('span');
icon.className = 'ux-toast__icon';
icon.setAttribute('aria-hidden', 'true');
icon.textContent = ICONS[variant] || ICONS.info;
t.appendChild(icon);
var body = document.createElement('div');
body.className = 'ux-toast__body';
if (opts.title) {
var h = document.createElement('div');
h.className = 'ux-toast__title';
h.textContent = opts.title;
body.appendChild(h);
}
var m = document.createElement('div');
m.className = 'ux-toast__msg';
m.textContent = opts.message == null ? '' : String(opts.message);
body.appendChild(m);
t.appendChild(body);
if (dismissible) {
var btn = document.createElement('button');
btn.className = 'ux-toast__close';
btn.type = 'button';
btn.setAttribute('aria-label', 'Dismiss notification');
btn.innerHTML = '×';
btn.addEventListener('click', function () { close(); });
t.appendChild(btn);
}
var timer = null, closed = false, remaining = duration, startedAt = 0;
var bar = null;
function clearTimer() {
if (timer) { clearTimeout(timer); timer = null; }
if (bar) { var w = 0; try { w = bar.getBoundingClientRect().width; } catch (e) {}
var pw = t.getBoundingClientRect().width || 1;
bar.style.transition = 'none';
bar.style.transform = 'scaleX(' + Math.max(0, w / pw) + ')';
}
}
function startTimer() {
if (!duration) return;
startedAt = Date.now();
timer = setTimeout(close, remaining);
if (bar && !reduceMotion()) {
// animate the progress bar to 0 over the remaining time
bar.style.transition = 'none';
bar.style.transform = 'scaleX(1)';
// force reflow so the next transition takes effect
void bar.offsetWidth;
bar.style.transition = 'transform ' + remaining + 'ms linear';
bar.style.transform = 'scaleX(0)';
}
}
function pause() {
if (!duration || closed) return;
remaining -= (Date.now() - startedAt);
clearTimer();
}
function resume() {
if (!duration || closed) return;
if (remaining <= 0) { close(); return; }
startTimer();
}
function close() {
if (closed) return;
closed = true;
if (timer) clearTimeout(timer);
t.setAttribute('data-state', 'leave');
var done = function () {
if (t.parentNode) t.parentNode.removeChild(t);
if (reg && !reg.children.length && reg.parentNode) reg.parentNode.removeChild(reg);
delete regions[pos];
try { if (typeof opts.onClose === 'function') opts.onClose(); } catch (e) {}
};
if (reduceMotion()) { done(); }
else { var t2 = setTimeout(done, 260); t.addEventListener('transitionend', function () { clearTimeout(t2); done(); }, { once: true }); }
}
if (duration) {
bar = document.createElement('div');
bar.className = 'ux-toast__bar';
t.appendChild(bar);
t.addEventListener('mouseenter', pause);
t.addEventListener('mouseleave', resume);
t.addEventListener('focusin', pause);
t.addEventListener('focusout', resume);
}
reg.appendChild(t);
// animate in
requestAnimationFrame(function () {
t.setAttribute('data-state', 'shown');
startTimer();
});
return { close: close, el: t };
}
function clear() {
Object.keys(regions).forEach(function (p) {
var r = regions[p];
if (r) Array.prototype.slice.call(r.querySelectorAll('.ux-toast__close')).forEach(function (b) { b.click(); });
});
}
function setPosition(pos) { lsSet(LS_KEY, pos); return pos; }
var API = {
show: show,
clear: clear,
setPosition: setPosition,
getPosition: prefPos,
success: function (msg, o) { return show(Object.assign({ message: msg, variant: 'success' }, o || {})); },
error: function (msg, o) { return show(Object.assign({ message: msg, variant: 'error' }, o || {})); },
info: function (msg, o) { return show(Object.assign({ message: msg, variant: 'info' }, o || {})); },
warn: function (msg, o) { return show(Object.assign({ message: msg, variant: 'warn' }, o || {})); }
};
global.Toast = API;
if (typeof module !== 'undefined' && module.exports) module.exports = API;
})(typeof window !== 'undefined' ? window : this);
/* ===== skeleton-loader.js ===== */
/* skeleton-loader.js — content-placeholder shimmer skeletons. Zero dependencies.
*
* Part of the UX build-feature set (shares ux-primitives.css tokens).
*
* Skeleton.render(mount, { variant, count?, ...opts }) // fill a container
* Skeleton.html(spec) // -> HTML string
* Skeleton.swap(mount, realNodeOrHtml) // replace skeleton, fade in
* Skeleton.wrap(mount, promise, spec) // show skeleton until promise settles
*
* variant : 'text' | 'card' | 'list-row' | 'image' | 'avatar' | 'media'
* count : repeat the block N times (default 1; text uses `lines`)
*
* a11y: the mount is marked aria-busy="true" while skeletons show; the skeleton
* nodes are aria-hidden so screen readers announce nothing until real content
* lands. swap()/wrap() clear aria-busy and fade the real content in.
*/
(function (global) {
function resolve(m) { return typeof m === 'string' ? document.querySelector(m) : m; }
function lines(n, opts) {
n = n || 3;
var out = '';
var widths = ['', '', 'ux-skel--w90', 'ux-skel--w75', 'ux-skel--w60'];
for (var i = 0; i < n; i++) {
var last = (i === n - 1);
var w = last ? 'ux-skel--w60' : (widths[i % widths.length] || '');
out += '<span class="ux-skel ux-skel--text ' + w + '"></span>';
}
return '<div class="ux-skel-lines" role="presentation">' + out + '</div>';
}
var BLOCKS = {
text: function (o) {
return (o.title ? '<span class="ux-skel ux-skel--title"></span>' : '') + lines(o.lines || 3, o);
},
image: function () { return '<span class="ux-skel ux-skel--image"></span>'; },
avatar: function () { return '<span class="ux-skel ux-skel--avatar"></span>'; },
media: function (o) {
return '<div class="ux-skel-media">'
+ '<span class="ux-skel ux-skel--thumb"></span>'
+ '<div class="ux-skel-media__body"><span class="ux-skel ux-skel--title" style="width:60%"></span>'
+ lines(o.lines || 2, o) + '</div></div>';
},
card: function (o) {
return '<div class="ux-skel-card">'
+ '<span class="ux-skel ux-skel--image"></span>'
+ '<span class="ux-skel ux-skel--title" style="margin-top:14px"></span>'
+ lines(o.lines || 2, o)
+ '<div style="display:flex;gap:8px;margin-top:12px">'
+ '<span class="ux-skel ux-skel--chip"></span><span class="ux-skel ux-skel--chip"></span></div>'
+ '</div>';
},
'list-row': function (o) {
return '<div class="ux-skel-row">'
+ '<span class="ux-skel ux-skel--thumb"></span>'
+ '<div class="ux-skel-row__body">'
+ '<span class="ux-skel ux-skel--text ux-skel--w60"></span>'
+ '<span class="ux-skel ux-skel--text ux-skel--w40"></span></div>'
+ '<span class="ux-skel ux-skel--btn"></span>'
+ '</div>';
}
};
function html(spec) {
spec = spec || {};
if (typeof spec === 'string') spec = { variant: spec };
var v = spec.variant || 'text';
var block = BLOCKS[v] || BLOCKS.text;
var count = spec.count || 1;
var out = '';
for (var i = 0; i < count; i++) out += block(spec);
return out;
}
function render(mount, spec) {
var el = resolve(mount);
if (!el) { console.warn('[skeleton-loader] mount not found:', mount); return null; }
el.setAttribute('data-ux-skel-host', '');
el.setAttribute('aria-busy', 'true');
var wrap = document.createElement('div');
wrap.setAttribute('aria-hidden', 'true');
wrap.setAttribute('data-ux-skel', '');
wrap.innerHTML = html(spec);
el.innerHTML = '';
el.appendChild(wrap);
return el;
}
function swap(mount, real) {
var el = resolve(mount);
if (!el) return null;
el.removeAttribute('aria-busy');
el.innerHTML = '';
if (real == null) return el;
if (typeof real === 'string') el.innerHTML = real;
else if (real.nodeType) el.appendChild(real);
// fade the fresh content in
var target = el.firstElementChild || el;
if (target && target.classList) {
target.classList.add('ux-skel-in');
target.addEventListener('animationend', function () { target.classList.remove('ux-skel-in'); }, { once: true });
}
return el;
}
function wrap(mount, promise, spec) {
render(mount, spec);
return Promise.resolve(promise).then(
function (v) { return v; },
function (err) { swap(mount, ''); throw err; }
);
// caller decides what to swap in on success: wrap(...).then(data => Skeleton.swap(mount, view(data)))
}
var API = { render: render, html: html, swap: swap, wrap: wrap };
global.Skeleton = API;
if (typeof module !== 'undefined' && module.exports) module.exports = API;
})(typeof window !== 'undefined' ? window : this);
/* ===== empty-state.js ===== */
/* empty-state.js — reusable empty / zero-results / error / offline block.
* Zero dependencies. Part of the UX build-feature set (shares ux-primitives.css).
*
* EmptyState.render(mount, {
* variant, // 'no-results' | 'no-data' | 'error' | 'offline' (default 'no-data')
* icon, // optional: raw SVG/HTML string OR emoji; overrides the built-in
* title, // headline (falls back to a per-variant default)
* text, // subtext (optional)
* primary, // { label, onClick } or { label, href } (optional)
* secondary // { label, onClick } or { label, href } (optional)
* }) -> element
*
* EmptyState.html(opts) // -> HTML string (for your own templating)
*
* a11y: informational variants use role="status" (aria-live polite); error/offline
* use role="alert". CTAs are real <button>/<a> (keyboard + focus ring). Built-in
* icons are aria-hidden decorative SVG.
*/
(function (global) {
function resolve(m) { return typeof m === 'string' ? document.querySelector(m) : m; }
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"');
}
// inline decorative SVGs (currentColor)
var SVG = {
'no-results': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="M21 21l-3.5-3.5"></path></svg>',
'no-data': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7l9-4 9 4-9 4-9-4z"></path><path d="M3 7v10l9 4 9-4V7"></path><path d="M12 11v10"></path></svg>',
'error': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M12 8v5"></path><path d="M12 16h.01"></path></svg>',
'offline': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18"></path><path d="M8.5 8.5a7 7 0 0 0-3 2"></path><path d="M2 8.8a11 11 0 0 1 5-3"></path><path d="M16.7 11.3A7 7 0 0 1 18.5 12.5"></path><path d="M22 8.8a11 11 0 0 0-6.3-3.2"></path><path d="M12 20h.01"></path></svg>'
};
var DEFAULT_TITLE = {
'no-results': 'No results found',
'no-data': 'Nothing here yet',
'error': 'Something went wrong',
'offline': 'You’re offline'
};
function actionHtml(a, primary) {
if (!a || !a.label) return '';
var cls = 'ux-empty__btn' + (primary ? ' ux-empty__btn--primary' : '') + ' ux-focusable';
if (a.href) return '<a class="' + cls + '" href="' + esc(a.href) + '">' + esc(a.label) + '</a>';
return '<button type="button" class="' + cls + '" data-ux-empty-act="' + (primary ? 'primary' : 'secondary') + '">' + esc(a.label) + '</button>';
}
function html(o) {
o = o || {};
var v = o.variant || 'no-data';
var icon = o.icon != null ? o.icon : (SVG[v] || SVG['no-data']);
var title = o.title != null ? o.title : (DEFAULT_TITLE[v] || DEFAULT_TITLE['no-data']);
var actions = actionHtml(o.primary, true) + actionHtml(o.secondary, false);
return '<div class="ux-empty__icon">' + icon + '</div>'
+ '<p class="ux-empty__title">' + esc(title) + '</p>'
+ (o.text ? '<p class="ux-empty__text">' + esc(o.text) + '</p>' : '')
+ (actions ? '<div class="ux-empty__actions">' + actions + '</div>' : '');
}
function render(mount, o) {
o = o || {};
var el = resolve(mount);
if (!el) { console.warn('[empty-state] mount not found:', mount); return null; }
var v = o.variant || 'no-data';
var alertish = (v === 'error' || v === 'offline');
el.classList.add('ux-empty', 'ux-empty--' + v);
el.setAttribute('role', alertish ? 'alert' : 'status');
el.setAttribute('aria-live', alertish ? 'assertive' : 'polite');
el.innerHTML = html(o);
// wire button callbacks (links navigate on their own)
if (o.primary && typeof o.primary.onClick === 'function') {
var pb = el.querySelector('[data-ux-empty-act="primary"]');
if (pb) pb.addEventListener('click', o.primary.onClick);
}
if (o.secondary && typeof o.secondary.onClick === 'function') {
var sb = el.querySelector('[data-ux-empty-act="secondary"]');
if (sb) sb.addEventListener('click', o.secondary.onClick);
}
return el;
}
var API = { render: render, html: html };
global.EmptyState = API;
if (typeof module !== 'undefined' && module.exports) module.exports = API;
})(typeof window !== 'undefined' ? window : this);
/* ===== copy-to-clipboard.js ===== */
/* copy-to-clipboard.js — one-liner copy behavior. Zero dependencies.
*
* Part of the UX build-feature set (shares ux-primitives.css tokens). Integrates
* with toast-notify if Toast is present, otherwise shows inline feedback.
*
* Copy.copy(text) // -> Promise<boolean> (clipboard API + execCommand fallback)
* Copy.attach(el, opts) // wire one trigger element
* Copy.init(root=document) // auto-wire every [data-copy] under root
*
* Data-attribute auto-wiring (no JS needed beyond loading this file):
* <button data-copy="DWK-32014">Copy SKU</button>
* <button data-copy data-copy-target="#snippet">Copy code</button> <!-- copies that node's text/value -->
* optional: data-copy-feedback="toast|inline|both" data-copy-label="Copy" data-copy-copied="Copied!"
*
* a11y: feedback is announced via an aria-live="polite" region; the button keeps
* an aria-label; the icon is decorative.
*/
(function (global) {
var COPY_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15V5a2 2 0 0 1 2-2h10"></path></svg>';
var liveRegion = null;
function announce(msg) {
if (!liveRegion) {
liveRegion = document.createElement('div');
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('role', 'status');
liveRegion.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;';
document.body.appendChild(liveRegion);
}
liveRegion.textContent = '';
// reassign next tick so repeat messages are re-announced
setTimeout(function () { liveRegion.textContent = msg; }, 30);
}
function copy(text) {
text = text == null ? '' : String(text);
// modern path (needs secure context + focus; may reject)
if (global.navigator && navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text).then(function () { return true; }, function () { return legacy(text); });
}
return Promise.resolve(legacy(text));
}
function legacy(text) {
try {
var ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none;';
document.body.appendChild(ta);
ta.select();
ta.setSelectionRange(0, text.length);
var ok = document.execCommand('copy');
document.body.removeChild(ta);
return !!ok;
} catch (e) { return false; }
}
function targetText(el) {
// explicit literal wins
var lit = el.getAttribute('data-copy');
if (lit) return lit;
var sel = el.getAttribute('data-copy-target');
if (sel) {
var t = document.querySelector(sel);
if (t) return ('value' in t && t.value != null && t.tagName !== undefined && /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)) ? t.value : t.textContent.trim();
}
return '';
}
function pill(el, msg, isError) {
var p = document.createElement('span');
p.className = 'ux-copy-pill' + (isError ? ' is-error' : '');
p.textContent = msg;
p.setAttribute('aria-hidden', 'true');
// anchor relative to the trigger
var host = el;
var cs = global.getComputedStyle ? getComputedStyle(host) : null;
if (cs && cs.position === 'static') host.style.position = 'relative';
host.appendChild(p);
var r = el.getBoundingClientRect();
p.style.left = (el.offsetWidth / 2) + 'px';
p.style.top = '0px';
requestAnimationFrame(function () { p.setAttribute('data-show', 'true'); });
setTimeout(function () {
p.setAttribute('data-show', 'false');
setTimeout(function () { if (p.parentNode) p.parentNode.removeChild(p); }, 200);
}, 1200);
}
function feedback(el, mode, ok, copiedLabel) {
var msg = ok ? (copiedLabel || 'Copied!') : 'Copy failed';
announce(msg);
if (mode === 'toast' || mode === 'both') {
if (global.Toast) { ok ? Toast.success(msg) : Toast.error(msg); }
else if (mode === 'toast') { pill(el, msg, !ok); } // graceful fallback if Toast absent
}
if (mode === 'inline' || mode === 'both') {
el.classList.add(ok ? 'is-copied' : 'is-error');
pill(el, msg, !ok);
setTimeout(function () { el.classList.remove('is-copied', 'is-error'); }, 1300);
}
}
function attach(el, opts) {
if (!el || el.__uxCopyBound) return el;
opts = opts || {};
el.__uxCopyBound = true;
// decorate a bare .ux-copy button with an icon + label if empty
if (el.classList && el.classList.contains('ux-copy') && !el.children.length && !el.textContent.trim()) {
el.innerHTML = '<span class="ux-copy__ic">' + COPY_SVG + '</span><span>' + (opts.label || el.getAttribute('data-copy-label') || 'Copy') + '</span>';
}
if (el.tagName === 'BUTTON' && !el.getAttribute('type')) el.setAttribute('type', 'button');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', opts.label || el.getAttribute('data-copy-label') || 'Copy to clipboard');
var mode = opts.feedback || el.getAttribute('data-copy-feedback') || (global.Toast ? 'toast' : 'inline');
var copiedLabel = opts.copiedLabel || el.getAttribute('data-copy-copied') || 'Copied!';
el.addEventListener('click', function () {
var text = (typeof opts.text === 'function') ? opts.text(el)
: (opts.text != null ? opts.text : targetText(el));
copy(text).then(function (ok) {
feedback(el, mode, ok, copiedLabel);
try { if (typeof opts.onCopy === 'function') opts.onCopy(ok, text, el); } catch (e) {}
});
});
return el;
}
function init(root) {
root = root || document;
var els = root.querySelectorAll('[data-copy], [data-copy-target]');
Array.prototype.forEach.call(els, function (el) { attach(el); });
return els.length;
}
var API = { copy: copy, attach: attach, init: init };
global.Copy = API;
if (typeof module !== 'undefined' && module.exports) module.exports = API;
// auto-wire declarative triggers once the DOM is ready
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { init(document); });
else init(document);
}
})(typeof window !== 'undefined' ? window : this);
/* ===== command-palette.js ===== */
/* command-palette.js — cmd-k / ctrl-k command palette. Zero dependencies.
*
* Part of the UX build-feature set (shares ux-primitives.css tokens).
*
* var palette = CommandPalette.create({
* commands: [
* { id:'new', title:'New collection', section:'Actions', subtitle:'Create a curated set',
* keywords:'add create', icon:'⊕', hint:'C', run(){ ... } },
* ...
* ],
* placeholder: 'Type a command…',
* hotkey: true, // bind Cmd/Ctrl+K globally (default true)
* recentKey: 'ux:cmdk:recent', // localStorage key for recents (default this)
* maxRecent: 5,
* onSelect: cmd => {} // optional; runs after cmd.run()
* });
* palette.open(); palette.close(); palette.setCommands([...]); palette.destroy();
*
* Fuzzy subsequence filter over title + subtitle + keywords + section. Keyboard:
* up/down (wrap), Enter runs the selected command, Esc closes. Recents float to a
* "Recent" group when the query is empty. ARIA combobox: input role=combobox with
* aria-expanded/aria-controls/aria-activedescendant; results are a role=listbox of
* role=option. Focus is trapped in the dialog and restored to the prior element on
* close.
*/
(function (global) {
var uid = 0;
function lsGet(k) { try { return JSON.parse(localStorage.getItem(k) || '[]'); } catch (e) { return []; } }
function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} }
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"');
}
// subsequence fuzzy match; returns a score (higher = better) or -1 for no match
function fuzzy(needle, hay) {
needle = needle.toLowerCase(); hay = hay.toLowerCase();
if (!needle) return 0;
var n = 0, score = 0, streak = 0, lastIdx = -1;
for (var i = 0; i < hay.length && n < needle.length; i++) {
if (hay[i] === needle[n]) {
score += 1 + streak; // reward consecutive hits
if (lastIdx === i - 1) streak++; else streak = 1;
if (i === 0 || /\s|[-_/]/.test(hay[i - 1])) score += 3; // word-boundary bonus
lastIdx = i; n++;
}
}
return n === needle.length ? score : -1;
}
var SEARCH_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="M21 21l-3.5-3.5"></path></svg>';
function create(cfg) {
cfg = cfg || {};
var commands = (cfg.commands || []).slice();
var recentKey = cfg.recentKey || 'ux:cmdk:recent';
var maxRecent = cfg.maxRecent || 5;
var listId = 'ux-cmdk-list-' + (++uid);
var backdrop = document.createElement('div');
backdrop.className = 'ux-cmdk-backdrop';
backdrop.setAttribute('data-open', 'false');
backdrop.hidden = true;
backdrop.innerHTML =
'<div class="ux-cmdk" role="dialog" aria-modal="true" aria-label="Command palette">'
+ '<div class="ux-cmdk__head">'
+ '<span class="ux-cmdk__search-ic">' + SEARCH_SVG + '</span>'
+ '<input class="ux-cmdk__input" type="text" role="combobox" autocomplete="off" '
+ 'spellcheck="false" aria-expanded="true" aria-controls="' + listId + '" '
+ 'aria-autocomplete="list" placeholder="' + esc(cfg.placeholder || 'Type a command…') + '">'
+ '<span class="ux-cmdk__kbd">esc</span>'
+ '</div>'
+ '<ul class="ux-cmdk__list" id="' + listId + '" role="listbox" aria-label="Commands"></ul>'
+ '</div>';
var panel = backdrop.querySelector('.ux-cmdk');
var input = backdrop.querySelector('.ux-cmdk__input');
var list = backdrop.querySelector('.ux-cmdk__list');
var results = []; // flat, in display order (excludes group headers)
var selected = 0;
var lastFocus = null;
var mounted = false;
function recents() { return lsGet(recentKey); }
function pushRecent(id) {
var r = recents().filter(function (x) { return x !== id; });
r.unshift(id); r = r.slice(0, maxRecent); lsSet(recentKey, r);
}
function compute(q) {
q = (q || '').trim();
if (!q) {
// empty query: Recent group (if any) then everything by section
var recIds = recents();
var recCmds = recIds.map(function (id) {
return commands.filter(function (c) { return c.id === id; })[0];
}).filter(Boolean);
var groups = [];
if (recCmds.length) groups.push({ name: 'Recent', items: recCmds });
bySection(commands).forEach(function (g) { groups.push(g); });
return groups;
}
var scored = commands.map(function (c) {
var hay = [c.title, c.subtitle, c.keywords, c.section].filter(Boolean).join(' ');
return { c: c, s: fuzzy(q, hay) };
}).filter(function (x) { return x.s >= 0; })
.sort(function (a, b) { return b.s - a.s; })
.map(function (x) { return x.c; });
return scored.length ? [{ name: 'Results', items: scored }] : [];
}
function bySection(cmds) {
var order = [], map = {};
cmds.forEach(function (c) {
var s = c.section || 'Commands';
if (!map[s]) { map[s] = { name: s, items: [] }; order.push(map[s]); }
map[s].items.push(c);
});
return order;
}
function draw(q) {
var groups = compute(q);
results = [];
if (!groups.length) {
list.innerHTML = '<li class="ux-cmdk__empty" role="presentation">No matching commands</li>';
input.setAttribute('aria-activedescendant', '');
return;
}
var html = '';
groups.forEach(function (g) {
html += '<li class="ux-cmdk__group" role="presentation">' + esc(g.name) + '</li>';
g.items.forEach(function (c) {
var idx = results.length;
results.push(c);
var oid = 'ux-cmdk-opt-' + uid + '-' + idx;
html += '<li class="ux-cmdk__opt" role="option" id="' + oid + '" data-i="' + idx + '" aria-selected="false">'
+ (c.icon ? '<span class="ux-cmdk__ic" aria-hidden="true">' + esc(c.icon) + '</span>' : '<span class="ux-cmdk__ic"></span>')
+ '<span class="ux-cmdk__opt-body"><span class="ux-cmdk__title">' + esc(c.title) + '</span>'
+ (c.subtitle ? '<span class="ux-cmdk__sub">' + esc(c.subtitle) + '</span>' : '') + '</span>'
+ (c.hint ? '<span class="ux-cmdk__hint">' + esc(c.hint) + '</span>' : '')
+ '</li>';
});
});
list.innerHTML = html;
selected = 0;
highlight();
}
function optEls() { return list.querySelectorAll('.ux-cmdk__opt'); }
function highlight() {
var els = optEls();
els.forEach(function (el, i) {
var on = (i === selected);
el.setAttribute('aria-selected', on ? 'true' : 'false');
if (on) {
input.setAttribute('aria-activedescendant', el.id);
el.scrollIntoView({ block: 'nearest' });
}
});
if (!els.length) input.setAttribute('aria-activedescendant', '');
}
function move(delta) {
var n = results.length;
if (!n) return;
selected = (selected + delta + n) % n;
highlight();
}
function run(i) {
var c = results[i];
if (!c) return;
pushRecent(c.id);
close();
try { if (typeof c.run === 'function') c.run(c); } catch (e) { console.error('[command-palette] run failed', e); }
if (typeof cfg.onSelect === 'function') cfg.onSelect(c);
}
function onKey(e) {
if (e.key === 'ArrowDown') { e.preventDefault(); move(1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); }
else if (e.key === 'Enter') { e.preventDefault(); run(selected); }
else if (e.key === 'Escape') { e.preventDefault(); close(); }
else if (e.key === 'Tab') { e.preventDefault(); move(e.shiftKey ? -1 : 1); } // trap: cycle options
}
function onListClick(e) {
var opt = e.target.closest('.ux-cmdk__opt');
if (opt) run(+opt.dataset.i);
}
function onListMove(e) {
var opt = e.target.closest('.ux-cmdk__opt');
if (opt) { selected = +opt.dataset.i; highlight(); }
}
function onBackdrop(e) { if (e.target === backdrop) close(); }
function mount() {
if (mounted) return;
document.body.appendChild(backdrop);
input.addEventListener('input', function () { draw(input.value); });
input.addEventListener('keydown', onKey);
list.addEventListener('click', onListClick);
list.addEventListener('mousemove', onListMove);
backdrop.addEventListener('mousedown', onBackdrop);
mounted = true;
}
function open() {
mount();
lastFocus = document.activeElement;
backdrop.hidden = false;
input.value = '';
draw('');
requestAnimationFrame(function () {
backdrop.setAttribute('data-open', 'true');
input.focus();
});
}
function close() {
backdrop.setAttribute('data-open', 'false');
var done = function () { backdrop.hidden = true; };
var t = setTimeout(done, 180);
backdrop.addEventListener('transitionend', function () { clearTimeout(t); done(); }, { once: true });
if (lastFocus && lastFocus.focus) { try { lastFocus.focus(); } catch (e) {} }
}
function toggle() { (backdrop.hidden) ? open() : close(); }
// global hotkey Cmd/Ctrl+K
var hotkeyHandler = null;
if (cfg.hotkey !== false) {
hotkeyHandler = function (e) {
if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
e.preventDefault(); toggle();
}
};
document.addEventListener('keydown', hotkeyHandler);
}
function setCommands(next) { commands = (next || []).slice(); if (!backdrop.hidden) draw(input.value); }
function destroy() {
if (hotkeyHandler) document.removeEventListener('keydown', hotkeyHandler);
if (backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
mounted = false;
}
return { open: open, close: close, toggle: toggle, setCommands: setCommands, destroy: destroy, el: backdrop };
}
var API = { create: create, fuzzy: fuzzy };
global.CommandPalette = API;
if (typeof module !== 'undefined' && module.exports) module.exports = API;
})(typeof window !== 'undefined' ? window : this);
/* ===== modal-rig.js ===== */
/*!
* modal-rig.js — make any modal/panel "fully done": drag-to-rearrange,
* stretch/resize, collapsible sections, maximize, all persisted to localStorage.
* Vanilla JS, zero deps, framework-agnostic. Exposes window.ModalRig.
*
* Part of the UX build-feature set (themes via ux-primitives.css + modal-rig.css;
* still renders correctly with neither loaded — structural styles are inlined).
*
* ModalRig.rig(panelEl, { key:'mynote' }) // enhance an existing panel
* ModalRig.section(headerEl, bodyEl, 'mynote:links') // collapsible section
* var m = ModalRig.open({ key:'mynote', title:'Note', bodyHtml:'<p>…</p>' })
* ModalRig.init(root=document) // wire declarative hooks (idempotent)
*
* Declarative auto-wiring (no JS needed beyond loading this file):
* <div data-ux-modal-rig data-ux-modal-key="panel1"> … </div> // rig an in-DOM panel
* <button data-ux-modal-open="#tmpl" data-ux-modal-title="Details">Open</button>
* where #tmpl is a <template> (or any element) whose innerHTML becomes the body.
*/
(function (global) {
function lsGet(k) { try { return JSON.parse(localStorage.getItem(k) || 'null'); } catch (e) { return null; } }
function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} }
function px(n) { return Math.round(n) + 'px'; }
function applyRect(panel, key, opts) {
var w = opts.width || 760;
var h = opts.height || Math.min(620, Math.round(window.innerHeight * 0.8));
var r = lsGet('modalrig:' + key);
if (r && r.w) {
panel.style.left = px(r.left || 0); panel.style.top = px(r.top || 0);
panel.style.width = px(r.w); panel.style.height = px(r.h);
} else {
panel.style.left = px(Math.max(0, (window.innerWidth - w) / 2));
panel.style.top = px(Math.max(0, (window.innerHeight - h) / 2));
panel.style.width = px(w); panel.style.height = px(h);
}
}
function saveRect(panel, key) {
if (panel.getAttribute('data-rig-max') === '1') return; // don't persist the maximized state
lsSet('modalrig:' + key, {
left: parseInt(panel.style.left) || 0, top: parseInt(panel.style.top) || 0,
w: panel.offsetWidth, h: panel.offsetHeight
});
}
function startDrag(e, panel, key) {
if (e.target.closest && e.target.closest('[data-rig-btn]')) return; // not when clicking header buttons
if (panel.getAttribute('data-rig-max') === '1') unmaximize(panel, key); // dragging exits maximize
var box = panel.getBoundingClientRect();
var dx = e.clientX - box.left, dy = e.clientY - box.top;
function move(ev) {
panel.style.left = px(Math.max(0, Math.min(window.innerWidth - 80, ev.clientX - dx)));
panel.style.top = px(Math.max(0, Math.min(window.innerHeight - 40, ev.clientY - dy)));
}
function up() { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); saveRect(panel, key); }
document.addEventListener('mousemove', move); document.addEventListener('mouseup', up);
e.preventDefault();
}
function maximize(panel) {
panel.setAttribute('data-rig-pre', JSON.stringify({ left: panel.style.left, top: panel.style.top, w: panel.style.width, h: panel.style.height }));
panel.setAttribute('data-rig-max', '1');
panel.style.left = '2vw'; panel.style.top = '2vh'; panel.style.width = '96vw'; panel.style.height = '96vh';
}
function unmaximize(panel, key) {
var pre = null;
try { pre = JSON.parse(panel.getAttribute('data-rig-pre') || 'null'); } catch (e) { pre = null; }
panel.removeAttribute('data-rig-max');
if (pre) { panel.style.left = pre.left; panel.style.top = pre.top; panel.style.width = pre.w; panel.style.height = pre.h; }
}
function toggleMax(panel, key, btn) {
if (panel.getAttribute('data-rig-max') === '1') { unmaximize(panel, key); if (btn) btn.textContent = '⤢'; }
else { maximize(panel); if (btn) btn.textContent = '⤡'; }
}
function injectMaxBtn(handle, panel, key) {
if (handle.querySelector('[data-rig-max-btn]')) return;
var b = document.createElement('button');
b.type = 'button';
b.className = 'ux-modal__btn ux-modal__max';
b.setAttribute('data-rig-btn', '1'); b.setAttribute('data-rig-max-btn', '1');
b.title = 'maximize / restore'; b.textContent = '⤢';
b.addEventListener('click', function (e) { e.stopPropagation(); toggleMax(panel, key, b); });
handle.appendChild(b);
}
// Enhance an already-in-DOM panel. Idempotent per panel (won't double-bind).
function rig(panel, opts) {
if (!panel || panel.__uxRigBound) return panel;
opts = opts || {};
var key = opts.key || 'default';
panel.__uxRigBound = true;
panel.classList.add('ux-modal');
panel.style.position = 'absolute';
if (opts.resizable !== false) { panel.style.resize = 'both'; if (!panel.style.overflow) panel.style.overflow = 'hidden'; }
applyRect(panel, key, opts);
var handle = typeof opts.handle === 'string' ? panel.querySelector(opts.handle) : (opts.handle || panel.firstElementChild);
if (handle) {
handle.style.cursor = 'move'; handle.style.userSelect = 'none';
handle.addEventListener('mousedown', function (e) { startDrag(e, panel, key); });
if (opts.maximizable !== false) injectMaxBtn(handle, panel, key);
}
panel.addEventListener('mouseup', function () { saveRect(panel, key); });
if (opts.onResize && window.ResizeObserver) {
var ro = new ResizeObserver(function () { try { opts.onResize(panel); } catch (e) {} });
ro.observe(panel);
}
return panel;
}
// Collapsible section: clicking headerEl toggles bodyEl; state persists under key.
function section(headerEl, bodyEl, key) {
if (!headerEl || !bodyEl || headerEl.__uxRigSection) return;
headerEl.__uxRigSection = true;
var collapsed = lsGet('modalrig-sec:' + key) === true;
var caret = document.createElement('span');
caret.textContent = collapsed ? '▸ ' : '▾ ';
headerEl.insertBefore(caret, headerEl.firstChild);
headerEl.style.cursor = 'pointer'; headerEl.style.userSelect = 'none';
bodyEl.style.display = collapsed ? 'none' : '';
headerEl.addEventListener('click', function () {
var nowOpen = bodyEl.style.display === 'none';
bodyEl.style.display = nowOpen ? '' : 'none';
caret.textContent = nowOpen ? '▾ ' : '▸ ';
lsSet('modalrig-sec:' + key, !nowOpen);
});
}
// Build a complete modal (backdrop + panel + header) and rig it. Returns
// { backdrop, panel, body, close }. Closes on backdrop click, ×, and Esc.
function open(o) {
o = o || {};
var key = o.key || 'modal';
var old = document.getElementById('modalrig-bd-' + key); if (old) old.remove();
var bd = document.createElement('div');
bd.id = 'modalrig-bd-' + key;
bd.className = 'ux-modal-backdrop';
// structural fallback so it works with no CSS loaded:
bd.style.cssText = 'position:fixed;inset:0;z-index:' + (o.z || 'var(--ux-z-palette,2147482000)') + ';background:rgba(0,0,0,.5);';
var panel = document.createElement('div');
panel.className = 'ux-modal';
panel.style.cssText = 'display:flex;flex-direction:column;min-width:340px;min-height:180px;';
panel.addEventListener('click', function (e) { e.stopPropagation(); });
var head = document.createElement('div');
head.className = 'ux-modal__head';
head.style.cssText = 'flex:0 0 auto;display:flex;justify-content:space-between;align-items:center;gap:8px;';
var title = document.createElement('span');
title.className = 'ux-modal__title';
title.textContent = o.title || '';
var btns = document.createElement('span'); btns.className = 'ux-modal__btns'; btns.style.cssText = 'flex:0 0 auto;display:flex;align-items:center;';
var x = document.createElement('button');
x.type = 'button'; x.className = 'ux-modal__btn ux-modal__close';
x.setAttribute('data-rig-btn', '1'); x.textContent = '×'; x.title = 'close (Esc)';
x.setAttribute('aria-label', 'Close');
var body = document.createElement('div');
body.className = 'ux-modal__body';
body.style.cssText = 'flex:1 1 auto;overflow:auto;';
if (o.bodyHtml != null) body.innerHTML = o.bodyHtml; else if (o.bodyEl) body.appendChild(o.bodyEl);
btns.appendChild(x); head.appendChild(title); head.appendChild(btns);
panel.appendChild(head); panel.appendChild(body); bd.appendChild(panel); document.body.appendChild(bd);
function close() { bd.remove(); if (o.onClose) o.onClose(); }
bd.addEventListener('click', close); x.addEventListener('click', close);
if (!window.__modalRigEsc) { window.__modalRigEsc = true; document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { var t = document.querySelector('[id^="modalrig-bd-"]'); if (t) t.remove(); } }); }
rig(panel, { key: key, handle: head, resizable: o.resizable, maximizable: o.maximizable, width: o.width, height: o.height, onResize: o.onResize });
return { backdrop: bd, panel: panel, body: body, close: close };
}
// Declarative wiring — idempotent; safe to call on a page that uses none.
function init(root) {
root = root || (typeof document !== 'undefined' ? document : null);
if (!root) return 0;
var n = 0;
Array.prototype.forEach.call(root.querySelectorAll('[data-ux-modal-rig]'), function (el) {
if (el.__uxRigBound) return;
rig(el, { key: el.getAttribute('data-ux-modal-key') || ('rig-' + (++n)) });
});
Array.prototype.forEach.call(root.querySelectorAll('[data-ux-modal-open]'), function (btn) {
if (btn.__uxRigOpenBound) return;
btn.__uxRigOpenBound = true;
btn.addEventListener('click', function () {
var sel = btn.getAttribute('data-ux-modal-open');
var src = sel ? document.querySelector(sel) : null;
var html = src ? (src.tagName === 'TEMPLATE' ? src.innerHTML : src.innerHTML) : '';
open({ key: btn.getAttribute('data-ux-modal-key') || sel || 'modal', title: btn.getAttribute('data-ux-modal-title') || '', bodyHtml: html });
});
});
return n;
}
global.ModalRig = { rig: rig, section: section, open: open, init: init };
if (typeof module !== 'undefined' && module.exports) module.exports = global.ModalRig;
})(typeof window !== 'undefined' ? window : this);
/* ===== bento-grid.js ===== */
/* ============================================================================
bento-grid.js — editorial "bento box" tile layout. Zero dependencies.
Part of the UX build-feature set (IIFE window.Bento, shares ux-primitives.css).
Converted from the original ES-module so it concatenates cleanly into the
ux-primitives.bundle.js classic script alongside the other primitives.
Bento.render(container, items, opts) // render tiles from a data array
Bento.tile(item, i) // build ONE tile element
Bento.init(root=document) // auto-wire [data-ux-bento] containers
// whose JSON items live in a
// <script type="application/json">
Each item:
{ span?, variant?, eyebrow?, title?, desc?, cta?, image?, stat?, label?,
href?, accent?, bg? }
Markup is namespaced .ux-bento / .ux-bento-item so it can't clobber site CSS.
============================================================================ */
(function (global) {
var NAMED = { feature: 'is-accent', stat: 'is-stat', accent: 'is-accent', plain: '' };
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"');
}
function autoSpan(item, i) {
if (item.span) return item.span;
if (item.variant === 'feature') return '2x2';
if (item.variant === 'stat') return '1x1';
if (i % 5 === 0) return '2x1';
return '1x1';
}
function tile(item, i) {
item = item || {}; i = i || 0;
var span = autoSpan(item, i);
var el = document.createElement(item.href ? 'a' : 'div');
el.className = 'ux-bento-item ' + (NAMED[item.variant] || '');
if (span && span !== '1x1') el.setAttribute('data-span', span);
if (item.href) { el.href = item.href; el.setAttribute('data-href', item.href); }
if (item.image) { el.classList.add('has-image'); el.style.setProperty('--ux-bento-img', 'url("' + item.image + '")'); }
if (item.accent) el.style.setProperty('--ux-bento-accent', item.accent);
if (item.bg) el.style.setProperty('--ux-bento-bg', item.bg);
var html = '';
if (item.variant === 'stat') {
html += '<div class="stat">' + esc(item.stat) + '</div>';
if (item.label) html += '<div class="label">' + esc(item.label) + '</div>';
} else {
if (item.eyebrow) html += '<div class="eyebrow">' + esc(item.eyebrow) + '</div>';
if (item.title) html += '<p class="title">' + esc(item.title) + '</p>';
if (item.desc) html += '<p class="desc">' + esc(item.desc) + '</p>';
if (item.cta) html += '<div class="cta">' + esc(item.cta) + ' ›</div>';
}
el.innerHTML = html;
return el;
}
function render(container, items, opts) {
if (!container) { console.warn('[bento-grid] container required'); return null; }
items = items || []; opts = opts || {};
container.classList.add('ux-bento');
container.innerHTML = '';
if (opts.vars) Object.keys(opts.vars).forEach(function (k) { container.style.setProperty(k, opts.vars[k]); });
var frag = document.createDocumentFragment();
items.forEach(function (item, i) { var t = tile(item, i); t._item = item; frag.appendChild(t); });
container.appendChild(frag);
var onClick = null;
if (typeof opts.onTileClick === 'function') {
onClick = function (ev) {
var t = ev.target.closest('.ux-bento-item');
if (!t || !container.contains(t)) return;
if (opts.onTileClick(t._item, ev) === false) ev.preventDefault();
};
container.addEventListener('click', onClick);
}
return {
el: container, items: items,
destroy: function () {
if (onClick) container.removeEventListener('click', onClick);
container.innerHTML = ''; container.classList.remove('ux-bento');
}
};
}
// Declarative: <div data-ux-bento><script type="application/json">[ … ]</script></div>
// Idempotent; no-ops if nothing on the page uses it.
function init(root) {
root = root || (typeof document !== 'undefined' ? document : null);
if (!root) return 0;
var n = 0;
Array.prototype.forEach.call(root.querySelectorAll('[data-ux-bento]'), function (el) {
if (el.__uxBentoBound) return;
el.__uxBentoBound = true;
var src = el.querySelector('script[type="application/json"]');
if (!src) return;
var items = [];
try { items = JSON.parse(src.textContent || '[]'); } catch (e) { console.warn('[bento-grid] bad JSON', e); return; }
render(el, items);
n++;
});
return n;
}
global.Bento = { render: render, tile: tile, init: init };
if (typeof module !== 'undefined' && module.exports) module.exports = global.Bento;
})(typeof window !== 'undefined' ? window : this);
/* ===== ux-grid-lifecycle.js (window.UXGrid — safe skeleton+empty controller) ===== */
/* ux-grid-lifecycle.js — lifecycle-aware grid activation layer over Skeleton +
* EmptyState. Zero dependencies (uses window.Skeleton/EmptyState if present, else
* falls back gracefully). Part of the UX build-feature set. Exposes window.UXGrid.
*
* THE PROBLEM THIS SOLVES (proven by the TK-12031 negative-control):
* The DW product grid clears #grid then ASYNC-appends products. A plain
* overwrite-on-init skeleton persists ABOVE the products (ghost regression), and a
* plain empty-state flashes during the loading gap. This controller fixes both:
*
* UXGrid.init(container, opts) — FIRST-LOAD-ONLY, idempotent per container:
* 1. If the container already has real (non-skeleton/non-empty) children →
* it's server-rendered → do NOTHING.
* 2. Else append skeleton placeholders (does NOT clear the container) and attach
* a MutationObserver on the container's childList.
* 3. When the FIRST real product child appears → remove ALL skeletons + any
* empty-state, mark populated, DISCONNECT the observer (no leak).
* 4. skeletonTimeout (default 8s): clear skeletons regardless (never leave them
* stuck); if still zero real children → show empty-state ONCE (true zero, not
* the loading gap). The observer stays until a late product arrives or hardCap.
* 5. hardCap (default 15s): disconnect the observer unconditionally (leak guard).
*
* FIRST-LOAD-ONLY: after populate/cap the observer is disconnected and a done flag
* set, so a later sort/density re-render (clear+refill) is NEVER touched — no ghost
* skeleton re-flash, no empty-state flicker. Skeleton is injected at most once
* (guarded); empty-state is shown at most once (guarded).
*
* Test seam: window.__uxGridTimeout (ms) overrides skeletonTimeout for the negative-
* control ONLY (a page/plist must never set it). data-ux-grid-timeout does the same
* declaratively if a site ever needs a custom window.
*/
(function (global) {
var doc = global.document;
if (!doc) return;
function isSkel(n) {
return n.nodeType === 1 && (
(n.matches && n.matches('[data-ux-skel-holder],[data-ux-skel]')) ||
(n.classList && n.classList.contains('ux-skel'))
);
}
function isEmpty(n) { return n.nodeType === 1 && n.matches && n.matches('[data-ux-empty-holder]'); }
function realCount(c) {
var n = 0, k = c.children;
for (var i = 0; i < k.length; i++) { if (!isSkel(k[i]) && !isEmpty(k[i])) n++; }
return n;
}
function clearSkel(c) {
var s = c.querySelectorAll('[data-ux-skel-holder],[data-ux-skel]');
Array.prototype.forEach.call(s, function (n) { if (n.parentNode) n.parentNode.removeChild(n); });
if (c.removeAttribute) c.removeAttribute('aria-busy');
}
function clearEmpty(c) {
var e = c.querySelectorAll('[data-ux-empty-holder]');
Array.prototype.forEach.call(e, function (n) { if (n.parentNode) n.parentNode.removeChild(n); });
}
function showSkel(c, variant, count) {
if (c.querySelector('[data-ux-skel-holder]')) return;
var holder = doc.createElement('div');
holder.setAttribute('data-ux-skel-holder', '');
holder.setAttribute('aria-hidden', 'true');
if (global.Skeleton && Skeleton.html) {
try { holder.innerHTML = Skeleton.html({ variant: variant || 'card', count: count || 8 }); } catch (e) {}
}
c.setAttribute('aria-busy', 'true');
c.appendChild(holder);
}
function showEmpty(c, opts) {
if (c.querySelector('[data-ux-empty-holder]')) return;
var holder = doc.createElement('div');
holder.setAttribute('data-ux-empty-holder', '');
c.appendChild(holder);
if (global.EmptyState && EmptyState.render) {
try { EmptyState.render(holder, opts || { variant: 'no-results' }); } catch (e) { holder.textContent = (opts && opts.title) || 'No results found'; }
} else { holder.textContent = (opts && opts.title) || 'No results found'; }
}
function init(c, opts) {
if (!c || c.__uxGrid) return; c.__uxGrid = true;
opts = opts || {};
var variant = opts.variant || c.getAttribute('data-ux-grid-skeleton') || 'card';
var count = parseInt(opts.count || c.getAttribute('data-ux-grid-count') || '8', 10) || 8;
var wantEmpty = (opts.empty !== false) && (c.getAttribute('data-ux-grid-empty') !== 'off');
var toAttr = parseInt(c.getAttribute('data-ux-grid-timeout') || '0', 10);
var skelTimeout = (typeof global.__uxGridTimeout === 'number' ? global.__uxGridTimeout : 0) || opts.skeletonTimeout || toAttr || 8000;
// hardCap is a LEAK GUARD only — it may disconnect the observer ONLY when no empty-state
// is being displayed (see below). It must NEVER tear down the observer while an empty-state
// banner is up, or a late-arriving product would be stranded UNDER a permanent "No results".
var hardCap = (typeof global.__uxGridHardCap === 'number' ? global.__uxGridHardCap : 0) || opts.hardCap || Math.max(15000, skelTimeout + 6000);
var emptyOpts = {
variant: 'no-results',
title: opts.emptyTitle || c.getAttribute('data-ux-grid-empty-title') || undefined,
text: opts.emptyText || c.getAttribute('data-ux-grid-empty-text') || undefined
};
// already populated (server-rendered) → leave it alone
if (realCount(c) > 0) { c.__uxGridDone = true; return; }
showSkel(c, variant, count);
var done = false, emptyShown = false, obs = null, tSkel = null, tCap = null;
function finish() {
if (done) return; done = true;
clearSkel(c); // self-contained: never leave a stuck skeleton if finish() fires before tSkel (e.g. hardCap < skelTimeout)
try { if (obs) obs.disconnect(); } catch (e) {}
if (tSkel) clearTimeout(tSkel);
if (tCap) clearTimeout(tCap);
if (c.removeAttribute) c.removeAttribute('aria-busy');
c.__uxGridDone = true;
}
// The observer is the SINGLE source of truth for "products arrived" and lives until they
// do. Products appearing at ANY time (before the skeleton timeout, during an empty-state
// banner, or long after hardCap) always clear skeleton + empty-state and finish. This is
// what makes empty-state safe: it can NEVER remain stranded over a populated grid.
try {
obs = new MutationObserver(function () {
if (realCount(c) > 0) { clearSkel(c); clearEmpty(c); finish(); }
});
obs.observe(c, { childList: true });
} catch (e) { /* no MutationObserver → timeout path still clears the skeleton */ }
tSkel = setTimeout(function () {
clearSkel(c); // never leave a skeleton stuck
if (realCount(c) === 0 && wantEmpty && !emptyShown) { showEmpty(c, emptyOpts); emptyShown = true; }
// Do NOT finish here — the observer stays alive so a late product (even a very slow API
// or a facet re-query that later returns rows) always retracts the empty-state.
}, skelTimeout);
// Leak guard: disconnect ONLY if we never showed an empty-state (nothing to strand) and the
// grid is still empty. If an empty-state IS up, we deliberately keep watching — a stranded
// "No results" over real products is a worse failure than one long-lived idle observer on a
// grid that receives no mutations (which costs nothing and is GC'd with the element).
tCap = setTimeout(function () {
if (!emptyShown && realCount(c) === 0) finish();
}, hardCap);
return { finish: finish };
}
global.UXGrid = { init: init };
if (typeof module !== 'undefined' && module.exports) module.exports = global.UXGrid;
})(typeof window !== 'undefined' ? window : this);
/* ============================================================================
ux-primitives.bundle — declarative auto-init (idempotent).
Scans the DOM for data-attributes and activates the matching primitive.
Every block is wrapped in try/catch and guarded so a page that uses NONE of
them is a silent no-op, and a missing primitive never breaks the others.
Re-callable as UXPrimitives.init(root) after injecting new DOM.
============================================================================ */
(function (global) {
var doc = global.document;
if (!doc) return;
function each(list, fn) { Array.prototype.forEach.call(list || [], fn); }
function ready(fn) {
if (doc.readyState === 'loading') doc.addEventListener('DOMContentLoaded', fn, { once: true });
else fn();
}
function initAll(root) {
root = root || doc;
try { if (global.Copy && Copy.init) Copy.init(root); } catch (e) { console.warn('[ux-primitives] copy', e); }
try { if (global.ModalRig && ModalRig.init) ModalRig.init(root); } catch (e) { console.warn('[ux-primitives] modal-rig', e); }
try { if (global.Bento && Bento.init) Bento.init(root); } catch (e) { console.warn('[ux-primitives] bento', e); }
try {
if (global.Skeleton) each(root.querySelectorAll('[data-ux-skeleton]'), function (el) {
if (el.__uxSkelBound) return; el.__uxSkelBound = true;
var lines = +(el.getAttribute('data-ux-skeleton-lines') || 0);
Skeleton.render(el, {
variant: el.getAttribute('data-ux-skeleton') || 'text',
count: +(el.getAttribute('data-ux-skeleton-count') || 1),
lines: lines || undefined
});
});
} catch (e) { console.warn('[ux-primitives] skeleton', e); }
try {
if (global.EmptyState) each(root.querySelectorAll('[data-ux-empty]'), function (el) {
if (el.__uxEmptyBound) return; el.__uxEmptyBound = true;
EmptyState.render(el, {
variant: el.getAttribute('data-ux-empty') || 'no-data',
title: el.getAttribute('data-ux-empty-title') || undefined,
text: el.getAttribute('data-ux-empty-text') || undefined
});
});
} catch (e) { console.warn('[ux-primitives] empty-state', e); }
// lifecycle-aware grid activation (safe skeleton + zero-detection empty-state,
// first-load-only, auto-clear via MutationObserver). This is the SAFE default for
// product grids — prefer it over bare [data-ux-skeleton]/[data-ux-empty] on a live grid.
try {
if (global.UXGrid) each(root.querySelectorAll('[data-ux-grid]'), function (el) {
if (el.__uxGrid) return; // UXGrid.init also guards; this avoids a redundant call
UXGrid.init(el);
});
} catch (e) { console.warn('[ux-primitives] grid-lifecycle', e); }
try {
if (global.Toast) each(root.querySelectorAll('[data-ux-toast]'), function (el) {
if (el.__uxToastBound) return; el.__uxToastBound = true;
var fire = function () {
Toast.show({ message: el.getAttribute('data-ux-toast') || 'Done',
variant: el.getAttribute('data-ux-toast-variant') || 'info' });
};
if (el.hasAttribute('data-ux-toast-onload')) fire();
else el.addEventListener('click', fire);
});
} catch (e) { console.warn('[ux-primitives] toast', e); }
try {
if (global.CommandPalette) each(root.querySelectorAll('[data-ux-cmdk]'), function (el) {
if (el.__uxCmdkBound) return; el.__uxCmdkBound = true;
var src = el.querySelector('script[type="application/json"]');
var cmds = [];
if (src) { try { cmds = JSON.parse(src.textContent || '[]'); } catch (e2) {} }
cmds = cmds.map(function (c) { if (!c.run && c.href) { var h = c.href; c.run = function () { location.href = h; }; } return c; });
var p = CommandPalette.create({ commands: cmds, hotkey: el.getAttribute('data-ux-cmdk') !== 'no-hotkey' });
el.__uxCmdk = p;
each(root.querySelectorAll('[data-ux-cmdk-open]'), function (btn) {
if (btn.__uxCmdkOpenBound) return; btn.__uxCmdkOpenBound = true;
btn.addEventListener('click', function () { p.open(); });
});
});
} catch (e) { console.warn('[ux-primitives] command-palette', e); }
return true;
}
global.UXPrimitives = { init: initAll, version: '1.1.1' };
ready(function () { if (global.__uxPrimitivesAutoInit === false) return; try { initAll(doc); } catch (e) { console.warn('[ux-primitives] auto-init', e); } });
})(typeof window !== 'undefined' ? window : this);