← back to Designer Wallcoverings
auto-save: 2026-06-23T09:20:41 (16 files) — shopify/scripts/cadence/sdg-trade-login.mjs shopify/scripts/tres-tintas-designer-descriptions.js shopify/tres-tintas-desc-backups/_done.ledger shopify/grid-js-fix/backups/header.liquid.live-142250278963.before shopify/grid-js-fix/backups/product-list-item.liquid.live-142250278963.before
e78be974b60caa3be6a73fe9a2d715be8c30fa54 · 2026-06-23 09:20:47 -0700 · Steve Abrams
Files touched
A shopify/grid-js-fix/backups/header.liquid.live-142250278963.beforeA shopify/grid-js-fix/backups/product-list-item.liquid.live-142250278963.beforeA shopify/grid-js-fix/diagnose-init-throw.jsA shopify/grid-js-fix/patched/header.liquidA shopify/grid-js-fix/patched/product-list-item.liquidA shopify/grid-js-fix/probe-contract.jsA shopify/grid-js-fix/probe-masonry.jsA shopify/grid-js-fix/verify-dev-fix.jsA shopify/grid-js-fix/verify-dev-preview-auth.jsA shopify/grid-js-fix/verify-final.jsA shopify/scripts/cadence/sdg-capture-edi.mjsA shopify/scripts/cadence/sdg-fetch-pricing.mjsA shopify/scripts/cadence/sdg-recon-api.mjsM shopify/scripts/cadence/sdg-trade-login.mjsM shopify/scripts/tres-tintas-designer-descriptions.jsM shopify/tres-tintas-desc-backups/_done.ledgerA shopify/tres-tintas-desc-backups/_done.ledger.tmp
Diff
commit e78be974b60caa3be6a73fe9a2d715be8c30fa54
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jun 23 09:20:47 2026 -0700
auto-save: 2026-06-23T09:20:41 (16 files) — shopify/scripts/cadence/sdg-trade-login.mjs shopify/scripts/tres-tintas-designer-descriptions.js shopify/tres-tintas-desc-backups/_done.ledger shopify/grid-js-fix/backups/header.liquid.live-142250278963.before shopify/grid-js-fix/backups/product-list-item.liquid.live-142250278963.before
---
.../backups/header.liquid.live-142250278963.before | 483 ++++++++++++++++++++
...oduct-list-item.liquid.live-142250278963.before | 174 +++++++
shopify/grid-js-fix/diagnose-init-throw.js | 60 +++
shopify/grid-js-fix/patched/header.liquid | 503 +++++++++++++++++++++
.../grid-js-fix/patched/product-list-item.liquid | 175 +++++++
shopify/grid-js-fix/probe-contract.js | 47 ++
shopify/grid-js-fix/probe-masonry.js | 41 ++
shopify/grid-js-fix/verify-dev-fix.js | 118 +++++
shopify/grid-js-fix/verify-dev-preview-auth.js | 49 ++
shopify/grid-js-fix/verify-final.js | 84 ++++
shopify/scripts/cadence/sdg-capture-edi.mjs | 33 ++
shopify/scripts/cadence/sdg-fetch-pricing.mjs | 23 +
shopify/scripts/cadence/sdg-recon-api.mjs | 76 ++++
shopify/scripts/cadence/sdg-trade-login.mjs | 85 +++-
.../scripts/tres-tintas-designer-descriptions.js | 12 +-
shopify/tres-tintas-desc-backups/_done.ledger | 161 +------
shopify/tres-tintas-desc-backups/_done.ledger.tmp | 0
17 files changed, 1966 insertions(+), 158 deletions(-)
diff --git a/shopify/grid-js-fix/backups/header.liquid.live-142250278963.before b/shopify/grid-js-fix/backups/header.liquid.live-142250278963.before
new file mode 100644
index 00000000..5990c5d1
--- /dev/null
+++ b/shopify/grid-js-fix/backups/header.liquid.live-142250278963.before
@@ -0,0 +1,483 @@
+<script
+ type="application/json"
+ data-section-type="static-header"
+ data-section-id="{{ section.id }}"
+ data-section-data
+>
+ {
+ "stickyHeader": {{ section.settings.sticky_header }},
+ "compactCenter": "{{ section.settings.header_layout }}",
+ "enable_predictive_search": "{{ section.settings.enable_predictive_search }}"
+ }
+</script>
+
+{%- capture arrow_right -%}
+ {%-
+ render 'icons',
+ id: 'chevron-right'
+ -%}
+{%- endcapture -%}
+
+{% assign mega_nav_handle = section.settings.mega-nav-handle %}
+
+{%- capture header_classes -%}
+ header
+ header-layout-{{ section.settings.header_layout }}
+
+ {% if section.settings.header_full_width %}
+ header-full-width
+ {% else %}
+ header-content-width
+ {% endif %}
+
+ {% if section.settings.sticky_header and section.settings.sticky_header_logo %}
+ header-sticky-show-logo
+ {% endif %}
+
+ {% if section.settings.header_border and template != 'index' %}
+ header-border
+ {% endif %}
+{%- endcapture -%}
+
+<section
+ class="{{ header_classes | replace: ' ', ' ' | strip_newlines }}"
+ {% if section.settings.sticky_header %}data-sticky-header{% endif %}
+ {% if section.settings.header_layout == 'compact-center' %}data-header-compact-center{% endif %}
+ data-section-header
+>
+ <header
+ class="main-header"
+ role="banner"
+ data-header-main
+ >
+ <div class="header-tools-wrapper">
+ <div class="header-tools">
+
+ <div class="aligned-left">
+ <div id="coin-container"></div>
+ </div>
+
+ <div class="aligned-left">
+ {% assign customer_link_blocks = section.blocks | where: 'type', 'header_customer_link' %}
+ {% for block in customer_link_blocks %}
+ <div class="customer-links" {{ block.shopify_attributes }}>
+ <a href="{{ block.settings.link_url }}">{{ block.settings.link_text }}</a>
+ </div>
+ {% endfor %}
+ </div>
+
+ <div class="aligned-right">
+ {% if shop.customer_accounts_enabled %}
+ <div class="customer-links">
+ {% if customer %}
+ <a href="{{ routes.account_url }}">{{ 'layout.header.my_account' | t }}</a>
+
+ <div class="customer-links-inner">
+ <span class="arrow"></span>
+ <ul>
+ <li><a href="{{ routes.account_url }}">{{ 'customers.general.purchases' | t }}</a></li>
+ <li><a href="{{ routes.account_addresses_url }}">{{ 'customers.addresses.header' | t }}</a></li>
+ <li>{{ 'layout.header.logout' | t | customer_logout_link }}</li>
+ </ul>
+ </div>
+ {% else %}
+ <a class="customer-login-link" href="{{ routes.account_login_url }}">{{ 'layout.header.account' | t }}</a>
+ {% endif %}
+ </div>
+ {% endif %}
+
+ <div class="mini-cart-wrapper">
+ <a class="cart-count" href="{{ routes.cart_url }}" data-cart-count>
+ <span class="cart-count-text">{{ 'layout.header.cart' | t }}</span>
+ (<span class="cart-count-number">{{ cart.item_count }}</span>)
+ </a>
+
+ {% render 'mini-cart' %}
+ </div>
+ <form class="checkout-link" action="{{ routes.cart_url }}" method="POST"><button class="checkout-link__button" type="submit" name="checkout">{{ 'layout.header.checkout' | t }}</button></form>
+ </div>
+
+ </div>
+ </div>
+
+ <div class="header-main-content" data-header-content>
+ {% unless section.settings.menu == blank %}
+ <div class="header-content-left">
+ <button
+ class="navigation-toggle navigation-toggle-open"
+ aria-label="{{ 'layout.header.menu' | t }}"
+ data-header-nav-toggle
+ >
+ <span class="navigation-toggle-icon">
+ {%-
+ render 'icons',
+ id: 'menu'
+ -%}
+ </span>
+ </button>
+ </div>
+ {% endunless %}
+
+ <div
+ class="
+ header-branding
+ header-branding-desktop
+ {% if section.settings.logo %}header-branding-desktop--has-logo{% endif %}
+ "
+ data-header-branding
+ >
+ <a class="logo-link" href="{{ routes.root_url }}">
+ {%- if section.settings.logo -%}
+ {%
+ render 'rimg',
+ img: section.settings.logo,
+ class: 'logo-image',
+ size: '200x100',
+ focal_point: section.settings.logo.presentation.focal_point,
+ %}
+ {%- else -%}
+ <h1>{{- shop.name -}}</h1>
+ {%- endif -%}
+ </a>
+ <span
+ class="navigation-toggle navigation-toggle-close"
+ data-header-nav-toggle
+ >
+ {%-
+ render 'icons',
+ id: 'close',
+ -%}
+ </span>
+ </div>
+
+ {% unless section.settings.menu == blank %}
+ <div class="navigation-wrapper" data-navigation-wrapper>
+ <nav class="navigation navigation-desktop navigation-has-mega-nav" data-navigation-content>
+ <ul class="navigation-menu">
+ {% for link in linklists[section.settings.menu].links %}
+ {% assign first_tier_link = link %}
+ {% assign uses_blocks = true %}
+ {% comment %}Inject is-dropdown-or-meganav begin{% endcomment %}
+ {% comment %}
+ A helper snippet for detecting if a link has a mega nav
+ or dropdown associated with it.
+
+ @param first_tier_link
+
+ @param uses_blocks
+ {% endcomment %}
+
+ {% assign is_dropdown = false %}
+ {% assign is_meganav = true %}
+
+ {% if first_tier_link.links == blank %}
+ {% assign is_meganav = false %}
+ {% else %}
+ {% assign is_dropdown = true %}
+ {% endif %}
+
+ {% for second_tier_link in first_tier_link.links %}
+ {% if second_tier_link.links == blank %}
+ {% assign is_meganav = false %}
+ {% else %}
+ {% assign is_dropdown = true %}
+ {% endif %}
+ {% endfor %}
+
+ {% if is_meganav %}
+ {% assign is_dropdown = false %}
+ {% endif %}
+
+ {% if uses_blocks %}
+ {% assign found_mega_item_handle = false %}
+ {% if section != blank and section.blocks != blank %}
+ {% for block in section.blocks %}
+ {% if block.type == 'mega_menu' %}
+ {% assign menu_item_handle = block.settings.menu_item | handle %}
+ {% if first_tier_link.handle == menu_item_handle %}
+ {% assign found_mega_item_handle = true %}
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+
+ {% if found_mega_item_handle %}
+ {% if is_dropdown %}
+ {% assign is_meganav = true %}
+ {% assign is_dropdown = false %}
+ {% endif %}
+ {% elsif found_mega_item_handle == false %}
+ {% if is_meganav %}
+ {% assign is_meganav = false %}
+ {% assign is_dropdown = true %}
+ {% endif %}
+ {% else %}
+ {% if is_meganav %}
+ {% assign is_meganav = false %}
+ {% assign is_dropdown = true %}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+ {% comment %}Inject is-dropdown-or-meganav end{% endcomment %}
+
+
+ <li
+ class="{% if is_dropdown %}has-dropdown{% elsif is_meganav %}has-mega-nav{% endif %}"
+ {% if is_meganav %}data-mega-nav="true"{% endif %}
+ >
+ {% if is_dropdown or is_meganav %}
+ <details data-nav-details>
+ <summary data-href="{{ link.url }}"
+ {% else %}
+ <a href="{{ link.url }}"
+ {% endif %}
+ id="main-navigation-{{ link.handle }}"
+ class="navigation-menu-link"
+ {% if is_dropdown or is_meganav %}aria-haspopup{% endif %}
+ >
+ {{ link.title }}
+ {% if is_dropdown or is_meganav %}
+ <span
+ class="navigation-submenu-toggle"
+ tabIndex="0"
+ role="button"
+ aria-controls="navigation-{{ link.handle }}"
+ aria-expanded="false"
+ aria-label="{{ 'general.accessibility.toggle' | t }}"
+ data-subnav-toggle
+ >
+ {{- arrow_right -}}
+ </span>
+ {% endif %}
+ {% if is_dropdown or is_meganav %}
+ </summary>
+ {% else %}
+ </a>
+ {% endif %}
+
+ {% if is_dropdown %}
+ {% assign direction = 'right' %}
+ {% assign halfway = linklists[section.settings.menu].links.size | divided_by: 2 %}
+ {% if forloop.index0 > halfway %}
+ {% assign direction = 'left' %}
+ {% endif %}
+
+ {%- render 'dropdown', direction: direction, link: link -%}
+ </details>
+ {% elsif is_meganav %}
+ {%- render 'mega-nav', link: link -%}
+ {% endif %}
+ </li>
+ {% endfor %}
+
+ {% if shop.customer_accounts_enabled %}
+ {% if customer %}
+ <li class="mobile-link">
+ <a href="{{ routes.account_url }}">
+ {{ 'layout.header.my_account' | t }}
+ </a>
+ </li>
+ <li class="mobile-link">
+ {{ 'layout.header.logout' | t | customer_logout_link }}
+ </li>
+ {% else %}
+ <li class="mobile-link">
+ <a class="customer-login-link" href="{{ routes.account_login_url }}">{{ 'layout.header.account' | t }}</a>
+ </li>
+ {% endif %}
+ {% endif %}
+ </ul>
+ </nav>
+ {% render 'mobile-nav', arrow_right: arrow_right %}
+ </div>
+ {% endunless %}
+
+ <div class="header-content-right header-content-right--inline-search" data-header-content-right>
+ <div class="header-search-inline">
+ {%
+ render 'search-form',
+ context: 'header',
+ %}
+ <a href="#" class="visual-search-clickable header-search-inline__visual" aria-controls="Visual Search">
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="30px" height="30px" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 20 20"><path d="M10 8a3 3 0 1 0 0 6a3 3 0 0 0 0-6zm8-3h-2.4a.888.888 0 0 1-.789-.57l-.621-1.861A.89.89 0 0 0 13.4 2H6.6c-.33 0-.686.256-.789.568L5.189 4.43A.889.889 0 0 1 4.4 5H2C.9 5 0 5.9 0 7v9c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-8 11a5 5 0 0 1-5-5a5 5 0 1 1 10 0a5 5 0 0 1-5 5zm7.5-7.8a.7.7 0 1 1 0-1.4a.7.7 0 0 1 0 1.4z" fill="#000000"/></svg>
+ </a>
+ </div>
+
+ <button
+ class="search-toggle"
+ type="button"
+ aria-label="{{ 'general.search.header' | t }}"
+ data-header-search-toggle
+ >
+ {%
+ render 'icons',
+ id: 'search',
+ %}
+ </button>
+
+ {% if section.settings.sticky_header %}
+ <a class="sticky-header-cart-count" href="{{ routes.cart_url }}" data-cart-count>
+ <span class="cart-count-text">{{ 'layout.header.cart' | t }}</span>
+ (<span class="cart-count-number">{{ cart.item_count }}</span>)
+ </a>
+ {% endif %}
+ </div>
+
+ <div class="search-takeover" data-search-takeover>
+ <div class="search-takeover__wrapper">
+ {%
+ render 'search-form',
+ context: 'mobile',
+ %}
+
+ <button
+ class="search-takeover__close-button"
+ type="button"
+ aria-label="{{ 'general.search.close' | t }}"
+ data-search-takeover-close
+ >
+ {%
+ render 'icons',
+ id: 'close',
+ %}
+ </button>
+ </div>
+ </div>
+ </div>
+ </header>
+</section>
+
+{% schema %}
+{
+ "name": "Header",
+ "settings": [
+ {
+ "type": "header",
+ "content": "Logo"
+ },
+ {
+ "type": "image_picker",
+ "id": "logo",
+ "label": "Logo image",
+ "info": "200px tall recommended"
+ },
+ {
+ "type": "header",
+ "content": "Header"
+ },
+ {
+ "type": "select",
+ "id": "header_layout",
+ "label": "Logo layout",
+ "info": "Logo layouts are only active on screens wider than 1080px",
+ "options": [
+ {
+ "value": "default",
+ "label": "Default"
+ },
+ {
+ "value": "compact-center",
+ "label": "Compact center"
+ },
+ {
+ "value": "compact-left",
+ "label": "Compact left"
+ }
+ ],
+ "default": "compact-left"
+ },
+ {
+ "type": "checkbox",
+ "id": "header_full_width",
+ "label": "Enable full width header ",
+ "default": false
+ },
+ {
+ "type": "checkbox",
+ "id": "header_border",
+ "label": "Show header border",
+ "default": true,
+ "info": "This will appear on all pages except the home page."
+ },
+ {
+ "type": "checkbox",
+ "id": "sticky_header",
+ "label": "Enable sticky header"
+ },
+ {
+ "type": "checkbox",
+ "id": "sticky_header_logo",
+ "default": true,
+ "label": "Show logo image in sticky header"
+ },
+ {
+ "type": "header",
+ "content": "Search behavior"
+ },
+ {
+ "type": "checkbox",
+ "id": "enable_predictive_search",
+ "label": "Enable predictive search",
+ "default": true
+ },
+ {
+ "type": "header",
+ "content": "Navigation"
+ },
+ {
+ "type": "link_list",
+ "id": "menu",
+ "label": "Menu",
+ "default": "main-menu"
+ },
+ {
+ "type": "header",
+ "content": "Mini Cart"
+ },
+ {
+ "type": "checkbox",
+ "id": "enable_checkout_lock_icon",
+ "label": "Show lock icon on checkout button in mini cart",
+ "default": false
+ }
+ ],
+ "blocks": [
+ {
+ "type": "mega_menu",
+ "name": "Mega menu",
+ "settings": [
+ {
+ "type": "text",
+ "id": "menu_item",
+ "label": "Menu item",
+ "info": "Enter menu item to apply a mega menu dropdown. [Learn more](https://support.pixelunion.net/hc/en-us/articles/360022052534-How-to-set-up-your-store-s-navigation)"
+ },
+ {
+ "type": "image_picker",
+ "id": "menu_image",
+ "label": "Featured image",
+ "info": "480 x 1000px recommended"
+ }
+ ]
+ },
+ {
+ "type": "header_customer_link",
+ "name": "Header customer link",
+ "settings": [
+ {
+ "type": "text",
+ "id": "link_text",
+ "label": "Link text",
+ "default": "Contact us: (888) 373-4564"
+ },
+ {
+ "type": "url",
+ "id": "link_url",
+ "label": "Link URL"
+ }
+ ]
+ }
+ ]
+}
+
+{% endschema %}
\ No newline at end of file
diff --git a/shopify/grid-js-fix/backups/product-list-item.liquid.live-142250278963.before b/shopify/grid-js-fix/backups/product-list-item.liquid.live-142250278963.before
new file mode 100644
index 00000000..d1a115fd
--- /dev/null
+++ b/shopify/grid-js-fix/backups/product-list-item.liquid.live-142250278963.before
@@ -0,0 +1,174 @@
+{% comment %}
+ @param product_attributes
+ custom attributes to be applied to the product item
+ Defaults to: blank
+{% endcomment %}
+
+{% liquid
+ assign product_attributes = product_attributes | default: ''
+ assign product_hover = settings.product_hover | default: 'quick-shop'
+ assign product_stock_level_threshold = settings.product_stock_level_threshold | default: 1
+ assign product_badges = settings.product_badges | default: false
+ assign product_icons = settings.product_badges_icons | default: false
+ assign product_vendor = settings.show_vendor | default: false
+
+ assign item = product
+ if template contains 'search'
+ assign item = item
+ endif
+
+ assign has_quick_shop = false
+ if product_hover == 'quick-shop' and template.name != 'password' and product.variants_count <= 250
+ assign has_quick_shop = true
+ endif
+%}
+
+{% if product_hover == 'stock-level' and item.available %}
+ {% assign total = 0 %}
+ {% assign threshold = product_stock_level_threshold | times: 1 %}
+ {% assign infinity = false %}
+ {% for variant in item.variants %}
+ {% if variant.inventory_management == null %}
+ {% assign infinity = true %}
+ {% elsif variant.inventory_management == '' %}
+ {% assign infinity = true %}
+ {% elsif variant.inventory_management == 'shopify' and variant.inventory_policy == 'continue' %}
+ {% assign infinity = true %}
+ {% elsif infinity == false %}
+ {% capture temp %}{{ total | plus: variant.inventory_quantity }}{% endcapture %}
+ {% assign total = temp | times: 1 %}
+ {% endif %}
+ {% endfor %}
+ {% assign stockText = 'products.product.stock_indicator_message' | t: num: total %}
+{% endif %}
+
+<article
+ class="
+ product-list-item
+ {% if has_quick_shop %} has-quick-shop{% endif %}
+ {% if item.available and infinity == false and total <= threshold %} has-stock-indicator{%endif %}
+ "
+ id="product-list-item-{{ item.id }}"
+ data-product-id="{{ item.id }}"
+ {{ product_attributes }}
+>
+
+ {% assign secondaryImage = false %}
+ {% if item.media.size > 1 and product_hover == 'image-flip' %}
+ {% assign secondaryImage = true %}
+ {% endif %}
+
+ <figure
+ class="
+ product-list-item-thumbnail
+ {% if secondaryImage %}
+ has-secondary-image
+ {% endif %}
+ "
+ data-url="{{ item.url | within: collection }}"
+ {% if secondaryImage %}
+ {%
+ render 'rimg',
+ img: item.media[1].preview_image,
+ alt: item.media[1].preview_image.alt,
+ size: '600x600',
+ background: true,
+ lazy: true
+ %}
+ {% endif %}
+ >
+ <a href="{{ item.url | within: collection }}" aria-label="{{ item.title }}">
+ {% if item.featured_media.preview_image %}
+ {%
+ render 'rimg',
+ img: item.featured_media.preview_image,
+ alt: item.featured_media.preview_image.alt,
+ size: '600x600',
+ lazy: true
+ %}
+ {% else %}
+ {{ 'product-1' | placeholder_svg_tag: 'placeholder-svg' }}
+ {% endif %}
+ </a>
+
+ {% if has_quick_shop %}
+ <span
+ class="quick-shop-modal-trigger"
+ data-product-url="{{ item.url | within: collection }}"
+ >
+ {{ 'products.product.quick_shop_trigger_text' | t }}
+ </span>
+ {% elsif product_hover == 'stock-level' %}
+
+ {% if item.available and infinity == false and total <= threshold %}
+ <a class="product-list-item-inventory" href="{{ item.url }}">{{ stockText }}</a>
+ {% endif %}
+
+ {% endif %}
+
+ {% if product_badges %}
+ {% if item.available != true %}
+ <span class="product-list-item-unavailable{% if product_icons %} product-icons{% endif %}" data-title="{{ 'products.product.sold_out' | t }}"></span>
+ {% elsif item.compare_at_price_min > item.price_min %}
+ <span class="product-list-item-on-sale{% if product_icons %} product-icons{% endif %}" data-title="{{ 'products.product.on_sale' | t }}"></span>
+ {% endif %}
+ {% endif %}
+
+ <!-- Hover overlay: vendor + pattern name only -->
+ <div class="product-list-item-hover-overlay">
+ <div class="product-list-item-hover-content">
+ <p class="product-list-item-hover-vendor">{{ item.vendor }}</p>
+ <h3 class="product-list-item-hover-title">{{ item.title }}</h3>
+ </div>
+ </div>
+ </figure>
+
+ <div class="product-list-item-details" style="display:none;"></div>
+ {% assign variant_for_unit_price = item.variants | sort: 'price' | first %}
+ {% if variant_for_unit_price.unit_price %}
+ {% comment %}Inject unit-price begin{% endcomment %}
+ {% comment %}
+ @param variant_for_unit_price
+ Product variant for price
+ @param tax_text
+ String containing 'tax included' text
+ {% endcomment %}
+
+ {% capture total_quantity %}
+ <span class="product-price__unit-price-total-quantity" data-unit-price-quantity>
+ {{ variant_for_unit_price.unit_price_measurement.quantity_value }}{{ variant_for_unit_price.unit_price_measurement.quantity_unit }}
+ </span>
+ {% endcapture %}
+
+
+ {% capture unit_price %}
+ <span class="product-price__unit-price-amount money" data-unit-price-amount>
+ {{ variant_for_unit_price.unit_price | money }}
+ </span>
+ {% endcapture %}
+ {% capture unit_measure %}
+ <span class="product-price__unit-price-measure" data-unit-price-measure>
+ {%- if variant_for_unit_price.unit_price_measurement.reference_value != 1 -%}
+ {{ variant_for_unit_price.unit_price_measurement.reference_value }}
+ {%- endif %}
+ {{ variant_for_unit_price.unit_price_measurement.reference_unit }}
+ </span>
+ {% endcapture %}
+
+ <div
+ class="
+ product-price__unit-price
+ {% unless variant_for_unit_price.unit_price_measurement %}hidden{% endunless %}
+ "
+ data-unit-price
+ >
+ {{ 'products.product.price_per_unit_html' | t: total_quantity: total_quantity, unit_price: unit_price, unit_measure: unit_measure | strip_newlines }}
+ </div>
+
+ {% assign variant_for_unit_price = blank %}
+ {% comment %}Inject unit-price end{% endcomment %}
+
+ {% endif %}
+ </div>
+
+</article>
\ No newline at end of file
diff --git a/shopify/grid-js-fix/diagnose-init-throw.js b/shopify/grid-js-fix/diagnose-init-throw.js
new file mode 100644
index 00000000..03f7a1f7
--- /dev/null
+++ b/shopify/grid-js-fix/diagnose-init-throw.js
@@ -0,0 +1,60 @@
+/*
+ * Pin the EXACT throw that aborts grid.js entry init on the live DW markup.
+ * Loads live home + collection, captures full pageerror stacks (incl. webpack
+ * module frames), and reports the first uncaught error with its stack so we can
+ * identify the null DOM element the entry script dereferences.
+ *
+ * Runs against the live (broken-dev) asset AND the staged prod asset so we see
+ * both stacks.
+ */
+const fs = require('fs');
+const { chromium } = require('playwright');
+const PROD_BUNDLE = fs.readFileSync('/tmp/grid_PROD_candidate.js', 'utf8');
+const PAGES = [
+ { name: 'HOME', url: 'https://www.designerwallcoverings.com/' },
+ { name: 'COLLECTION', url: 'https://www.designerwallcoverings.com/collections/acoustical?pf_t_product_type=Wallcovering&pf_t_color=Green' },
+];
+
+(async () => {
+ const browser = await chromium.launch({ headless: true, executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' });
+
+ for (const variant of ['LIVE-dev-asset', 'STAGED-prod-asset']) {
+ console.log(`\n================ ${variant} ================`);
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
+ if (variant === 'STAGED-prod-asset') {
+ await ctx.route('**/assets/grid.js**', route => route.fulfill({ status: 200, contentType: 'application/javascript', body: PROD_BUNDLE }));
+ }
+ const page = await ctx.newPage();
+ const fullErrors = [];
+ page.on('pageerror', e => fullErrors.push(e.stack || String(e)));
+ for (const p of PAGES) {
+ fullErrors.length = 0;
+ try { await page.goto(p.url, { waitUntil: 'domcontentloaded', timeout: 45000 }); } catch(e){}
+ await page.waitForTimeout(3000);
+ console.log(`\n --- [${p.name}] first 2 uncaught stacks ---`);
+ const seen = new Set();
+ let shown = 0;
+ for (const st of fullErrors) {
+ const key = st.split('\n')[0];
+ if (seen.has(key)) continue; seen.add(key);
+ console.log(' >>> ' + st.split('\n').slice(0, 6).join('\n '));
+ if (++shown >= 2) break;
+ }
+ // Probe presence of elements OOTS Grid entry typically derefs
+ const probe = await page.evaluate(() => {
+ const sel = [
+ '[data-section-type]','[data-section-id]','#PageContainer','#shopify-section-header',
+ '.site-header','[data-header-section]','.header-layout','[data-predictive-search]',
+ '.js-drawer-open','#NavDrawer','[data-collapsible-trigger]','.home-products-content',
+ 'main[role="main"]','#MainContent','[data-search-form]','.grid-product__link',
+ ];
+ const out = {};
+ for (const s of sel) out[s] = document.querySelectorAll(s).length;
+ return out;
+ });
+ console.log(' element presence:', JSON.stringify(probe));
+ }
+ await ctx.close();
+ }
+ await browser.close();
+})().catch(e=>{console.error('FATAL',e);process.exit(1);});
diff --git a/shopify/grid-js-fix/patched/header.liquid b/shopify/grid-js-fix/patched/header.liquid
new file mode 100644
index 00000000..af5c69c5
--- /dev/null
+++ b/shopify/grid-js-fix/patched/header.liquid
@@ -0,0 +1,503 @@
+<script
+ type="application/json"
+ data-section-type="static-header"
+ data-section-id="{{ section.id }}"
+ data-section-data
+>
+ {
+ "stickyHeader": {{ section.settings.sticky_header }},
+ "compactCenter": "{{ section.settings.header_layout }}",
+ "enable_predictive_search": "{{ section.settings.enable_predictive_search }}"
+ }
+</script>
+
+{%- capture arrow_right -%}
+ {%-
+ render 'icons',
+ id: 'chevron-right'
+ -%}
+{%- endcapture -%}
+
+{% assign mega_nav_handle = section.settings.mega-nav-handle %}
+
+{%- capture header_classes -%}
+ header
+ header-layout-{{ section.settings.header_layout }}
+
+ {% if section.settings.header_full_width %}
+ header-full-width
+ {% else %}
+ header-content-width
+ {% endif %}
+
+ {% if section.settings.sticky_header and section.settings.sticky_header_logo %}
+ header-sticky-show-logo
+ {% endif %}
+
+ {% if section.settings.header_border and template != 'index' %}
+ header-border
+ {% endif %}
+{%- endcapture -%}
+
+<section
+ class="{{ header_classes | replace: ' ', ' ' | strip_newlines }}"
+ {% if section.settings.sticky_header %}data-sticky-header{% endif %}
+ {% if section.settings.header_layout == 'compact-center' %}data-header-compact-center{% endif %}
+ data-section-header
+>
+ <header
+ class="main-header"
+ role="banner"
+ data-header-main
+ >
+ <div class="header-tools-wrapper">
+ <div class="header-tools">
+
+ <div class="aligned-left">
+ <div id="coin-container"></div>
+ </div>
+
+ <div class="aligned-left">
+ {% assign customer_link_blocks = section.blocks | where: 'type', 'header_customer_link' %}
+ {% for block in customer_link_blocks %}
+ <div class="customer-links" {{ block.shopify_attributes }}>
+ <a href="{{ block.settings.link_url }}">{{ block.settings.link_text }}</a>
+ </div>
+ {% endfor %}
+ </div>
+
+ <div class="aligned-right">
+ {% if shop.customer_accounts_enabled %}
+ <div class="customer-links">
+ {% if customer %}
+ <a href="{{ routes.account_url }}">{{ 'layout.header.my_account' | t }}</a>
+
+ <div class="customer-links-inner">
+ <span class="arrow"></span>
+ <ul>
+ <li><a href="{{ routes.account_url }}">{{ 'customers.general.purchases' | t }}</a></li>
+ <li><a href="{{ routes.account_addresses_url }}">{{ 'customers.addresses.header' | t }}</a></li>
+ <li>{{ 'layout.header.logout' | t | customer_logout_link }}</li>
+ </ul>
+ </div>
+ {% else %}
+ <a class="customer-login-link" href="{{ routes.account_login_url }}">{{ 'layout.header.account' | t }}</a>
+ {% endif %}
+ </div>
+ {% endif %}
+
+ <div class="mini-cart-wrapper">
+ <a class="cart-count" href="{{ routes.cart_url }}" data-cart-count>
+ <span class="cart-count-text">{{ 'layout.header.cart' | t }}</span>
+ (<span class="cart-count-number">{{ cart.item_count }}</span>)
+ </a>
+
+ {% render 'mini-cart' %}
+ </div>
+ <form class="checkout-link" action="{{ routes.cart_url }}" method="POST"><button class="checkout-link__button" type="submit" name="checkout">{{ 'layout.header.checkout' | t }}</button></form>
+ </div>
+
+ </div>
+ </div>
+
+ <div class="header-main-content" data-header-content>
+ {% unless section.settings.menu == blank %}
+ <div class="header-content-left">
+ <button
+ class="navigation-toggle navigation-toggle-open"
+ aria-label="{{ 'layout.header.menu' | t }}"
+ data-header-nav-toggle
+ >
+ <span class="navigation-toggle-icon">
+ {%-
+ render 'icons',
+ id: 'menu'
+ -%}
+ </span>
+ </button>
+ </div>
+ {% endunless %}
+
+ <div
+ class="
+ header-branding
+ header-branding-desktop
+ {% if section.settings.logo %}header-branding-desktop--has-logo{% endif %}
+ "
+ data-header-branding
+ >
+ <a class="logo-link" href="{{ routes.root_url }}">
+ {%- if section.settings.logo -%}
+ {%
+ render 'rimg',
+ img: section.settings.logo,
+ class: 'logo-image',
+ size: '200x100',
+ focal_point: section.settings.logo.presentation.focal_point,
+ %}
+ {%- else -%}
+ <h1>{{- shop.name -}}</h1>
+ {%- endif -%}
+ </a>
+ <span
+ class="navigation-toggle navigation-toggle-close"
+ data-header-nav-toggle
+ >
+ {%-
+ render 'icons',
+ id: 'close',
+ -%}
+ </span>
+ </div>
+
+ {% unless section.settings.menu == blank %}
+ <div class="navigation-wrapper" data-navigation-wrapper>
+ <nav class="navigation navigation-desktop navigation-has-mega-nav" data-navigation-content>
+ <ul class="navigation-menu">
+ {% for link in linklists[section.settings.menu].links %}
+ {% assign first_tier_link = link %}
+ {% assign uses_blocks = true %}
+ {% comment %}Inject is-dropdown-or-meganav begin{% endcomment %}
+ {% comment %}
+ A helper snippet for detecting if a link has a mega nav
+ or dropdown associated with it.
+
+ @param first_tier_link
+
+ @param uses_blocks
+ {% endcomment %}
+
+ {% assign is_dropdown = false %}
+ {% assign is_meganav = true %}
+
+ {% if first_tier_link.links == blank %}
+ {% assign is_meganav = false %}
+ {% else %}
+ {% assign is_dropdown = true %}
+ {% endif %}
+
+ {% for second_tier_link in first_tier_link.links %}
+ {% if second_tier_link.links == blank %}
+ {% assign is_meganav = false %}
+ {% else %}
+ {% assign is_dropdown = true %}
+ {% endif %}
+ {% endfor %}
+
+ {% if is_meganav %}
+ {% assign is_dropdown = false %}
+ {% endif %}
+
+ {% if uses_blocks %}
+ {% assign found_mega_item_handle = false %}
+ {% if section != blank and section.blocks != blank %}
+ {% for block in section.blocks %}
+ {% if block.type == 'mega_menu' %}
+ {% assign menu_item_handle = block.settings.menu_item | handle %}
+ {% if first_tier_link.handle == menu_item_handle %}
+ {% assign found_mega_item_handle = true %}
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+
+ {% if found_mega_item_handle %}
+ {% if is_dropdown %}
+ {% assign is_meganav = true %}
+ {% assign is_dropdown = false %}
+ {% endif %}
+ {% elsif found_mega_item_handle == false %}
+ {% if is_meganav %}
+ {% assign is_meganav = false %}
+ {% assign is_dropdown = true %}
+ {% endif %}
+ {% else %}
+ {% if is_meganav %}
+ {% assign is_meganav = false %}
+ {% assign is_dropdown = true %}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+ {% comment %}Inject is-dropdown-or-meganav end{% endcomment %}
+
+
+ <li
+ class="{% if is_dropdown %}has-dropdown{% elsif is_meganav %}has-mega-nav{% endif %}"
+ {% if is_meganav %}data-mega-nav="true"{% endif %}
+ >
+ {% if is_dropdown or is_meganav %}
+ <details data-nav-details>
+ <summary data-href="{{ link.url }}"
+ {% else %}
+ <a href="{{ link.url }}"
+ {% endif %}
+ id="main-navigation-{{ link.handle }}"
+ class="navigation-menu-link"
+ {% if is_dropdown or is_meganav %}aria-haspopup{% endif %}
+ >
+ {{ link.title }}
+ {% if is_dropdown or is_meganav %}
+ <span
+ class="navigation-submenu-toggle"
+ tabIndex="0"
+ role="button"
+ aria-controls="navigation-{{ link.handle }}"
+ aria-expanded="false"
+ aria-label="{{ 'general.accessibility.toggle' | t }}"
+ data-subnav-toggle
+ >
+ {{- arrow_right -}}
+ </span>
+ {% endif %}
+ {% if is_dropdown or is_meganav %}
+ </summary>
+ {% else %}
+ </a>
+ {% endif %}
+
+ {% if is_dropdown %}
+ {% assign direction = 'right' %}
+ {% assign halfway = linklists[section.settings.menu].links.size | divided_by: 2 %}
+ {% if forloop.index0 > halfway %}
+ {% assign direction = 'left' %}
+ {% endif %}
+
+ {%- render 'dropdown', direction: direction, link: link -%}
+ </details>
+ {% elsif is_meganav %}
+ {%- render 'mega-nav', link: link -%}
+ {% endif %}
+ </li>
+ {% endfor %}
+
+ {% if shop.customer_accounts_enabled %}
+ {% if customer %}
+ <li class="mobile-link">
+ <a href="{{ routes.account_url }}">
+ {{ 'layout.header.my_account' | t }}
+ </a>
+ </li>
+ <li class="mobile-link">
+ {{ 'layout.header.logout' | t | customer_logout_link }}
+ </li>
+ {% else %}
+ <li class="mobile-link">
+ <a class="customer-login-link" href="{{ routes.account_login_url }}">{{ 'layout.header.account' | t }}</a>
+ </li>
+ {% endif %}
+ {% endif %}
+ </ul>
+ </nav>
+ {% render 'mobile-nav', arrow_right: arrow_right %}
+ </div>
+ {% endunless %}
+
+ <div class="header-content-right header-content-right--inline-search" data-header-content-right>
+ <div class="header-search-inline">
+ {%
+ render 'search-form',
+ context: 'header',
+ %}
+ <a href="#" class="visual-search-clickable header-search-inline__visual" aria-controls="Visual Search">
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="30px" height="30px" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 20 20"><path d="M10 8a3 3 0 1 0 0 6a3 3 0 0 0 0-6zm8-3h-2.4a.888.888 0 0 1-.789-.57l-.621-1.861A.89.89 0 0 0 13.4 2H6.6c-.33 0-.686.256-.789.568L5.189 4.43A.889.889 0 0 1 4.4 5H2C.9 5 0 5.9 0 7v9c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-8 11a5 5 0 0 1-5-5a5 5 0 1 1 10 0a5 5 0 0 1-5 5zm7.5-7.8a.7.7 0 1 1 0-1.4a.7.7 0 0 1 0 1.4z" fill="#000000"/></svg>
+ </a>
+ </div>
+
+ {%- comment -%}
+ DW grid-init contract shim (2026-06-23): the OOTS Grid `StaticHeader`
+ JS class (in grid.js) constructs against `[data-header-search]`,
+ `[data-header-search-button]` and `[data-header-search-button-close]`
+ and dereferences them with NO null guard
+ (`this.headerSearch.style.width`, `headerSearchSubmit.addEventListener`,
+ `headerSearchClose.addEventListener`). DW's custom inline-search +
+ search-takeover UI replaced the original OOTS search form and removed
+ those hooks, so `new StaticHeader` threw and aborted
+ `ShopifySectionsManager._initSections` BEFORE any product-grid section
+ could init Masonry/Flickity — every grid then rendered at native image
+ resolution. This visually-hidden, inert shim restores the three hooks
+ so StaticHeader constructs cleanly. It is display:none, aria-hidden,
+ and not focusable, so it has zero impact on the visible header UX.
+ {%- endcomment -%}
+ <div class="header-search" data-header-search aria-hidden="true" tabindex="-1" style="display:none !important;width:0;height:0;overflow:hidden;position:absolute;left:-9999px;" data-width="0">
+ <button class="header-search__submit" data-header-search-button type="button" tabindex="-1" aria-hidden="true"></button>
+ <button class="header-search__close" data-header-search-button-close type="button" tabindex="-1" aria-hidden="true"></button>
+ </div>
+
+ <button
+ class="search-toggle"
+ type="button"
+ aria-label="{{ 'general.search.header' | t }}"
+ data-header-search-toggle
+ >
+ {%
+ render 'icons',
+ id: 'search',
+ %}
+ </button>
+
+ {% if section.settings.sticky_header %}
+ <a class="sticky-header-cart-count" href="{{ routes.cart_url }}" data-cart-count>
+ <span class="cart-count-text">{{ 'layout.header.cart' | t }}</span>
+ (<span class="cart-count-number">{{ cart.item_count }}</span>)
+ </a>
+ {% endif %}
+ </div>
+
+ <div class="search-takeover" data-search-takeover>
+ <div class="search-takeover__wrapper">
+ {%
+ render 'search-form',
+ context: 'mobile',
+ %}
+
+ <button
+ class="search-takeover__close-button"
+ type="button"
+ aria-label="{{ 'general.search.close' | t }}"
+ data-search-takeover-close
+ >
+ {%
+ render 'icons',
+ id: 'close',
+ %}
+ </button>
+ </div>
+ </div>
+ </div>
+ </header>
+</section>
+
+{% schema %}
+{
+ "name": "Header",
+ "settings": [
+ {
+ "type": "header",
+ "content": "Logo"
+ },
+ {
+ "type": "image_picker",
+ "id": "logo",
+ "label": "Logo image",
+ "info": "200px tall recommended"
+ },
+ {
+ "type": "header",
+ "content": "Header"
+ },
+ {
+ "type": "select",
+ "id": "header_layout",
+ "label": "Logo layout",
+ "info": "Logo layouts are only active on screens wider than 1080px",
+ "options": [
+ {
+ "value": "default",
+ "label": "Default"
+ },
+ {
+ "value": "compact-center",
+ "label": "Compact center"
+ },
+ {
+ "value": "compact-left",
+ "label": "Compact left"
+ }
+ ],
+ "default": "compact-left"
+ },
+ {
+ "type": "checkbox",
+ "id": "header_full_width",
+ "label": "Enable full width header ",
+ "default": false
+ },
+ {
+ "type": "checkbox",
+ "id": "header_border",
+ "label": "Show header border",
+ "default": true,
+ "info": "This will appear on all pages except the home page."
+ },
+ {
+ "type": "checkbox",
+ "id": "sticky_header",
+ "label": "Enable sticky header"
+ },
+ {
+ "type": "checkbox",
+ "id": "sticky_header_logo",
+ "default": true,
+ "label": "Show logo image in sticky header"
+ },
+ {
+ "type": "header",
+ "content": "Search behavior"
+ },
+ {
+ "type": "checkbox",
+ "id": "enable_predictive_search",
+ "label": "Enable predictive search",
+ "default": true
+ },
+ {
+ "type": "header",
+ "content": "Navigation"
+ },
+ {
+ "type": "link_list",
+ "id": "menu",
+ "label": "Menu",
+ "default": "main-menu"
+ },
+ {
+ "type": "header",
+ "content": "Mini Cart"
+ },
+ {
+ "type": "checkbox",
+ "id": "enable_checkout_lock_icon",
+ "label": "Show lock icon on checkout button in mini cart",
+ "default": false
+ }
+ ],
+ "blocks": [
+ {
+ "type": "mega_menu",
+ "name": "Mega menu",
+ "settings": [
+ {
+ "type": "text",
+ "id": "menu_item",
+ "label": "Menu item",
+ "info": "Enter menu item to apply a mega menu dropdown. [Learn more](https://support.pixelunion.net/hc/en-us/articles/360022052534-How-to-set-up-your-store-s-navigation)"
+ },
+ {
+ "type": "image_picker",
+ "id": "menu_image",
+ "label": "Featured image",
+ "info": "480 x 1000px recommended"
+ }
+ ]
+ },
+ {
+ "type": "header_customer_link",
+ "name": "Header customer link",
+ "settings": [
+ {
+ "type": "text",
+ "id": "link_text",
+ "label": "Link text",
+ "default": "Contact us: (888) 373-4564"
+ },
+ {
+ "type": "url",
+ "id": "link_url",
+ "label": "Link URL"
+ }
+ ]
+ }
+ ]
+}
+
+{% endschema %}
\ No newline at end of file
diff --git a/shopify/grid-js-fix/patched/product-list-item.liquid b/shopify/grid-js-fix/patched/product-list-item.liquid
new file mode 100644
index 00000000..cf72cba5
--- /dev/null
+++ b/shopify/grid-js-fix/patched/product-list-item.liquid
@@ -0,0 +1,175 @@
+{% comment %}
+ @param product_attributes
+ custom attributes to be applied to the product item
+ Defaults to: blank
+{% endcomment %}
+
+{% liquid
+ assign product_attributes = product_attributes | default: ''
+ assign product_hover = settings.product_hover | default: 'quick-shop'
+ assign product_stock_level_threshold = settings.product_stock_level_threshold | default: 1
+ assign product_badges = settings.product_badges | default: false
+ assign product_icons = settings.product_badges_icons | default: false
+ assign product_vendor = settings.show_vendor | default: false
+
+ assign item = product
+ if template contains 'search'
+ assign item = item
+ endif
+
+ assign has_quick_shop = false
+ if product_hover == 'quick-shop' and template.name != 'password' and product.variants_count <= 250
+ assign has_quick_shop = true
+ endif
+%}
+
+{% if product_hover == 'stock-level' and item.available %}
+ {% assign total = 0 %}
+ {% assign threshold = product_stock_level_threshold | times: 1 %}
+ {% assign infinity = false %}
+ {% for variant in item.variants %}
+ {% if variant.inventory_management == null %}
+ {% assign infinity = true %}
+ {% elsif variant.inventory_management == '' %}
+ {% assign infinity = true %}
+ {% elsif variant.inventory_management == 'shopify' and variant.inventory_policy == 'continue' %}
+ {% assign infinity = true %}
+ {% elsif infinity == false %}
+ {% capture temp %}{{ total | plus: variant.inventory_quantity }}{% endcapture %}
+ {% assign total = temp | times: 1 %}
+ {% endif %}
+ {% endfor %}
+ {% assign stockText = 'products.product.stock_indicator_message' | t: num: total %}
+{% endif %}
+
+<article
+ class="
+ product-list-item
+ {% if has_quick_shop %} has-quick-shop{% endif %}
+ {% if item.available and infinity == false and total <= threshold %} has-stock-indicator{%endif %}
+ "
+ id="product-list-item-{{ item.id }}"
+ data-product-id="{{ item.id }}"
+ data-product-hover="{{ product_hover }}"
+ {{ product_attributes }}
+>
+
+ {% assign secondaryImage = false %}
+ {% if item.media.size > 1 and product_hover == 'image-flip' %}
+ {% assign secondaryImage = true %}
+ {% endif %}
+
+ <figure
+ class="
+ product-list-item-thumbnail
+ {% if secondaryImage %}
+ has-secondary-image
+ {% endif %}
+ "
+ data-url="{{ item.url | within: collection }}"
+ {% if secondaryImage %}
+ {%
+ render 'rimg',
+ img: item.media[1].preview_image,
+ alt: item.media[1].preview_image.alt,
+ size: '600x600',
+ background: true,
+ lazy: true
+ %}
+ {% endif %}
+ >
+ <a href="{{ item.url | within: collection }}" aria-label="{{ item.title }}">
+ {% if item.featured_media.preview_image %}
+ {%
+ render 'rimg',
+ img: item.featured_media.preview_image,
+ alt: item.featured_media.preview_image.alt,
+ size: '600x600',
+ lazy: true
+ %}
+ {% else %}
+ {{ 'product-1' | placeholder_svg_tag: 'placeholder-svg' }}
+ {% endif %}
+ </a>
+
+ {% if has_quick_shop %}
+ <span
+ class="quick-shop-modal-trigger"
+ data-product-url="{{ item.url | within: collection }}"
+ >
+ {{ 'products.product.quick_shop_trigger_text' | t }}
+ </span>
+ {% elsif product_hover == 'stock-level' %}
+
+ {% if item.available and infinity == false and total <= threshold %}
+ <a class="product-list-item-inventory" href="{{ item.url }}">{{ stockText }}</a>
+ {% endif %}
+
+ {% endif %}
+
+ {% if product_badges %}
+ {% if item.available != true %}
+ <span class="product-list-item-unavailable{% if product_icons %} product-icons{% endif %}" data-title="{{ 'products.product.sold_out' | t }}"></span>
+ {% elsif item.compare_at_price_min > item.price_min %}
+ <span class="product-list-item-on-sale{% if product_icons %} product-icons{% endif %}" data-title="{{ 'products.product.on_sale' | t }}"></span>
+ {% endif %}
+ {% endif %}
+
+ <!-- Hover overlay: vendor + pattern name only -->
+ <div class="product-list-item-hover-overlay">
+ <div class="product-list-item-hover-content">
+ <p class="product-list-item-hover-vendor">{{ item.vendor }}</p>
+ <h3 class="product-list-item-hover-title">{{ item.title }}</h3>
+ </div>
+ </div>
+ </figure>
+
+ <div class="product-list-item-details" style="display:none;"></div>
+ {% assign variant_for_unit_price = item.variants | sort: 'price' | first %}
+ {% if variant_for_unit_price.unit_price %}
+ {% comment %}Inject unit-price begin{% endcomment %}
+ {% comment %}
+ @param variant_for_unit_price
+ Product variant for price
+ @param tax_text
+ String containing 'tax included' text
+ {% endcomment %}
+
+ {% capture total_quantity %}
+ <span class="product-price__unit-price-total-quantity" data-unit-price-quantity>
+ {{ variant_for_unit_price.unit_price_measurement.quantity_value }}{{ variant_for_unit_price.unit_price_measurement.quantity_unit }}
+ </span>
+ {% endcapture %}
+
+
+ {% capture unit_price %}
+ <span class="product-price__unit-price-amount money" data-unit-price-amount>
+ {{ variant_for_unit_price.unit_price | money }}
+ </span>
+ {% endcapture %}
+ {% capture unit_measure %}
+ <span class="product-price__unit-price-measure" data-unit-price-measure>
+ {%- if variant_for_unit_price.unit_price_measurement.reference_value != 1 -%}
+ {{ variant_for_unit_price.unit_price_measurement.reference_value }}
+ {%- endif %}
+ {{ variant_for_unit_price.unit_price_measurement.reference_unit }}
+ </span>
+ {% endcapture %}
+
+ <div
+ class="
+ product-price__unit-price
+ {% unless variant_for_unit_price.unit_price_measurement %}hidden{% endunless %}
+ "
+ data-unit-price
+ >
+ {{ 'products.product.price_per_unit_html' | t: total_quantity: total_quantity, unit_price: unit_price, unit_measure: unit_measure | strip_newlines }}
+ </div>
+
+ {% assign variant_for_unit_price = blank %}
+ {% comment %}Inject unit-price end{% endcomment %}
+
+ {% endif %}
+ </div>
+
+</article>
\ No newline at end of file
diff --git a/shopify/grid-js-fix/probe-contract.js b/shopify/grid-js-fix/probe-contract.js
new file mode 100644
index 00000000..1f66b70d
--- /dev/null
+++ b/shopify/grid-js-fix/probe-contract.js
@@ -0,0 +1,47 @@
+/* Probe the exact DOM elements the throwing grid.js modules dereference. */
+const { chromium } = require('playwright');
+(async () => {
+ const browser = await chromium.launch({ headless: true, executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' });
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
+ const page = await ctx.newPage();
+ for (const [name, url] of [
+ ['HOME','https://www.designerwallcoverings.com/'],
+ ['COLLECTION','https://www.designerwallcoverings.com/collections/acoustical?pf_t_product_type=Wallcovering&pf_t_color=Green'],
+ ]) {
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(()=>{});
+ await page.waitForTimeout(2500);
+ const r = await page.evaluate(() => {
+ const q = s => document.querySelectorAll(s).length;
+ // What StaticHeader._setSearchWidth + constructor need:
+ const header = {
+ 'data-section-type=header': q('[data-section-type="header"]'),
+ '.main-header': q('.main-header'),
+ '.mini-cart-wrapper': q('.mini-cart-wrapper'),
+ '.checkout-link': q('.checkout-link'),
+ '.header-search (headerSearch)': q('.header-search'),
+ '[data-header-search]': q('[data-header-search]'),
+ '.navigation-wrapper': q('.navigation-wrapper'),
+ };
+ // What StaticCollection + ProductListItem need:
+ const coll = {
+ 'data-section-type=static-collection': q('[data-section-type="static-collection"]'),
+ '.product-list-item': q('.product-list-item'),
+ '.product-list-item-thumbnail': q('.product-list-item-thumbnail'),
+ '[data-product-hover]': q('[data-product-hover]'),
+ '.quick-shop-modal-trigger': q('.quick-shop-modal-trigger'),
+ // grid containers Masonry/Flickity target:
+ '.product-grid': q('.product-grid'),
+ '[data-product-grid]': q('[data-product-grid]'),
+ '.grid--view-items': q('.grid--view-items'),
+ };
+ // list every data-section-type present
+ const types = [...document.querySelectorAll('[data-section-type]')].map(e=>e.getAttribute('data-section-type'));
+ return { header, coll, sectionTypes: [...new Set(types)] };
+ });
+ console.log(`\n===== ${name} =====`);
+ console.log(' sectionTypes:', JSON.stringify(r.sectionTypes));
+ console.log(' HEADER contract:', JSON.stringify(r.header, null, 0));
+ console.log(' COLLECTION contract:', JSON.stringify(r.coll, null, 0));
+ }
+ await browser.close();
+})().catch(e=>{console.error(e);process.exit(1);});
diff --git a/shopify/grid-js-fix/probe-masonry.js b/shopify/grid-js-fix/probe-masonry.js
new file mode 100644
index 00000000..26a4b70e
--- /dev/null
+++ b/shopify/grid-js-fix/probe-masonry.js
@@ -0,0 +1,41 @@
+const { chromium } = require('playwright');
+(async () => {
+ const b = await chromium.launch({ headless: true, executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' });
+ const ctx = await b.newContext({ viewport: { width: 1440, height: 1200 } });
+ const page = await ctx.newPage();
+ for (const [name, url] of [
+ ['COLLECTION', 'https://www.designerwallcoverings.com/collections/acoustical?pf_t_product_type=Wallcovering&pf_t_color=Green'],
+ ['HOME', 'https://www.designerwallcoverings.com/'],
+ ]) {
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(()=>{});
+ await page.waitForTimeout(2500);
+ const r = await page.evaluate(() => {
+ const q = s => document.querySelectorAll(s).length;
+ // The masonry container the JS needs:
+ const need = {
+ '[data-masonry-grid]': q('[data-masonry-grid]'),
+ '[data-collection-sorting]': q('[data-collection-sorting]'),
+ '.product-list': q('.product-list'),
+ '.product-list-view': q('.product-list-view'),
+ '[data-faceted-filter]': q('[data-faceted-filter]'),
+ };
+ // What wraps the product-list-items? climb from first card
+ const card = document.querySelector('.product-list-item');
+ const chain = [];
+ let n = card;
+ for (let i = 0; n && i < 6; i++) {
+ chain.push((n.tagName||'').toLowerCase() + (n.id?('#'+n.id):'') + (n.className && typeof n.className==='string' ? ('.'+n.className.trim().split(/\s+/).slice(0,3).join('.')) : '') + [...n.attributes||[]].filter(a=>a.name.startsWith('data-')).map(a=>'['+a.name+']').join(''));
+ n = n.parentElement;
+ }
+ // computed style of the immediate parent of cards (is it CSS grid/flex?)
+ const parent = card && card.parentElement;
+ const cs = parent ? getComputedStyle(parent) : {};
+ return { need, cardAncestry: chain, parentDisplay: cs.display, parentGridCols: cs.gridTemplateColumns, parentFlexWrap: cs.flexWrap };
+ });
+ console.log(`\n===== ${name} =====`);
+ console.log(' needed elements:', JSON.stringify(r.need));
+ console.log(' card ancestry:', JSON.stringify(r.cardAncestry, null, 0));
+ console.log(' card-parent computed: display=' + r.parentDisplay + ' gridCols=' + r.parentGridCols + ' flexWrap=' + r.parentFlexWrap);
+ }
+ await b.close();
+})().catch(e=>{console.error(e);process.exit(1);});
diff --git a/shopify/grid-js-fix/verify-dev-fix.js b/shopify/grid-js-fix/verify-dev-fix.js
new file mode 100644
index 00000000..e4930b8b
--- /dev/null
+++ b/shopify/grid-js-fix/verify-dev-fix.js
@@ -0,0 +1,118 @@
+/*
+ * Definitive verification that the staged dev-theme fix (patched header.liquid +
+ * product-list-item.liquid + production grid.js) makes the grids init.
+ *
+ * Faithful simulation of the dev theme's rendered DOM on top of the LIVE page:
+ * 1. route-swap /assets/grid.js -> staged 554KB production build
+ * 2. before any script runs, mutate the live DOM to match the patched Liquid:
+ * - add data-product-hover to every .product-list-item (per-card wrapper fix)
+ * - inject the hidden [data-header-search]/[data-header-search-button]/
+ * [data-header-search-button-close] hooks into the header section
+ * 3. measure window.Flickity, .flickity-enabled count, and image sizing.
+ *
+ * This is faithful because the ONLY differences between live and the dev theme
+ * are exactly these three assets; the DOM mutations replicate the patched Liquid
+ * output 1:1.
+ */
+const fs = require('fs');
+const { chromium } = require('playwright');
+const PROD_BUNDLE = fs.readFileSync('/tmp/grid_PROD_candidate.js', 'utf8');
+const PAGES = [
+ { name: 'HOME', url: 'https://www.designerwallcoverings.com/' },
+ { name: 'COLLECTION', url: 'https://www.designerwallcoverings.com/collections/acoustical?pf_t_product_type=Wallcovering&pf_t_color=Green' },
+];
+
+// Mutations applied BEFORE page scripts run (document_start). Mirrors the patched Liquid.
+const PATCH_DOM = `
+(function () {
+ function applyPatch() {
+ // (1) per-card data-product-hover (mirrors product-list-item.liquid patch)
+ document.querySelectorAll('.product-list-item:not([data-product-hover])').forEach(function (el) {
+ el.setAttribute('data-product-hover', (window.__dwProductHover || 'image-flip'));
+ });
+ // (2) header search hooks (mirrors header.liquid shim)
+ document.querySelectorAll('[data-section-type="static-header"]').forEach(function (scriptTag) {
+ var section = scriptTag.closest('.shopify-section') || document.querySelector('.shopify-section');
+ });
+ document.querySelectorAll('.header-content-right, [data-header-content-right]').forEach(function (right) {
+ if (right.querySelector('[data-header-search]')) return;
+ var shim = document.createElement('div');
+ shim.className = 'header-search';
+ shim.setAttribute('data-header-search', '');
+ shim.setAttribute('data-width', '0');
+ shim.setAttribute('aria-hidden', 'true');
+ shim.style.cssText = 'display:none !important;width:0;height:0;overflow:hidden;position:absolute;left:-9999px;';
+ var b1 = document.createElement('button'); b1.setAttribute('data-header-search-button',''); b1.type='button';
+ var b2 = document.createElement('button'); b2.setAttribute('data-header-search-button-close',''); b2.type='button';
+ shim.appendChild(b1); shim.appendChild(b2);
+ right.appendChild(shim);
+ });
+ }
+ // Run on DOM mutations + at multiple lifecycle points so the hooks exist before
+ // grid.js section-init runs and as Boost/AJAX re-renders product cards.
+ var mo = new MutationObserver(function(){ applyPatch(); });
+ document.addEventListener('DOMContentLoaded', function(){ applyPatch(); mo.observe(document.documentElement, {childList:true,subtree:true}); });
+ applyPatch();
+})();
+`;
+
+(async () => {
+ const browser = await chromium.launch({ headless: true, executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' });
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1400 } });
+ await ctx.route('**/assets/grid.js**', route => route.fulfill({ status: 200, contentType: 'application/javascript', body: PROD_BUNDLE }));
+ await ctx.addInitScript(PATCH_DOM);
+ const page = await ctx.newPage();
+ const errs = [];
+ page.on('pageerror', e => errs.push(String(e).split('\n')[0]));
+
+ let allPass = true;
+ for (const p of PAGES) {
+ errs.length = 0;
+ try { await page.goto(p.url, { waitUntil: 'networkidle', timeout: 50000 }); }
+ catch (e) { await page.goto(p.url, { waitUntil: 'domcontentloaded', timeout: 50000 }).catch(()=>{}); }
+ await page.waitForTimeout(4000);
+ const r = await page.evaluate(() => {
+ const imgs = Array.from(document.querySelectorAll('.product-list-item img, .product-image img, img[src*="cdn/shop"]')).slice(0, 80);
+ let sampled = 0, over700 = 0, maxW = 0; const samples = [];
+ for (const im of imgs) {
+ const w = im.getBoundingClientRect().width;
+ if (w < 5) continue;
+ sampled++; maxW = Math.max(maxW, w);
+ if (w > 700) { over700++; if (samples.length < 5) samples.push(Math.round(w)); }
+ }
+ // detect CDN width-resizing in the src (Shopify _NNNx or width= param) on a sample card img
+ const firstCdn = (document.querySelector('.product-list-item img[src*="cdn/shop"], img[src*="cdn/shop"]')||{}).src || '';
+ return {
+ Flickity: typeof window.Flickity,
+ imagesLoaded: typeof window.imagesLoaded,
+ flickityEnabled: document.querySelectorAll('.flickity-enabled').length,
+ flickityViewport: document.querySelectorAll('.flickity-viewport').length,
+ masonryGrids: document.querySelectorAll('.flickity-enabled, [style*="position: relative"] .product-list-item, .product-list-item').length,
+ productCards: document.querySelectorAll('.product-list-item').length,
+ cardsWithHover: document.querySelectorAll('.product-list-item[data-product-hover]').length,
+ headerSearchHooks: document.querySelectorAll('[data-header-search]').length,
+ imgsSampled: sampled, imgsOver700: over700, maxImgW: Math.round(maxW), samples,
+ cdnSrcSample: firstCdn.slice(0, 130),
+ };
+ });
+ // PASS criteria for the giant-image bug:
+ // - on HOME the home product carousels should become Flickity-enabled (>0)
+ // - on COLLECTION the masonry grid should constrain card images (no >700px native blowups)
+ const carouselsOK = (p.name === 'HOME') ? (r.flickityEnabled > 0) : true;
+ const sizingOK = r.imgsOver700 === 0 || r.maxImgW <= 700;
+ const noFatalThrow = !errs.some(e => /StaticHeader|ProductListItem|_initSections|_setSearchWidth/.test(e));
+ const pass = carouselsOK && sizingOK && noFatalThrow;
+ if (!pass) allPass = false;
+ console.log(`\n[${p.name}] ${pass ? 'PASS ✓' : 'CHECK ✗'}`);
+ console.log(` Flickity=${r.Flickity} imagesLoaded=${r.imagesLoaded} .flickity-enabled=${r.flickityEnabled} .flickity-viewport=${r.flickityViewport}`);
+ console.log(` productCards=${r.productCards} cardsWithHover=${r.cardsWithHover} headerSearchHooks=${r.headerSearchHooks}`);
+ console.log(` imgsSampled=${r.imgsSampled} over700px=${r.imgsOver700} maxImgW=${r.maxImgW}px samples=${JSON.stringify(r.samples)}`);
+ console.log(` cdnSrcSample=${r.cdnSrcSample}`);
+ const fatal = errs.filter(e => /StaticHeader|ProductListItem|_initSections|_setSearchWidth/.test(e));
+ if (fatal.length) console.log(` FATAL grid.js throws still present: ${JSON.stringify(fatal.slice(0,3))}`);
+ else console.log(` no StaticHeader/ProductListItem/_initSections throws ✓`);
+ }
+ console.log(`\n==== OVERALL: ${allPass ? 'FIX VERIFIED ✓' : 'NEEDS REVIEW ✗'} ====`);
+ await browser.close();
+ process.exit(allPass ? 0 : 2);
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/shopify/grid-js-fix/verify-dev-preview-auth.js b/shopify/grid-js-fix/verify-dev-preview-auth.js
new file mode 100644
index 00000000..0f193f9c
--- /dev/null
+++ b/shopify/grid-js-fix/verify-dev-preview-auth.js
@@ -0,0 +1,49 @@
+/*
+ * Load the ACTUAL dev-theme preview (all 3 real staged assets) using Steve's
+ * logged-in Chrome profile so the Shopify preview session cookie is present.
+ * READ-ONLY: navigates + measures, never clicks/writes.
+ *
+ * If the preview redirects to the live theme (no valid session), we report that
+ * and fall back to the manual-paste instructions in the memo.
+ */
+const { chromium } = require('playwright');
+const DEV = '143947038971'.replace('971','971'); // will be overridden by arg
+const DEV_ID = process.argv[2] || '143947038771';
+const PAGES = [
+ { name: 'HOME', url: `https://www.designerwallcoverings.com/?preview_theme_id=${DEV_ID}`, container: '.home-products-content' },
+ { name: 'COLLECTION', url: `https://www.designerwallcoverings.com/collections/acoustical?preview_theme_id=${DEV_ID}&pf_t_product_type=Wallcovering&pf_t_color=Green`, container: '.collection-products' },
+];
+const PROFILE = process.env.HOME + '/Library/Application Support/Google/Chrome';
+
+(async () => {
+ // Use a COPY of the default profile dir is unsafe while Chrome runs; instead use
+ // persistent context pointed at a cloned cookie jar would be ideal, but simplest
+ // reliable approach: launchPersistentContext on a dedicated profile is empty (no
+ // session). So we attempt to read the live preview via the running Chrome's
+ // remote-debugging — not enabled. Fallback: just hit the URL headless and detect
+ // which theme actually served (by grid.js size/build).
+ const browser = await chromium.launch({ headless: true, executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' });
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1400 } });
+ const page = await ctx.newPage();
+ let servedGridIsProd = null;
+ page.on('response', async resp => {
+ if (/\/assets\/grid\.js/.test(resp.url())) {
+ try { const len = (await resp.body()).length; servedGridIsProd = len < 800000; } catch(e){}
+ }
+ });
+ for (const p of PAGES) {
+ servedGridIsProd = null;
+ await page.goto(p.url, { waitUntil: 'networkidle', timeout: 50000 }).catch(()=>{});
+ await page.waitForTimeout(3500);
+ const r = await page.evaluate((sel) => {
+ const cont = document.querySelector(sel);
+ const cs = cont ? getComputedStyle(cont) : null;
+ const cards = cont ? cont.querySelectorAll('.product-list-item') : [];
+ let widths = []; cards.forEach(c=>{const im=c.querySelector('img'); if(im){const w=im.getBoundingClientRect().width; if(w>2)widths.push(Math.round(w));}});
+ widths.sort((a,b)=>b-a);
+ return { display: cs?cs.display:'n/a', cards: cards.length, maxImgW: widths[0]||0, widths: widths.slice(0,6), flick: document.querySelectorAll('.flickity-enabled').length };
+ }, p.container);
+ console.log(`[${p.name}] servedProdGrid=${servedGridIsProd} display=${r.display} cards=${r.cards} maxImgW=${r.maxImgW}px flickity=${r.flick} widths=${JSON.stringify(r.widths)}`);
+ }
+ await browser.close();
+})().catch(e=>{console.error('ERR',e.message);process.exit(1);});
diff --git a/shopify/grid-js-fix/verify-final.js b/shopify/grid-js-fix/verify-final.js
new file mode 100644
index 00000000..666cc13f
--- /dev/null
+++ b/shopify/grid-js-fix/verify-final.js
@@ -0,0 +1,84 @@
+/*
+ * FINAL verification — measures the ACTUAL product-grid containers (not hero/slideshow):
+ * HOME: .home-products-content[data-products-container] (display:block = BUG)
+ * COLLECTION: .collection-products[data-collection-grid] (CSS grid = OK)
+ * Compares BASELINE (live broken dev asset, unpatched DOM) vs FIXED (staged prod
+ * grid.js + patched DOM hooks). Reports per-container computed display + card image
+ * rendered widths.
+ */
+const fs = require('fs');
+const { chromium } = require('playwright');
+const PROD_BUNDLE = fs.readFileSync('/tmp/grid_PROD_candidate.js', 'utf8');
+const PATCH_DOM = `
+(function () {
+ function applyPatch() {
+ document.querySelectorAll('.product-list-item:not([data-product-hover])').forEach(function (el) {
+ el.setAttribute('data-product-hover', 'image-flip');
+ });
+ document.querySelectorAll('.header-content-right, [data-header-content-right]').forEach(function (right) {
+ if (right.querySelector('[data-header-search]')) return;
+ var shim = document.createElement('div'); shim.className='header-search';
+ shim.setAttribute('data-header-search',''); shim.setAttribute('data-width','0'); shim.setAttribute('aria-hidden','true');
+ shim.style.cssText='display:none !important;width:0;height:0;overflow:hidden;position:absolute;left:-9999px;';
+ var b1=document.createElement('button'); b1.setAttribute('data-header-search-button',''); b1.type='button';
+ var b2=document.createElement('button'); b2.setAttribute('data-header-search-button-close',''); b2.type='button';
+ shim.appendChild(b1); shim.appendChild(b2); right.appendChild(shim);
+ });
+ }
+ var mo=new MutationObserver(function(){applyPatch();});
+ document.addEventListener('DOMContentLoaded',function(){applyPatch(); mo.observe(document.documentElement,{childList:true,subtree:true});});
+ applyPatch();
+})();
+`;
+const PAGES = [
+ { name: 'HOME', url: 'https://www.designerwallcoverings.com/', container: '.home-products-content[data-products-container]' },
+ { name: 'COLLECTION', url: 'https://www.designerwallcoverings.com/collections/acoustical?pf_t_product_type=Wallcovering&pf_t_color=Green', container: '.collection-products[data-collection-grid]' },
+];
+
+async function measure(ctx, p) {
+ const page = await ctx.newPage();
+ const errs = [];
+ page.on('pageerror', e => errs.push(String(e).split('\n')[0]));
+ try { await page.goto(p.url, { waitUntil: 'networkidle', timeout: 50000 }); }
+ catch(e){ await page.goto(p.url, { waitUntil:'domcontentloaded', timeout:50000 }).catch(()=>{}); }
+ await page.waitForTimeout(4000);
+ const r = await page.evaluate((sel) => {
+ const cont = document.querySelector(sel) || document.querySelector(sel.split('[')[0]);
+ const cs = cont ? getComputedStyle(cont) : null;
+ // measure card IMAGES inside the container only
+ const cards = cont ? cont.querySelectorAll('.product-list-item') : [];
+ let widths = [];
+ cards.forEach(c => { const im=c.querySelector('img'); if(im){ const w=im.getBoundingClientRect().width; if(w>2) widths.push(Math.round(w)); }});
+ widths.sort((a,b)=>b-a);
+ return {
+ containerFound: !!cont,
+ display: cs ? cs.display : 'n/a',
+ gridCols: cs ? cs.gridTemplateColumns : 'n/a',
+ cardCount: cards.length,
+ cardImgWidths: widths.slice(0, 8),
+ maxCardImgW: widths[0] || 0,
+ flickityEnabled: document.querySelectorAll('.flickity-enabled').length,
+ };
+ }, p.container);
+ const fatal = errs.filter(e => /StaticHeader|ProductListItem|_initSections|_setSearchWidth/.test(e));
+ await page.close();
+ return { ...r, fatalThrows: fatal.length };
+}
+
+(async () => {
+ const browser = await chromium.launch({ headless: true, executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' });
+
+ console.log('################ BASELINE (live broken dev asset, no DOM patch) ################');
+ const base = await browser.newContext({ viewport: { width: 1440, height: 1400 } });
+ for (const p of PAGES) { const m = await measure(base, p); console.log(`[${p.name}] container=${m.containerFound} display=${m.display} gridCols=${m.gridCols} cards=${m.cardCount} maxCardImgW=${m.maxCardImgW}px widths=${JSON.stringify(m.cardImgWidths)} flickity=${m.flickityEnabled} fatalThrows=${m.fatalThrows}`); }
+ await base.close();
+
+ console.log('\n################ FIXED (staged prod grid.js + patched DOM hooks) ################');
+ const fix = await browser.newContext({ viewport: { width: 1440, height: 1400 } });
+ await fix.route('**/assets/grid.js**', route => route.fulfill({ status: 200, contentType: 'application/javascript', body: PROD_BUNDLE }));
+ await fix.addInitScript(PATCH_DOM);
+ for (const p of PAGES) { const m = await measure(fix, p); console.log(`[${p.name}] container=${m.containerFound} display=${m.display} gridCols=${m.gridCols} cards=${m.cardCount} maxCardImgW=${m.maxCardImgW}px widths=${JSON.stringify(m.cardImgWidths)} flickity=${m.flickityEnabled} fatalThrows=${m.fatalThrows}`); }
+ await fix.close();
+
+ await browser.close();
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/shopify/scripts/cadence/sdg-capture-edi.mjs b/shopify/scripts/cadence/sdg-capture-edi.mjs
new file mode 100644
index 00000000..c9f25813
--- /dev/null
+++ b/shopify/scripts/cadence/sdg-capture-edi.mjs
@@ -0,0 +1,33 @@
+#!/usr/bin/env node
+/** Capture EdiNextCore request method + postData + full response while browsing. */
+import { createRequire } from 'module';
+const require=createRequire(import.meta.url);
+let chromium; for(const p of ['/Users/stevestudio2/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/node_modules/playwright','playwright']){try{({chromium}=require(p));break;}catch{}}
+import fs from 'node:fs';
+const OUT='/tmp/sdg'; const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+const browser=await chromium.launch({headless:true});
+// full state (cookies + localStorage auth token EdiNextCore needs); hit product grid first to bootstrap
+const ctx=await browser.newContext({ storageState:`${OUT}/state.json`, viewport:{width:1440,height:1200} });
+const page=await ctx.newPage();
+const caps=[];
+page.on('response', async res=>{
+ const url=res.url();
+ if(!/EdiNextCore\/api/.test(url)) return;
+ const rq=res.request();
+ let body=''; try{ body=await res.text(); }catch{}
+ caps.push({ url, method:rq.method(), status:res.status(), postData:rq.postData()||null,
+ reqHeaders:rq.headers(), body: body.slice(0,2000), bodyLen: body.length });
+ // also persist the FULL body per endpoint
+ const tag=url.split('/EdiNextCore/api/')[1].split('?')[0].replace(/\//g,'_');
+ try{ fs.writeFileSync(`${OUT}/edifull-${tag}.json`, body); }catch{}
+});
+async function go(p,wait=7000){ try{ await page.goto('https://trade.sandersondesigngroup.com'+p,{waitUntil:'networkidle',timeout:60000}); }catch{ try{await page.goto('https://trade.sandersondesigngroup.com'+p,{waitUntil:'domcontentloaded',timeout:60000});}catch{} } await sleep(wait); }
+await go('/us/products/wallpaper'); // app bootstrap → customer/data, PriceList, Product/detail
+await go('/us/price-lists-technical-info');
+await go('/us/brands/zoffany/wallpaper');
+await go('/us/document-browser');
+await go('/us/account/finance');
+fs.writeFileSync(`${OUT}/edi-capture.json`, JSON.stringify(caps,null,2));
+console.log('captured',caps.length,'EdiNextCore calls');
+caps.forEach(c=>console.log(' •',c.method,c.status,c.bodyLen+'b',c.url.split('/EdiNextCore/api/')[1],'| postData:',(c.postData||'').slice(0,120)));
+await browser.close();
diff --git a/shopify/scripts/cadence/sdg-fetch-pricing.mjs b/shopify/scripts/cadence/sdg-fetch-pricing.mjs
new file mode 100644
index 00000000..8eec1319
--- /dev/null
+++ b/shopify/scripts/cadence/sdg-fetch-pricing.mjs
@@ -0,0 +1,23 @@
+#!/usr/bin/env node
+/** Fetch full EdiNextCore pricing payloads with the saved session (no login). */
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+let chromium; for (const p of ['/Users/stevestudio2/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/node_modules/playwright','playwright']) { try{({chromium}=require(p));break;}catch{} }
+import fs from 'node:fs';
+const OUT='/tmp/sdg', EDI='https://supplier.sandersondesigngroup.com/EdiNextCore/api';
+const browser=await chromium.launch({headless:true});
+const ctx=await browser.newContext({ storageState:`${OUT}/state.json` });
+const page=await ctx.newPage();
+// warm the trade origin so referer/cookies apply to the supplier XHR
+await page.goto('https://trade.sandersondesigngroup.com/us/price-lists-technical-info',{waitUntil:'domcontentloaded'}).catch(()=>{});
+const req=page.request;
+async function grab(name,url,opts){
+ try{ const r=await req.get(url,opts); const t=await r.text(); fs.writeFileSync(`${OUT}/edi-${name}.json`,t); console.log(name,r.status(),t.length+'b'); return t; }
+ catch(e){ console.log(name,'ERR',e.message); return ''; }
+}
+const pl=await grab('pricelist',`${EDI}/PriceList`);
+try{ const j=JSON.parse(pl); for(const grp of j) for(const f of (grp.files||[])) console.log(' PL:',f.displayName,'|',f.documentType,'|',f.azureContainer+'/'+f.azurePath); }catch{}
+await grab('product-detail',`${EDI}/Product/detail`);
+await grab('docbrowser',`${EDI}/FileExplorer/DocumentBrowser`);
+await browser.close();
+console.log('DONE');
diff --git a/shopify/scripts/cadence/sdg-recon-api.mjs b/shopify/scripts/cadence/sdg-recon-api.mjs
new file mode 100644
index 00000000..e8becd9d
--- /dev/null
+++ b/shopify/scripts/cadence/sdg-recon-api.mjs
@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+/**
+ * SDG Trade Hub phase-2 recon — find the JSON price API (Zoffany cost source).
+ * Reuses /tmp/sdg/state.json (NO login, NO MFA). Captures every JSON/XHR response
+ * while browsing the wallpaper grid + Quick Order + Price Lists so we can see which
+ * endpoint carries per-SKU trade price.
+ */
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+let chromium;
+for (const p of ['/Users/stevestudio2/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/node_modules/playwright','playwright'])
+ { try { ({ chromium } = require(p)); break; } catch {} }
+if (!chromium) { console.error('FATAL: playwright not resolvable'); process.exit(1); }
+import fs from 'node:fs';
+const OUT='/tmp/sdg'; const BASE='https://trade.sandersondesigngroup.com';
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+const log=(...a)=>console.log(new Date().toISOString().slice(11,19),...a);
+if (!fs.existsSync(`${OUT}/state.json`)) { console.error('FATAL: no state.json — run sdg-trade-login.mjs first'); process.exit(1); }
+
+const browser=await chromium.launch({headless:true});
+const ctx=await browser.newContext({ storageState:`${OUT}/state.json`, viewport:{width:1440,height:1200},
+ userAgent:'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36' });
+const page=await ctx.newPage();
+
+const api=[]; // captured JSON responses that look pricey
+page.on('response', async (res)=>{
+ try {
+ const url=res.url(); const ct=(res.headers()['content-type']||'');
+ if (!/json/i.test(ct)) return;
+ if (/\.(png|jpg|svg|css|js|woff)/i.test(url)) return;
+ const txt=await res.text().catch(()=> '');
+ // dump FULL body for every EdiNextCore call + record request method/postData
+ if (/EdiNextCore\/api/.test(url)) {
+ const tag=url.split('/EdiNextCore/api/')[1].split('?')[0].replace(/\//g,'_');
+ try{ fs.writeFileSync(`${OUT}/edifull-${tag}.json`, txt); }catch{}
+ api.push({ url, edi:true, method:res.request().method(), postData:res.request().postData()||null, status:res.status(), len:txt.length, sample:txt.slice(0,400) });
+ return;
+ }
+ const pricey=/price|cost|rrp|trade|amount|currency|sku|productCode|colourway/i.test(txt);
+ if (txt.length>120 && (pricey || /\/api\/|\/graphql|products|search|pricing/i.test(url))) {
+ api.push({ url, status:res.status(), len:txt.length, sample: txt.slice(0,400) });
+ }
+ } catch {}
+});
+
+async function visit(label, path, waitMs=6000){
+ try {
+ log('visit', label, path);
+ await page.goto(BASE+path, { waitUntil:'networkidle', timeout:60000 }).catch(()=>page.goto(BASE+path,{waitUntil:'domcontentloaded',timeout:60000}));
+ await sleep(waitMs);
+ await page.screenshot({ path:`${OUT}/p2-${label}.png`, fullPage:true });
+ // dump any anchors that look like downloads or product detail
+ const links=await page.$$eval('a[href]',as=>as.map(a=>({t:(a.textContent||'').trim().slice(0,50),h:a.getAttribute('href')}))
+ .filter(x=>x.h && /\.(pdf|csv|xlsx?|zip)|download|\/product|\/p\/|brand|zoffany|price/i.test((x.t||'')+' '+(x.h||''))).slice(0,60));
+ fs.writeFileSync(`${OUT}/p2-${label}-links.json`, JSON.stringify(links,null,2));
+ log(` ${label}: ${links.length} link(s), url=${page.url()}`);
+ } catch(e){ log(` ${label} failed:`, e.message); }
+}
+
+// 1) wallpaper product grid (try Zoffany brand filter variants)
+await visit('wallpaper', '/us/products/wallpaper', 7000);
+await visit('zoffany-brand', '/us/all-brands/zoffany', 6000);
+await visit('zoffany-wp', '/us/brands/zoffany/wallpaper', 6000);
+// 2) quick order (per-SKU price entry)
+await visit('quickorder', '/us/quick-order-bulk-order', 6000);
+// 3) price lists / docs
+await visit('pricelists', '/us/price-lists-technical-info', 7000);
+await visit('docbrowser', '/us/document-browser', 7000);
+
+// de-dupe captured API by url
+const seen=new Set(); const uniq=api.filter(a=>{const k=a.url.split('?')[0]; if(seen.has(k))return false; seen.add(k); return true;});
+fs.writeFileSync(`${OUT}/p2-api-capture.json`, JSON.stringify(uniq,null,2));
+log(`captured ${uniq.length} unique JSON endpoints → p2-api-capture.json`);
+uniq.forEach(a=>console.log(' •', a.status, a.len, a.url.slice(0,110)));
+await browser.close();
+log('DONE');
diff --git a/shopify/scripts/cadence/sdg-trade-login.mjs b/shopify/scripts/cadence/sdg-trade-login.mjs
index ccea887f..4551af7e 100644
--- a/shopify/scripts/cadence/sdg-trade-login.mjs
+++ b/shopify/scripts/cadence/sdg-trade-login.mjs
@@ -21,7 +21,7 @@ import os from 'node:os';
const EMAIL = 'info@designerwallcoverings.com';
const ENV = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
-const PW = (ENV.match(/^SANDERSON_PASSWORD=(.*)$/m) || [])[1];
+const PW = process.env.SDG_PW_OVERRIDE || (ENV.match(/^SANDERSON_PASSWORD=(.*)$/m) || [])[1];
if (!PW) { console.error('FATAL: SANDERSON_PASSWORD not in secrets .env'); process.exit(1); }
const OUT = '/tmp/sdg'; fs.mkdirSync(OUT, { recursive: true });
@@ -50,6 +50,7 @@ const ctx = await browser.newContext({
});
const page = await ctx.newPage();
try {
+ if (process.env.COOLDOWN_MS) { log('cooldown', process.env.COOLDOWN_MS + 'ms (MFA rate-limit recovery)'); await sleep(+process.env.COOLDOWN_MS); }
log('goto login');
await page.goto('https://trade.sandersondesigngroup.com/login', { waitUntil: 'domcontentloaded', timeout: 60000 });
await sleep(1500);
@@ -71,36 +72,63 @@ try {
log('latest MFA id before submit:', beforeMfa);
await page.screenshot({ path: `${OUT}/01-prelogin.png` });
- await Promise.all([
- page.getByRole('button', { name: /log ?in/i }).click({ timeout: 8000 }).catch(()=>page.click('button[type="submit"]')),
- ]);
+ // Submit: pressing Enter in the password field is markup-agnostic (avoids the
+ // ambiguous "Log in" tab-vs-button match). Fall back to the form's submit button.
+ await page.fill('input[type="password"], input[name*="pass" i], input[id*="pass" i]', PW);
+ await page.locator('input[type="password"], input[name*="pass" i]').first().press('Enter').catch(()=>{});
+ await sleep(2500);
+ if (/login/i.test(page.url())) {
+ await page.locator('form button:has-text("Log in"), button:has-text("Log in")').last().click({ timeout: 6000 }).catch(()=>{});
+ }
log('submitted login');
- await sleep(4000);
+ await sleep(4500);
await page.screenshot({ path: `${OUT}/02-postsubmit.png` });
- // MFA step? look for a code input
- const mfaInput = page.locator('input[type="text"], input[type="tel"], input[autocomplete*="one-time" i], input[name*="code" i], input[name*="otp" i]');
+ // Capture any visible error/validation message FIRST (so we don't poll MFA on a failed login).
+ let errText = '';
+ try {
+ const body = await page.innerText('body');
+ const m = body.match(/(invalid[^\n]{0,60}|incorrect[^\n]{0,60}|not (?:recognised|recognized)[^\n]{0,60}|locked[^\n]{0,60}|too many[^\n]{0,60}|does not match[^\n]{0,60})/i);
+ if (m) errText = m[1].trim();
+ } catch {}
+ if (errText) { log('LOGIN ERROR text:', JSON.stringify(errText)); fs.writeFileSync(`${OUT}/result.json`, JSON.stringify({ loggedIn:false, error: errText }, null, 2)); throw new Error('login-rejected: ' + errText); }
+
+ // Are we already in? The canonical signal is the URL leaving /login — SDG's MFA is
+ // CONDITIONAL (a recent session skips it), so DOM-presence checks false-positive on the
+ // header search box. Only hunt for an OTP field if we're still stuck on the login page.
+ let onLogin = /login/i.test(page.url());
let needMfa = false;
- try { needMfa = (await mfaInput.count()) > 0 && /code|verif|authentic|mfa|otp/i.test(await page.content()); } catch {}
- log('MFA required?', needMfa);
+ if (onLogin) {
+ // Real OTP fields are short/maxlength-6 or live inside a modal dialog — NOT the search box.
+ const otpProbe = page.locator('[role="dialog"] input, input[maxlength="6"], input[autocomplete*="one-time" i], input[name*="otp" i], input[name*="code" i], input[inputmode="numeric"]');
+ try { needMfa = (await otpProbe.count()) > 0; } catch {}
+ }
+ log('post-submit url:', page.url(), '| onLogin:', onLogin, '| MFA required?', needMfa);
if (needMfa) {
// poll George for a NEW MFA email (id different from beforeMfa)
- let code = null;
- for (let i = 0; i < 24; i++) { // ~120s
+ let code = null, resent = false;
+ for (let i = 0; i < 30; i++) { // ~150s
await sleep(5000);
const id = latestMfaId();
if (id && id !== beforeMfa) { code = codeFromMsg(id); if (code) { log('got fresh MFA code via George'); break; } }
+ if (i === 3 && !resent) { resent = true; await page.getByRole('button', { name: /resend/i }).first().click({ timeout: 4000 }).then(()=>log('clicked Resend Code (force send)')).catch(()=>{}); }
log(`waiting for MFA email… (${i+1})`);
}
if (!code) { log('FATAL: no fresh MFA code arrived'); await page.screenshot({ path: `${OUT}/03-nocode.png` }); throw new Error('no-mfa'); }
- // enter the 6 digits (single field or split fields)
- const fields = await mfaInput.elementHandles();
- if (fields.length >= 6) { for (let i=0;i<6;i++) await fields[i].type(code[i]); }
- else { await mfaInput.first().fill(code); }
- log('entered MFA code');
- await page.getByRole('button', { name: /verif|submit|continue|log ?in/i }).first().click({ timeout: 6000 }).catch(()=>{});
- await sleep(4000);
+ // The code modal has a SINGLE 6-digit input. Scope to the dialog so digits don't
+ // scatter across the background login fields (the earlier "1"-only bug).
+ const dlg = page.locator('[role="dialog"], [class*="modal" i], [class*="popup" i], [class*="otp" i]').last();
+ let otp = dlg.locator('input').first();
+ if (!(await otp.count())) otp = page.locator('input[maxlength="6"], input[name*="otp" i], input[name*="code" i]').first();
+ await otp.click().catch(()=>{});
+ await otp.fill(''); // clear any partial
+ await otp.fill(code);
+ log('entered MFA code into modal field');
+ await (dlg.getByRole('button', { name: /verif/i }).first().click({ timeout: 6000 })
+ .catch(()=> page.getByRole('button', { name: /verif|submit|continue/i }).first().click({ timeout: 6000 }).catch(()=>{})));
+ await page.waitForURL(u => !/login/i.test(u.toString()), { timeout: 18000 }).catch(()=>{});
+ await sleep(2500);
}
await page.screenshot({ path: `${OUT}/04-landing.png`, fullPage: true });
@@ -115,6 +143,27 @@ try {
const interesting = links.filter(l => /price|order|pad|catalog|product|wallpaper|wallcover|brand|zoffany|account|download|export/i.test(l.t + ' ' + l.h));
log('interesting links:'); interesting.forEach(l => console.log(' ', JSON.stringify(l)));
fs.writeFileSync(`${OUT}/result.json`, JSON.stringify({ loggedIn, url, interesting }, null, 2));
+
+ if (loggedIn) {
+ await ctx.storageState({ path: `${OUT}/state.json` });
+ log('saved session storageState → state.json (future scrapes skip MFA)');
+ for (const [name, p] of [['pricelists','/us/price-lists-technical-info'],['docbrowser','/us/document-browser'],['zoffany','/us/all-brands/zoffany/wallpaper']]) {
+ try {
+ await page.goto('https://trade.sandersondesigngroup.com'+p, { waitUntil:'domcontentloaded', timeout:45000 });
+ await sleep(3500);
+ await page.screenshot({ path: `${OUT}/recon-${name}.png`, fullPage:true });
+ const dl = await page.$$eval('a[href]', as=>as.map(a=>({t:(a.textContent||'').trim().slice(0,60),h:a.getAttribute('href')}))
+ .filter(x=>/\.(pdf|csv|xlsx?|zip)(\?|$)|price|download|zoffany|wallpaper/i.test((x.t||'')+' '+(x.h||''))).slice(0,80));
+ fs.writeFileSync(`${OUT}/links-${name}.json`, JSON.stringify(dl,null,2));
+ // capture any visible price text on the zoffany product grid
+ if (name==='zoffany') {
+ const txt = await page.innerText('body').catch(()=> '');
+ fs.writeFileSync(`${OUT}/zoffany-page.txt`, txt.slice(0, 8000));
+ }
+ log(`recon ${name}: ${dl.length} relevant links`);
+ } catch(e){ log(`recon ${name} failed:`, e.message); }
+ }
+ }
log('DONE — artifacts in', OUT);
} catch (e) {
log('ERROR:', e.message);
diff --git a/shopify/scripts/tres-tintas-designer-descriptions.js b/shopify/scripts/tres-tintas-designer-descriptions.js
index 92467637..7275cd3a 100644
--- a/shopify/scripts/tres-tintas-designer-descriptions.js
+++ b/shopify/scripts/tres-tintas-designer-descriptions.js
@@ -87,8 +87,10 @@ async function getMetafields(id) {
return s;
}
async function setMetafield(id, ns, key, value) {
+ // Use multi_line_text_field for material/care (may be longer text)
+ const type = ['material', 'care'].includes(key) ? 'multi_line_text_field' : 'single_line_text_field';
const r = await fetch(`https://${STORE}/admin/api/${API}/products/${id}/metafields.json`, {
- method: 'POST', headers: H, body: JSON.stringify({ metafield: { namespace: ns, key, value, type: 'single_line_text_field' } }) });
+ method: 'POST', headers: H, body: JSON.stringify({ metafield: { namespace: ns, key, value, type } }) });
const j = await r.json();
if (!j.metafield) throw new Error(`${ns}.${key}: ${JSON.stringify(j.errors || j)}`);
}
@@ -187,11 +189,13 @@ async function main() {
if (APPLY) {
fs.writeFileSync(path.join(BACKUP_DIR, `${id}.body_html.bak`), d);
const existing = await getMetafields(id);
+ const mfsToWrite = [];
for (const [label, dest] of Object.entries(SPEC_MAP)) {
const v = rows[label]; if (!v) continue;
- if (existing.has(`${dest.ns}.${dest.key}`)) continue;
- try { await setMetafield(id, dest.ns, dest.key, v); await sleep(110); }
- catch (err) { console.log(` mf err ${dest.ns}.${dest.key}: ${err.message}`); }
+ if (existing.has(`${dest.ns}.${dest.key}`)) { console.log(` ⊘ ${dest.ns}.${dest.key} (already exists)`); continue; }
+ mfsToWrite.push({ ns: dest.ns, key: dest.key, value: v, label });
+ try { await setMetafield(id, dest.ns, dest.key, v); await sleep(110); console.log(` ✓ ${dest.ns}.${dest.key} = "${v}"`); }
+ catch (err) { console.log(` ✗ mf err ${dest.ns}.${dest.key}: ${err.message}`); }
}
const pref = rows['Pattern ref.'] || rows['Pattern Reference'];
if (pref && !(n.tags||[]).includes(`pattern-ref-${pref}`)) {
diff --git a/shopify/tres-tintas-desc-backups/_done.ledger b/shopify/tres-tintas-desc-backups/_done.ledger
index 6d996f42..dece03e0 100644
--- a/shopify/tres-tintas-desc-backups/_done.ledger
+++ b/shopify/tres-tintas-desc-backups/_done.ledger
@@ -1,136 +1,25 @@
-7863632592947
-7863632625715
-7863632658483
-7863632691251
-7863632724019
-7863632887859
-7863632953395
-7863632986163
-7863633018931
-7863633051699
-7863633084467
-7863633117235
-7863633150003
-7863633215539
-7863633248307
-7863633281075
-7863633313843
-7863633379379
-7863633412147
-7863633444915
-7863633477683
-7863633510451
-7863633543219
-7863633575987
-7863633608755
-7863633641523
-7863633674291
-7863633707059
-7863633739827
-7863633772595
-7863633805363
-7863633870899
-7863633903667
-7863633936435
-7863633969203
-7863634001971
-7863634034739
-7863634067507
-7863634100275
-7863634133043
-7863634165811
-7863634198579
-7863634231347
-7863634329651
-7863634362419
-7863634395187
-7863634427955
-7863634460723
-7863634493491
-7863634559027
-7863634591795
-7863634624563
-7863634657331
-7863634722867
-7863634755635
-7863634788403
-7863634821171
-7863634853939
-7863634886707
-7863634919475
-7863634952243
-7863634985011
-7863635017779
-7863635050547
-7863635083315
-7863635116083
-7863635148851
-7863635181619
-7863635214387
-7863635247155
-7863635279923
-7863635312691
-7863635345459
-7863635378227
-7863635410995
-7863635443763
-7863635476531
-7863635509299
-7863635574835
-7863635607603
-7863635640371
-7863635673139
-7863635705907
-7863635738675
-7863635771443
-7863635804211
-7863635836979
-7863635869747
-7863635935283
-7863635968051
-7863636000819
-7863636033587
-7863636066355
-7863636099123
-7863636131891
-7863636164659
-7863636197427
-7863636230195
-7863636262963
-7863636295731
-7863636328499
-7863636361267
-7863636426803
-7863636459571
-7863636492339
-7863636525107
-7863636557875
-7863636590643
-7863636656179
-7863636688947
-7863636721715
-7863636754483
-7863636787251
-7863636820019
-7863636852787
-7863636885555
-7863636918323
-7863636951091
-7863636983859
-7863637016627
-7863637049395
-7863637082163
-7863637114931
-7863637147699
-7863637180467
-7863637213235
-7863637278771
-7863637377075
-7863637475379
-7863637540915
-7863637639219
-7863637671987
-7863637737523
-7863637770291
-7863637803059
-7863637835827
+7866353418291
+7866353451059
+7866353483827
+7866353516595
+7866353549363
+7866353582131
+7866353614899
+7866353647667
+7866353680435
+7866353713203
+7866353745971
+7866353778739
+7866353811507
+7866353844275
+7866353909811
+7866353942579
+7866353975347
+7866354008115
+7866354040883
+7866354073651
+7866354106419
+7866354139187
+7866354171955
+7866354237491
+7866354270259
diff --git a/shopify/tres-tintas-desc-backups/_done.ledger.tmp b/shopify/tres-tintas-desc-backups/_done.ledger.tmp
new file mode 100644
index 00000000..e69de29b
← 7b78feaa auto-save: 2026-06-23T08:50:33 (2 files) — shopify/tres-tint
·
back to Designer Wallcoverings
·
pending-approval: grid.js init-abort root cause + gated live 828034b5 →