[object Object]

← back to Flockedwallpaper

front-page-steward: ux-primitives v1.1.0 lifecycle grid activation (data-ux-grid: safe skeleton auto-clear + zero-detect empty-state) TK-12031

78de9f16697080ca3e65a9d9c8bf369073d758c6 · 2026-09-22 16:05:37 -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

Diff

commit 78de9f16697080ca3e65a9d9c8bf369073d758c6
Author: front-page-steward <steve@designerwallcoverings.com>
Date:   Tue Sep 22 16:05:37 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
---
 index.html               |   4 +-
 ux-primitives.bundle.css |   2 +-
 ux-primitives.bundle.js  | 139 ++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 140 insertions(+), 5 deletions(-)

diff --git a/index.html b/index.html
index 124f103..89c7c95 100644
--- a/index.html
+++ b/index.html
@@ -138,7 +138,7 @@ html.hf-fw-cta     [data-f="fw-cta"]    { 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>
@@ -368,7 +368,7 @@ html.hf-fw-cta     [data-f="fw-cta"]    { display:none !important }
 })();
 </script>
 
-<div class="product-grid" id="productGrid"></div>
+<div class="product-grid" id="productGrid" 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>
   <button class="load-more" id="loadMore" onclick="displayBatch()" style="display:none"><span>Load More</span></button>
 </div>
 
diff --git a/ux-primitives.bundle.css b/ux-primitives.bundle.css
index e6bb113..f321f17 100644
--- a/ux-primitives.bundle.css
+++ b/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/ux-primitives.bundle.js b/ux-primitives.bundle.js
index f074f36..9d9d0ec 100644
--- a/ux-primitives.bundle.js
+++ b/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);

← 5e587a7 front-page-steward: rework ux-primitives to silent bundle (r  ·  back to Flockedwallpaper  ·  front-page-steward: ux-primitives v1.1.1 — Cody slow-load fi d0f64b8 →