[object Object]

← back to Designer Wallcoverings

Add native-Liquid infinite scroll for theme 142250278963 (collection-list, blog, blog-masonry) + gated push script

e2ae1b4deeee49ecac59565d18553f80341fc7bf · 2026-06-22 15:18:46 -0700 · Steve

Files touched

Diff

commit e2ae1b4deeee49ecac59565d18553f80341fc7bf
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jun 22 15:18:46 2026 -0700

    Add native-Liquid infinite scroll for theme 142250278963 (collection-list, blog, blog-masonry) + gated push script
---
 .../theme-infinite-scroll-142250278963/README.md   |  59 ++++++++
 .../assets/dw-native-infinite.js                   | 137 +++++++++++++++++
 .../push-native-infinite-to-steves-v5.sh           |  69 +++++++++
 .../sections/blog-masonry.liquid                   |  96 ++++++++++++
 .../sections/blog.liquid                           | 111 ++++++++++++++
 .../sections/collection-list.liquid                | 168 +++++++++++++++++++++
 .../snippets/pagination.liquid                     |  26 ++++
 7 files changed, 666 insertions(+)

diff --git a/shopify/theme-infinite-scroll-142250278963/README.md b/shopify/theme-infinite-scroll-142250278963/README.md
new file mode 100644
index 00000000..3a41c05d
--- /dev/null
+++ b/shopify/theme-infinite-scroll-142250278963/README.md
@@ -0,0 +1,59 @@
+# Infinite scroll — theme "Steves Version 5.0" (142250278963)
+
+Goal: every grid/list surface on the dev theme infinite-scrolls.
+
+## Renderer split (audited 2026-06-22, read-only Admin Assets API)
+
+| Surface | Route/template | Renderer | Was | Now |
+|---|---|---|---|---|
+| Collection product grid | `templates/collection.json` (Boost `apps` block; native `collection` section renders only the description) | **Boost** | infinite (Boost global) | unchanged ✓ |
+| Search results | `templates/search.json` (native `template-search` `disabled:true`; Boost `apps` block) | **Boost** | infinite (Boost global) | unchanged ✓ |
+| Vendor / tag pages | render through `collection.json` | **Boost** | infinite (Boost global) | unchanged ✓ |
+| Boost belt-and-suspenders | `assets/boost-sd-custom.js` (hides `.boost-sd__pagination`, auto-clicks next on scroll) | Boost+JS | infinite | unchanged ✓ |
+| **/collections (list of collections)** | `templates/list-collections.json` → `sections/collection-list.liquid` | **native Liquid** `{% paginate %}` | numbered pager | **infinite (this change)** |
+| **Blog index** | `sections/blog.liquid` | native Liquid `{% paginate %}` | numbered pager | **infinite (this change)** |
+| **Blog (masonry)** | `sections/blog-masonry.liquid` | native Liquid `{% paginate %}` | numbered pager | **infinite (this change)** |
+| Related / "You may also like" | `sections/static-product-recommendations.liquid` (`limit:4`) | native, bounded | fixed 4 | n/a — bounded recommendation row |
+| Complementary products | `snippets/complementary-products.liquid` | native slider, `recommendations.products` | bounded slider | n/a — bounded cross-sell |
+| Recently viewed | `sections/recently-viewed.liquid` | native, bounded | bounded | n/a — bounded |
+| Home "Featured collection" ×4 | `sections/home-products.liquid` (`limit: cols×rows`) | native, bounded | fixed module | n/a — merchandised module |
+| Home collections / masonry / slideshow | `index.json` modules | native, bounded | fixed | n/a — merchandised modules |
+| Article comments | `sections/article.liquid` (`paginate article.comments by 10`) | native | numbered | n/a — comment thread, not a content grid |
+
+Boost's per-template embedded `paginationType` reads `"default"`, but the live
+effective behavior is `infinite_scroll` because Boost's **global dashboard
+setting** overrides the theme-snapshot value (confirmed live on /collections/new-arrivals
+by the main session). No theme template disables/overrides Boost back to numbered,
+so all Boost surfaces (collection/search/vendor/tag) are consistently infinite. ✓
+
+## What changed (native Liquid → infinite scroll)
+
+New reusable helper `assets/dw-native-infinite.js` — progressive enhancement.
+For any `[data-dw-infinite]` wrapper it reads `data-dw-infinite-next`
+(= `paginate.next.url`), fetches the next page, appends the items-container's
+children, then reads the next-next URL from the fetched page. Hides the numbered
+`<ul.pagination>` (kept in DOM as no-JS / SEO fallback; restored on fetch error).
+Uses `IntersectionObserver` with a scroll fallback, skips masonry sizer nodes.
+
+Sections wrapped with the markup contract + `<script>` include:
+- `sections/collection-list.liquid` — items `.dw-collections-grid` (linklist mode left unpaginated)
+- `sections/blog.liquid` — items `.blog-posts`
+- `sections/blog-masonry.liquid` — items `.blog-posts-masonry` (sizer skipped)
+
+`snippets/pagination.liquid` is unchanged (now emitted inside `[data-dw-infinite-pager]`).
+
+## Push (GATED — do not run autonomously; theme writes need Steve's go)
+
+```bash
+cd /Users/stevestudio2/Projects/Designer-Wallcoverings/shopify/theme-infinite-scroll-142250278963
+# theme-scoped token lives in DW-Agents/dw-agents/update-theme-quantity.js
+# (regex shpat_[a-f0-9]{32}) — export it, don't paste the literal here.
+THEME_TOKEN="$(grep -oE 'shpat_[a-f0-9]{32}' \
+  ../../DW-Agents/dw-agents/update-theme-quantity.js | head -1)" \
+  bash push-native-infinite-to-steves-v5.sh
+```
+
+Pushes 4 assets to theme **142250278963** only (hard-guarded against live
+143739584563). Backs up each asset (GET before PUT) to `backups/`, verifies
+(GET after PUT). Idempotent. Preview URLs + revert instructions printed on
+completion. NEVER publishes the theme.
diff --git a/shopify/theme-infinite-scroll-142250278963/assets/dw-native-infinite.js b/shopify/theme-infinite-scroll-142250278963/assets/dw-native-infinite.js
new file mode 100644
index 00000000..6a2f73b4
--- /dev/null
+++ b/shopify/theme-infinite-scroll-142250278963/assets/dw-native-infinite.js
@@ -0,0 +1,137 @@
+/* DW Native Infinite Scroll — progressive enhancement for theme {% paginate %} grids.
+ *
+ * Converts the theme's native numbered-pager Liquid grids (collection-list /
+ * blog / blog-masonry) to append-on-scroll, WITHOUT touching the Boost AI
+ * Search & Discovery surfaces (collection/search product grids — those are
+ * handled by Boost's own global infinite_scroll + assets/boost-sd-custom.js).
+ *
+ * Markup contract (added by the section .liquid files):
+ *   <div data-dw-infinite              ← scroll container wrapper
+ *        data-dw-infinite-items=".blog-posts"      ← selector of the element whose
+ *                                                     CHILDREN are the cards
+ *        data-dw-infinite-next="/blogs/news?page=2" ← next page URL (paginate.next.url)
+ *        data-dw-infinite-page="1">
+ *     <div class="blog-posts"> … cards … </div>
+ *     <ul class="pagination" data-dw-infinite-pager> … numbered links … </ul>  ← noscript fallback
+ *   </div>
+ *
+ * Behavior: when the viewport nears the bottom, fetch the next URL, extract the
+ * same items-container from the returned HTML, append its children to the live
+ * items-container, then read the NEXT next-url from the fetched page. Stops when
+ * there is no further next URL. The numbered <ul.pagination> is hidden once JS
+ * takes over (kept in the DOM for no-JS users + SEO crawl).
+ */
+(function () {
+  'use strict';
+
+  function initContainer(root) {
+    if (root.__dwInfiniteInit) return;
+    root.__dwInfiniteInit = true;
+
+    var itemsSel = root.getAttribute('data-dw-infinite-items');
+    var itemsEl = itemsSel ? root.querySelector(itemsSel) : root;
+    if (!itemsEl) return;
+
+    var nextUrl = root.getAttribute('data-dw-infinite-next') || '';
+    var loading = false;
+    var done = !nextUrl;
+
+    // Hide the numbered pager — JS is now driving. Kept in DOM as no-JS fallback.
+    var pager = root.querySelector('[data-dw-infinite-pager]');
+    if (pager) pager.setAttribute('hidden', 'hidden');
+
+    // Sentinel + loading indicator appended after the items container.
+    var sentinel = document.createElement('div');
+    sentinel.className = 'dw-infinite-sentinel';
+    sentinel.setAttribute('aria-hidden', 'true');
+    sentinel.style.cssText = 'min-height:1px;width:100%';
+    var spinner = document.createElement('div');
+    spinner.className = 'dw-infinite-loading';
+    spinner.style.cssText = 'display:none;text-align:center;padding:24px 0;font:600 11px/1 -apple-system,system-ui,sans-serif;letter-spacing:1.5px;text-transform:uppercase;color:#999';
+    spinner.textContent = 'Loading more…';
+    itemsEl.parentNode.insertBefore(spinner, itemsEl.nextSibling);
+    itemsEl.parentNode.insertBefore(sentinel, spinner.nextSibling);
+
+    function appendFrom(html, url) {
+      var doc = new DOMParser().parseFromString(html, 'text/html');
+      // Find the matching items container on the fetched page.
+      var src = null;
+      var fetchedRoot = doc.querySelector('[data-dw-infinite][data-dw-infinite-items="' + itemsSel + '"]') ||
+                        doc.querySelector('[data-dw-infinite]');
+      if (fetchedRoot) src = itemsSel ? fetchedRoot.querySelector(itemsSel) : fetchedRoot;
+      if (!src) src = doc.querySelector(itemsSel || '*');
+      if (src) {
+        var frag = document.createDocumentFragment();
+        // Skip masonry sizer elements (they must stay single).
+        Array.prototype.slice.call(src.children).forEach(function (child) {
+          if (child.hasAttribute && (child.hasAttribute('data-masonry-sizer') ||
+              (child.classList && child.classList.contains('masonry-grid-sizer')))) return;
+          frag.appendChild(child.cloneNode(true));
+        });
+        itemsEl.appendChild(frag);
+        // Re-layout masonry if the theme's masonry lib is present.
+        if (window.dwRelayoutMasonry) { try { window.dwRelayoutMasonry(itemsEl); } catch (e) {} }
+        document.dispatchEvent(new CustomEvent('dw:infinite:appended', { detail: { container: itemsEl, url: url } }));
+      }
+      // Read the next-next url from the fetched root (or its pager).
+      var newNext = '';
+      if (fetchedRoot) newNext = fetchedRoot.getAttribute('data-dw-infinite-next') || '';
+      if (!newNext) {
+        var np = doc.querySelector('[data-dw-infinite-pager] .next a, [data-dw-infinite-pager] a[rel="next"]');
+        if (np) newNext = np.getAttribute('href') || '';
+      }
+      return newNext;
+    }
+
+    function loadMore() {
+      if (loading || done) return;
+      loading = true;
+      spinner.style.display = 'block';
+      fetch(nextUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin' })
+        .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
+        .then(function (html) {
+          nextUrl = appendFrom(html, nextUrl);
+          if (!nextUrl) done = true;
+        })
+        .catch(function () {
+          // On any error, fall back to showing the numbered pager again.
+          done = true;
+          if (pager) pager.removeAttribute('hidden');
+        })
+        .then(function () {
+          loading = false;
+          spinner.style.display = 'none';
+          // If the new content didn't fill the viewport, keep loading.
+          if (!done) maybeLoad();
+        });
+    }
+
+    function maybeLoad() {
+      if (done || loading) return;
+      var rect = sentinel.getBoundingClientRect();
+      if (rect.top - window.innerHeight < 700) loadMore();
+    }
+
+    if ('IntersectionObserver' in window) {
+      var io = new IntersectionObserver(function (entries) {
+        if (entries.some(function (e) { return e.isIntersecting; })) loadMore();
+      }, { rootMargin: '700px 0px' });
+      io.observe(sentinel);
+    } else {
+      window.addEventListener('scroll', maybeLoad, { passive: true });
+    }
+    // Initial check in case the first page is short.
+    maybeLoad();
+  }
+
+  function initAll() {
+    var roots = document.querySelectorAll('[data-dw-infinite]');
+    for (var i = 0; i < roots.length; i++) initContainer(roots[i]);
+  }
+
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', initAll);
+  } else {
+    initAll();
+  }
+})();
diff --git a/shopify/theme-infinite-scroll-142250278963/push-native-infinite-to-steves-v5.sh b/shopify/theme-infinite-scroll-142250278963/push-native-infinite-to-steves-v5.sh
new file mode 100755
index 00000000..a2d3ce6e
--- /dev/null
+++ b/shopify/theme-infinite-scroll-142250278963/push-native-infinite-to-steves-v5.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+# Stage native-Liquid infinite scroll onto the UNPUBLISHED dev theme
+# "Steves Version 5.0" (142250278963) on designer-laboratory-sandbox.
+#
+# Converts the theme's three native {% paginate %} numbered-pager grids
+# (collection-list / blog / blog-masonry) to append-on-scroll infinite scroll.
+# Leaves the Boost AI Search & Discovery surfaces (collection/search PRODUCT
+# grids) untouched — those already infinite-scroll via Boost's global setting
+# + assets/boost-sd-custom.js. NEVER touches the live theme 143739584563.
+#
+# Each asset is backed up (GET before PUT) and verified (GET after PUT).
+# Idempotent: re-running only re-PUTs assets whose live value differs.
+#
+#   THEME_TOKEN=shpat_xxx bash push-native-infinite-to-steves-v5.sh
+set -euo pipefail
+cd "$(dirname "$0")"
+STORE="designer-laboratory-sandbox.myshopify.com"; API="2024-10"
+TGT_THEME=142250278963          # Steves Version 5.0 (unpublished dev)
+LIVE_THEME=143739584563         # NewWall Template — NEVER write to this
+TOK="${THEME_TOKEN:?set THEME_TOKEN}"
+
+# Hard guard: refuse to ever target the live theme id.
+if [ "$TGT_THEME" = "$LIVE_THEME" ]; then echo "REFUSING: target == live theme"; exit 2; fi
+
+python3 - "$STORE" "$API" "$TOK" "$TGT_THEME" <<'PY'
+import sys,json,os,urllib.request,urllib.parse,datetime,time
+store,api,tok,tid=sys.argv[1:5]
+H={"X-Shopify-Access-Token":tok,"Content-Type":"application/json"}
+base=f"https://{store}/admin/api/{api}/themes/{tid}/assets.json"
+def get(key):
+    u=base+"?"+urllib.parse.urlencode({"asset[key]":key})
+    return json.load(urllib.request.urlopen(urllib.request.Request(u,headers=H)))["asset"]["value"]
+def put(key,val):
+    body=json.dumps({"asset":{"key":key,"value":val}}).encode()
+    r=urllib.request.urlopen(urllib.request.Request(base,data=body,method="PUT",headers=H))
+    return r.status
+
+# local file -> theme asset key
+FILES={
+ "assets/dw-native-infinite.js":      "assets/dw-native-infinite.js",
+ "sections/collection-list.liquid":   "sections/collection-list.liquid",
+ "sections/blog.liquid":              "sections/blog.liquid",
+ "sections/blog-masonry.liquid":      "sections/blog-masonry.liquid",
+}
+os.makedirs("backups", exist_ok=True)
+stamp=datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
+for lf,key in FILES.items():
+    want=open(lf,encoding="utf-8").read()
+    try: live=get(key)
+    except Exception: live=None
+    if live is not None:
+        bk=f"backups/{key.replace('/','_')}.{stamp}.bak"
+        open(bk,"w",encoding="utf-8").write(live)
+        print(f"backup: {bk} ({len(live)} bytes)")
+    if live==want:
+        print(f"skip  : {key} (already current)"); continue
+    st=put(key,want)
+    time.sleep(0.6)  # gentle rate
+    ver=get(key)
+    ok = ver==want
+    print(f"PUT   : {key} HTTP {st} verify={'OK' if ok else 'MISMATCH'} ({len(want)} bytes)")
+    if not ok: print(f"  *** verify mismatch on {key} — investigate"); sys.exit(1)
+
+print()
+print("DONE. Preview the native grids on the dev theme:")
+print(f"  /collections        -> https://www.designerwallcoverings.com/collections?preview_theme_id={tid}")
+print(f"  /blogs/<blog-handle> -> https://www.designerwallcoverings.com/blogs/news?preview_theme_id={tid}")
+print(f"revert: PUT each backups/*.{stamp}.bak back to its key on theme {tid}")
+PY
diff --git a/shopify/theme-infinite-scroll-142250278963/sections/blog-masonry.liquid b/shopify/theme-infinite-scroll-142250278963/sections/blog-masonry.liquid
new file mode 100644
index 00000000..3ebc9d65
--- /dev/null
+++ b/shopify/theme-infinite-scroll-142250278963/sections/blog-masonry.liquid
@@ -0,0 +1,96 @@
+<script
+  type="application/json"
+  data-section-type="static-blog-masonry"
+  data-section-id="{{ section.id }}"
+>
+</script>
+
+{% assign emphasis_first_post = false %}
+{% if section.settings.emphasis_first_post %}
+  {% assign emphasis_first_post = true %}
+{% endif %}
+
+<section>
+  <h1 class="page-title">{{ blog.title }}</h1>
+
+  {% render 'breadcrumbs' %}
+
+  <div class="blog-wrapper-masonry">
+    {% paginate blog.articles by section.settings.articles_per_page %}
+
+      {%- comment -%} DW infinite scroll: wrapper carries the next-page URL; dw-native-infinite.js appends on scroll (masonry sizer is skipped to avoid duplication). {%- endcomment -%}
+      <div
+        data-dw-infinite
+        data-dw-infinite-items=".blog-posts-masonry"
+        data-dw-infinite-next="{% if paginate.next %}{{ paginate.next.url }}{% endif %}"
+        data-dw-infinite-page="{{ paginate.current_page }}"
+      >
+      <div class="blog-posts-masonry masonry-grid" data-masonry-grid>
+        <div class="masonry-grid-sizer" data-masonry-sizer></div>
+
+        {% for article in blog.articles %}
+          {% assign is_emphasized = false %}
+
+          {%- if emphasis_first_post -%}
+            {% comment %}
+              Don't emphasize post if:
+                - is not first page
+                - if tags are active
+                - is not first in loop
+            {% endcomment %}
+            {%- if paginate.current_page == 1 and current_tags == blank and forloop.first -%}
+              {% assign is_emphasized = true %}
+            {%- endif -%}
+          {%- endif -%}
+
+          {%
+            render 'section-blog-post-masonry',
+            article: article,
+            is_emphasized: is_emphasized
+          %}
+        {% else %}
+          <div class="blog-post-masonry masonry-grid-item" data-masonry-item>
+            <span class="empty">
+              {{ 'blogs.blog.empty' | t }}
+            </span>
+          </div>
+
+        {% endfor %}
+      </div>
+
+      {% if paginate.previous or paginate.next %}
+        <div data-dw-infinite-pager>
+          {% render 'pagination', paginate: paginate %}
+        </div>
+      {% endif %}
+      </div>
+
+    {% endpaginate %}
+  </div>
+</section>
+
+<script src="{{ 'dw-native-infinite.js' | asset_url }}" defer></script>
+
+{% schema %}
+
+{
+  "name": "Blog",
+  "settings": [
+    {
+      "type": "range",
+      "id": "articles_per_page",
+      "label": "Posts per page",
+      "min": 3,
+      "max": 12,
+      "step": 3,
+      "default": 9
+    },
+    {
+      "type": "checkbox",
+      "id": "emphasis_first_post",
+      "label": "Emphasize first post"
+    }
+  ]
+}
+
+{% endschema %}
diff --git a/shopify/theme-infinite-scroll-142250278963/sections/blog.liquid b/shopify/theme-infinite-scroll-142250278963/sections/blog.liquid
new file mode 100644
index 00000000..b7cedbfb
--- /dev/null
+++ b/shopify/theme-infinite-scroll-142250278963/sections/blog.liquid
@@ -0,0 +1,111 @@
+<script
+  type="application/json"
+  data-section-type="static-blog"
+  data-section-id="{{ section.id }}"
+>
+</script>
+
+<section
+  {% if section.settings.tag-filter %}data-tag-filter{% endif %}
+>
+
+  <h1 class="page-title">{{ blog.title }}</h1>
+
+  {% render 'breadcrumbs' %}
+
+  {% render 'section-blog-sidebar' %}
+
+  <div class="blog-wrapper">
+    {% paginate blog.articles by section.settings.articles_per_page %}
+
+      {%- comment -%} DW infinite scroll: wrapper carries the next-page URL; dw-native-infinite.js appends on scroll. {%- endcomment -%}
+      <div
+        data-dw-infinite
+        data-dw-infinite-items=".blog-posts"
+        data-dw-infinite-next="{% if paginate.next %}{{ paginate.next.url }}{% endif %}"
+        data-dw-infinite-page="{{ paginate.current_page }}"
+      >
+        <div class="blog-posts">
+          {% for article in blog.articles %}
+            {% render 'section-blog-post', article: article %}
+          {% else %}
+            <span class="empty">{{ 'blogs.blog.empty' | t }}</span>
+          {% endfor %}
+        </div>
+
+        {% if paginate.previous or paginate.next %}
+          <div data-dw-infinite-pager>
+            {% render 'pagination', paginate: paginate %}
+          </div>
+        {% endif %}
+      </div>
+
+    {% endpaginate %}
+
+    {%- for block in section.blocks -%}
+      {%- case block.type -%}
+      {%- when 'promotional-image' -%}
+        {% render 'block-blog-promo', block: block %}
+      {%- endcase -%}
+    {%- endfor -%}
+  </div>
+</section>
+
+<script src="{{ 'dw-native-infinite.js' | asset_url }}" defer></script>
+
+{% schema %}
+{
+  "name": "Blog",
+  "settings": [
+    {
+      "type": "range",
+      "id": "articles_per_page",
+      "label": "Posts per page",
+      "min": 2,
+      "max": 10,
+      "step": 1,
+      "default": 4
+    },
+    {
+      "type": "checkbox",
+      "id": "tag-filter",
+      "label": "Show tag filter"
+    },
+    {
+      "type": "checkbox",
+      "id": "rss-icon",
+      "label": "Show subscribe link"
+    }
+  ],
+  "blocks": [
+    {
+      "type": "promotional-image",
+      "name": "Promotional image",
+      "settings": [
+        {
+          "type": "image_picker",
+          "id": "image",
+          "label": "Image"
+        },
+        {
+          "type": "text",
+          "id": "heading",
+          "label": "Heading"
+        },
+        {
+          "type": "text",
+          "id": "subtitle",
+          "label": "Subheading"
+        },
+        {
+          "type": "url",
+          "id": "url",
+          "label": "Link"
+        }
+      ],
+      "limit": 1
+    }
+  ]
+}
+
+{% endschema %}
diff --git a/shopify/theme-infinite-scroll-142250278963/sections/collection-list.liquid b/shopify/theme-infinite-scroll-142250278963/sections/collection-list.liquid
new file mode 100644
index 00000000..10fe83e8
--- /dev/null
+++ b/shopify/theme-infinite-scroll-142250278963/sections/collection-list.liquid
@@ -0,0 +1,168 @@
+<script
+  type="application/json"
+  data-section-id="{{ section.id }}"
+  data-section-type="static-collections-list"
+>
+</script>
+
+
+{% assign use_masonry = false %}
+
+{% if section.settings.layout == 'masonry' %}
+  {% assign use_masonry = true %}
+{% endif %}
+
+<section
+  class="collections-list-section {% unless use_masonry %}collections-list-grid{% endunless %}"
+  {% if use_masonry %}data-collection-masonry{% endif %}>
+  <h1 class="page-title">{{ 'collections.listing.header' | t }}</h1>
+
+  {% render 'breadcrumbs' %}
+
+  <!-- STICKY SHOP BY COLOR BAR -->
+  <div id="dw-color-bar" style="position:sticky;top:0;z-index:100;background:#FAF9F6;border-bottom:1px solid #e5e5e5;padding:12px 0;margin:0 -20px 24px;transition:box-shadow 0.3s;">
+    <div style="max-width:1200px;margin:0 auto;padding:0 16px;">
+      <p style="font-family:-apple-system,system-ui,sans-serif;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:#999;margin:0 0 10px;text-align:center;">Shop by Color</p>
+      <div style="display:flex;flex-wrap:nowrap;overflow-x:auto;justify-content:center;gap:16px;scrollbar-width:none;-ms-overflow-style:none;padding:2px 0;">
+        <a href="/collections/blue-wallpaper-collection" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#3B5998;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Blue</span></a>
+        <a href="/collections/green-wallpaper-collection" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#4A7C59;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Green</span></a>
+        <a href="/collections/gold-wallpaper" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#C4995C;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Gold</span></a>
+        <a href="/collections/red-wallpaper-collection" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#9B2335;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Red</span></a>
+        <a href="/collections/pink-wallcovering-2" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#D4768B;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Pink</span></a>
+        <a href="/collections/purple" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#6B3FA0;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Purple</span></a>
+        <a href="/collections/orange" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#D2691E;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Orange</span></a>
+        <a href="/collections/yellow-wallpaper-collection" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#DAA520;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Yellow</span></a>
+        <a href="/collections/black" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#1a1a1a;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Black</span></a>
+        <a href="/collections/white-wallpaper-collection" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#F5F5F0;margin:0 auto 4px;border:2px solid #ddd;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">White</span></a>
+        <a href="/collections/grey-wallcovering" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#808080;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Gray</span></a>
+        <a href="/collections/brown-wallpaper-collection" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#6B4226;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Brown</span></a>
+        <a href="/collections/beige-cream-wallpaper-collection" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#C8B99A;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Beige</span></a>
+        <a href="/collections/grey-silver" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#C0C0C0,#E8E8E8);margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Silver</span></a>
+        <a href="/collections/metallic-wallpaper-collections" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#B8860B,#DAA520,#F0E68C);margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Metallic</span></a>
+        <a href="/collections/coral" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#FF6F61;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Coral</span></a>
+        <a href="/collections/turquoise" style="text-decoration:none;text-align:center;flex-shrink:0;"><div style="width:36px;height:36px;border-radius:50%;background:#40B5AD;margin:0 auto 4px;border:2px solid #eee;box-shadow:0 1px 4px rgba(0,0,0,0.08);"></div><span style="font-family:-apple-system,system-ui,sans-serif;font-size:10px;color:#666;">Teal</span></a>
+      </div>
+    </div>
+  </div>
+  <style>
+    #dw-color-bar::-webkit-scrollbar{display:none}
+    #dw-color-bar div::-webkit-scrollbar{display:none}
+    #dw-color-bar a:hover div{transform:scale(1.15);transition:transform 0.2s}
+    @media(max-width:768px){#dw-color-bar>div>div{justify-content:flex-start;padding:0 8px}}
+  </style>
+  <!-- END STICKY COLOR BAR -->
+
+  {% assign using_linklist = false %}
+  {% if linklists['all-collections'] and linklists['all-collections'].links.size > 0 %}
+    {% assign using_linklist = true %}
+  {% endif %}
+
+  {% assign collections_per_page = section.settings.collections_per_row | times: section.settings.collections_number_of_rows %}
+
+  <div class="collections-list-content">
+    {%- if using_linklist -%}
+      {%- comment -%} Linklist mode is unpaginated (all links on one page) — no infinite scroll needed. {%- endcomment -%}
+      <div
+        class="rows-of-{{ section.settings.collections_per_row }} {% if use_masonry %}masonry-grid{% endif %}"
+        {% if use_masonry %}data-masonry-grid{% endif %}
+      >
+        {%- if use_masonry -%}
+          <div class="masonry-grid-sizer" data-masonry-sizer></div>
+        {%- endif -%}
+        {% for link in linklists['all-collections'].links %}
+          {%
+            render 'collection-list-item',
+            collection_list_item: 'linklist',
+            link: link,
+            use_masonry: use_masonry,
+            description: section.settings.show_description
+          %}
+        {% endfor %}
+      </div>
+    {%- else -%}
+      {%- paginate collections by collections_per_page -%}
+        {%- comment -%} DW infinite scroll: wrapper carries the next-page URL; dw-native-infinite.js appends on scroll. {%- endcomment -%}
+        <div
+          data-dw-infinite
+          data-dw-infinite-items=".dw-collections-grid"
+          data-dw-infinite-next="{% if paginate.next %}{{ paginate.next.url }}{% endif %}"
+          data-dw-infinite-page="{{ paginate.current_page }}"
+        >
+          <div
+            class="dw-collections-grid rows-of-{{ section.settings.collections_per_row }} {% if use_masonry %}masonry-grid{% endif %}"
+            {% if use_masonry %}data-masonry-grid{% endif %}
+          >
+            {%- if use_masonry -%}
+              <div class="masonry-grid-sizer" data-masonry-sizer></div>
+            {%- endif -%}
+            {%- for collection in collections -%}
+              {%-
+                render 'collection-list-item',
+                collection_list_item: 'collection',
+                collection: collection,
+                use_masonry: use_masonry,
+                description: section.settings.show_description
+              -%}
+            {%- endfor -%}
+          </div>
+
+          <div data-dw-infinite-pager>
+            {% render 'pagination', paginate: paginate %}
+          </div>
+        </div>
+      {%- endpaginate -%}
+    {% endif %}
+  </div>
+</section>
+
+<script src="{{ 'dw-native-infinite.js' | asset_url }}" defer></script>
+
+{% schema %}
+{
+  "name": "Collection list page",
+  "settings": [
+    {
+      "type": "select",
+      "id": "layout",
+      "label": "Layout",
+      "options": [
+        {
+          "value": "default",
+          "label": "Default"
+        },
+        {
+          "value": "masonry",
+          "label": "Masonry"
+        }
+      ],
+      "default": "default"
+    },
+    {
+      "type": "checkbox",
+      "id": "show_description",
+      "label": "Show description",
+      "default": false
+    },
+    {
+      "type": "range",
+      "id": "collections_per_row",
+      "label": "Collections per row",
+      "min": 2,
+      "max": 4,
+      "step": 1,
+      "default": 3
+    },
+    {
+      "type": "range",
+      "id": "collections_number_of_rows",
+      "label": "Rows",
+      "min": 2,
+      "max": 6,
+      "step": 1,
+      "default": 6
+    }
+  ]
+}
+
+{% endschema %}
+
diff --git a/shopify/theme-infinite-scroll-142250278963/snippets/pagination.liquid b/shopify/theme-infinite-scroll-142250278963/snippets/pagination.liquid
new file mode 100644
index 00000000..d8edf52e
--- /dev/null
+++ b/shopify/theme-infinite-scroll-142250278963/snippets/pagination.liquid
@@ -0,0 +1,26 @@
+
+{% if paginate.previous or paginate.next %}
+  <ul class="pagination">
+
+    {% if paginate.previous %}
+      <li class="previous"><a href="{{ paginate.previous.url }}">&#xe601;</a></li>
+    {% endif %}
+
+    {% for part in paginate.parts %}
+      {% if part.is_link %}
+        <li>{{ part.title | link_to: part.url }}</li>
+      {% else %}
+        {% if part.title == '&hellip;' %}
+          <li class="ellipsis">{{ part.title }}</li>
+        {% else %}
+          <li class="active">{{ part.title | link_to: part.url }}</li>
+        {% endif %}
+      {% endif %}
+    {% endfor %}
+
+    {% if paginate.next %}
+      <li class="next"><a href="{{ paginate.next.url }}">&#xe600;</a></li>
+    {% endif %}
+  </ul>
+{% endif %}
+

← c05eddc1 Bucket B Scalamandre: roll-variant recovery script + run rep  ·  back to Designer Wallcoverings  ·  Generalize Bucket-B lander: bucketB-land-all.sh drains ALL s e8b5ae15 →