← back to Linenwallpaper
front-page-steward: ux-primitives v1.1.0 lifecycle grid activation (data-ux-grid: safe skeleton auto-clear + zero-detect empty-state) TK-12031
f16cc5c067c1ce032046027aa37e197f9c2a505c · 2026-09-22 16:05:36 -0700 · front-page-steward
Real-activation neg-control proven both engines: skeleton appears-then-auto-clears, products render, sort re-render 0 ghost skeletons, true-zero empty-state only, no new bundle errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjud9yhkmyLh365yZodCdn
Files touched
M public/index.htmlM public/ux-primitives.bundle.cssM public/ux-primitives.bundle.js
Diff
commit f16cc5c067c1ce032046027aa37e197f9c2a505c
Author: front-page-steward <steve@designerwallcoverings.com>
Date: Tue Sep 22 16:05:36 2026 -0700
front-page-steward: ux-primitives v1.1.0 lifecycle grid activation (data-ux-grid: safe skeleton auto-clear + zero-detect empty-state) TK-12031
Real-activation neg-control proven both engines: skeleton appears-then-auto-clears, products render, sort re-render 0 ghost skeletons, true-zero empty-state only, no new bundle errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjud9yhkmyLh365yZodCdn
---
public/index.html | 4 +-
public/ux-primitives.bundle.css | 2 +-
public/ux-primitives.bundle.js | 139 +++++++++++++++++++++++++++++++++++++++-
3 files changed, 140 insertions(+), 5 deletions(-)
diff --git a/public/index.html b/public/index.html
index 058450e..1d31961 100644
--- a/public/index.html
+++ b/public/index.html
@@ -279,7 +279,7 @@ html.hf-sample [data-f="sample"] { display:none !important }
<!-- noscript fallback is added by set-fb-pixel.sh on flip (omitted here to avoid unflipped img phone-home) -->
<noscript><img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1431180262113856<!-- End Meta Pixel -->ev=PageView<!-- End Meta Pixel -->noscript=1"/></noscript>
<!-- End Meta Pixel -->
- <!-- ux-primitives.bundle v1.0.0#21023ffcfab3 (front-page-steward TK-12031) -->
+ <!-- ux-primitives.bundle v1.1.0#aeb58948a264 (front-page-steward TK-12031) -->
<link rel="stylesheet" href="ux-primitives.bundle.css">
<script src="ux-primitives.bundle.js" defer></script>
</head>
@@ -632,7 +632,7 @@ html.hf-sample [data-f="sample"] { display:none !important }
<button data-sort-col="price">Price <span class="arr"></span></button>
</div>
<div id="rails" style="display:none"></div>
- <div class="grid" id="grid"></div>
+ <div class="grid" id="grid" data-ux-grid data-ux-grid-skeleton="card" data-ux-grid-count="8" data-ux-grid data-ux-grid-skeleton="card" data-ux-grid-count="8"></div>
<div class="loading" id="loading">Loading more…</div>
<div class="sentinel" id="sentinel"></div>
</section>
diff --git a/public/ux-primitives.bundle.css b/public/ux-primitives.bundle.css
index e6bb113..f321f17 100644
--- a/public/ux-primitives.bundle.css
+++ b/public/ux-primitives.bundle.css
@@ -1,4 +1,4 @@
-/* ux-primitives.bundle.css — canonical Bundle-B UX primitives (generated 2026-09-22T21:06:28Z)
+/* ux-primitives.bundle.css — canonical Bundle-B UX primitives (generated 2026-09-22T22:59:00Z)
Shared ux-primitives.css tokens (md5 614018b) included ONCE, then each
primitive's own CSS. All selectors namespaced .ux-* / .ux-bento* / .ux-modal*
so nothing collides with corner-nav (cnav-*), hero-4grid, or site styles.
diff --git a/public/ux-primitives.bundle.js b/public/ux-primitives.bundle.js
index f074f36..9d9d0ec 100644
--- a/public/ux-primitives.bundle.js
+++ b/public/ux-primitives.bundle.js
@@ -1,4 +1,4 @@
-/* ux-primitives.bundle.js — canonical Bundle-B UX primitives (generated 2026-09-22T21:06:28Z)
+/* ux-primitives.bundle.js — canonical Bundle-B UX primitives (generated 2026-09-22T22:59: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
@@ -1125,6 +1125,131 @@
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;
+ var hardCap = 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;
+ 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;
+ }
+ 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 */ }
+
+ tSkel = setTimeout(function () {
+ clearSkel(c);
+ if (realCount(c) === 0 && wantEmpty && !emptyShown) { showEmpty(c, emptyOpts); emptyShown = true; }
+ // keep observing until a late product arrives (removes empty) or hardCap
+ }, skelTimeout);
+ tCap = setTimeout(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.
@@ -1171,6 +1296,16 @@
});
} 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;
@@ -1202,6 +1337,6 @@
return true;
}
- global.UXPrimitives = { init: initAll, version: '1.0.0' };
+ global.UXPrimitives = { init: initAll, version: '1.1.0' };
ready(function () { if (global.__uxPrimitivesAutoInit === false) return; try { initAll(doc); } catch (e) { console.warn('[ux-primitives] auto-init', e); } });
})(typeof window !== 'undefined' ? window : this);
← 5a96446 front-page-steward: rework ux-primitives to silent bundle (r
·
back to Linenwallpaper
·
front-page-steward: ux-primitives v1.1.1 — Cody slow-load fi 6d57cb0 →