← back to Designerwallcoverings
TK-11186 Step D+E prep: showroom-vendor guard in New Arrivals + Trending taggers; fresh 855-row restore-map; Step E surgical 2-asset theme deploy (data-driven hide + render-time card skip) for LIVE theme 145121607731
5555d3e7f96ecdc11b60066e27e2ff169a7c10b0 · 2026-09-03 12:06:50 -0700 · steve
Files touched
M scripts/tk11186-showroom-hide/out/restore-map-latest.jsonA scripts/tk11186-showroom-hide/theme-deploy/hide-browse-hidden.ORIGINAL.liquidA scripts/tk11186-showroom-hide/theme-deploy/hide-browse-hidden.liquidA scripts/tk11186-showroom-hide/theme-deploy/product-list-item.ORIGINAL.liquidA scripts/tk11186-showroom-hide/theme-deploy/product-list-item.liquidA scripts/tk11186-showroom-hide/theme-deploy/push-theme-assets.mjs
Diff
commit 5555d3e7f96ecdc11b60066e27e2ff169a7c10b0
Author: steve <steve@designerwallcoverings.com>
Date: Thu Sep 3 12:06:50 2026 -0700
TK-11186 Step D+E prep: showroom-vendor guard in New Arrivals + Trending taggers; fresh 855-row restore-map; Step E surgical 2-asset theme deploy (data-driven hide + render-time card skip) for LIVE theme 145121607731
---
.../out/restore-map-latest.json | 2 +-
.../hide-browse-hidden.ORIGINAL.liquid | 52 +++++
.../theme-deploy/hide-browse-hidden.liquid | 60 +++++
.../theme-deploy/product-list-item.ORIGINAL.liquid | 234 +++++++++++++++++++
.../theme-deploy/product-list-item.liquid | 257 +++++++++++++++++++++
.../theme-deploy/push-theme-assets.mjs | 90 ++++++++
6 files changed, 694 insertions(+), 1 deletion(-)
diff --git a/scripts/tk11186-showroom-hide/out/restore-map-latest.json b/scripts/tk11186-showroom-hide/out/restore-map-latest.json
index 1106221..4dc368e 100644
--- a/scripts/tk11186-showroom-hide/out/restore-map-latest.json
+++ b/scripts/tk11186-showroom-hide/out/restore-map-latest.json
@@ -1,6 +1,6 @@
{
"ticket": "TK-11186",
- "builtAt": "2026-09-03T18:51:54.094Z",
+ "builtAt": "2026-09-03T19:02:12.119Z",
"store": "designer-laboratory-sandbox.myshopify.com",
"apiVersion": "2024-10",
"showroomVendors": [
diff --git a/scripts/tk11186-showroom-hide/theme-deploy/hide-browse-hidden.ORIGINAL.liquid b/scripts/tk11186-showroom-hide/theme-deploy/hide-browse-hidden.ORIGINAL.liquid
new file mode 100644
index 0000000..a827be6
--- /dev/null
+++ b/scripts/tk11186-showroom-hide/theme-deploy/hide-browse-hidden.ORIGINAL.liquid
@@ -0,0 +1,52 @@
+{% comment %}
+ Hide browse-hidden products (Phillip Jeffries) from ALL rendered grids.
+ Runs repeatedly to catch Boost pagination, filtering, and AJAX loads.
+{% endcomment %}
+<script>
+(function(){
+ var HIDDEN_VENDORS = ['Phillip Jeffries'];
+
+ function hideProducts() {
+ // Method 1: Find vendor text elements
+ document.querySelectorAll('.product-vendor, .product-list-item-vendor, [class*="vendor"]').forEach(function(el) {
+ var text = el.textContent.trim();
+ if (HIDDEN_VENDORS.indexOf(text) > -1) {
+ var card = el.closest('.product-list-item, .boost-sd__product-item, [class*="product-item"], [class*="product-card"], article, .boost-sd__product');
+ if (card && card.style.display !== 'none') { card.style.display = 'none'; }
+ }
+ });
+
+ // Method 2: Find product titles containing vendor name
+ document.querySelectorAll('.product-list-item-title a, .boost-sd__product-title a, [class*="product-title"] a, h2 a, h3 a').forEach(function(el) {
+ var text = el.textContent.trim();
+ for (var i = 0; i < HIDDEN_VENDORS.length; i++) {
+ if (text.indexOf(HIDDEN_VENDORS[i]) > -1) {
+ var card = el.closest('.product-list-item, .boost-sd__product-item, [class*="product-item"], [class*="product-card"], article, .boost-sd__product');
+ if (card && card.style.display !== 'none') { card.style.display = 'none'; }
+ break;
+ }
+ }
+ });
+
+ // Method 3: Check Boost's JSON data in script tags
+ document.querySelectorAll('[data-vendor="Phillip Jeffries"]').forEach(function(el) {
+ el.style.display = 'none';
+ });
+ }
+
+ // Run immediately
+ hideProducts();
+
+ // Run after DOM ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function() { hideProducts(); });
+ }
+
+ // Run every 2 seconds to catch Boost pagination/AJAX loads
+ setInterval(hideProducts, 2000);
+
+ // MutationObserver as additional safety
+ var observer = new MutationObserver(function() { setTimeout(hideProducts, 100); });
+ observer.observe(document.body, { childList: true, subtree: true });
+})();
+</script>
diff --git a/scripts/tk11186-showroom-hide/theme-deploy/hide-browse-hidden.liquid b/scripts/tk11186-showroom-hide/theme-deploy/hide-browse-hidden.liquid
new file mode 100644
index 0000000..38e66bb
--- /dev/null
+++ b/scripts/tk11186-showroom-hide/theme-deploy/hide-browse-hidden.liquid
@@ -0,0 +1,60 @@
+{% comment %}
+ Suppress showroom-only vendors from ALL browse/search grids. TK-11186.
+
+ DATA-DRIVEN — no vendor name is hardcoded in logic. The showroom-vendor list is
+ sourced (in priority order) from:
+ 1. shop metafield custom.showroom_vendors (comma-separated; store-wide, all themes)
+ 2. theme setting settings.showroom_vendors (comma-separated)
+ 3. a canonical fallback snapshot of ~/Projects/fix-live-board/config/showroom-vendors.json
+ (last resort so suppression never silently fails if 1+2 are unset).
+ Canonical source of truth = showroom-vendors.json. Set the shop metafield to that
+ file's contents (metafield-update skill) and this snippet follows it automatically.
+
+ Two layers:
+ (a) RENDER-TIME: product-list-item.liquid emits NO card HTML for a showroom vendor
+ (the real fix — no SEO-crawlable markup, no flash). This snippet's CSS also
+ hides any card that carries data-vendor at paint time (before JS).
+ (b) BACKSTOP: a JS observer catches Boost AJAX-rendered grids that bypass Liquid,
+ reading the SAME injected list (case-insensitive). Kept minimal.
+{% endcomment %}
+{%- liquid
+ assign showroom_raw = shop.metafields.custom.showroom_vendors
+ if showroom_raw == blank
+ assign showroom_raw = settings.showroom_vendors
+ endif
+ if showroom_raw == blank
+ assign showroom_raw = 'Phillip Jeffries'
+ endif
+ assign showroom_vendors = showroom_raw | split: ','
+-%}
+<style>
+{%- for v in showroom_vendors -%}
+{%- assign vt = v | strip -%}
+{%- if vt != blank -%}
+[data-vendor="{{ vt | escape }}"]{display:none!important;}
+{%- endif -%}
+{%- endfor -%}
+</style>
+<script>
+(function(){
+ var HIDDEN = [{%- for v in showroom_vendors -%}{%- assign vt = v | strip -%}{%- if vt != blank -%}{{ vt | json }}{%- unless forloop.last -%},{%- endunless -%}{%- endif -%}{%- endfor -%}].map(function(s){return String(s).trim().toLowerCase();});
+ if (!HIDDEN.length) return;
+ var CARD_SEL = '.product-list-item, .boost-sd__product-item, [class*="product-item"], [class*="product-card"], article, .boost-sd__product';
+ function isHidden(text){ text = (text||'').trim().toLowerCase(); for (var i=0;i<HIDDEN.length;i++){ if (text.indexOf(HIDDEN[i]) > -1) return true; } return false; }
+ function hideProducts(){
+ document.querySelectorAll('.product-vendor, .product-list-item-vendor, [class*="vendor"]').forEach(function(el){
+ if (isHidden(el.textContent)){ var c = el.closest(CARD_SEL); if (c && c.style.display !== 'none') c.style.display = 'none'; }
+ });
+ document.querySelectorAll('.product-list-item-title a, .boost-sd__product-title a, [class*="product-title"] a, h2 a, h3 a').forEach(function(el){
+ if (isHidden(el.textContent)){ var c = el.closest(CARD_SEL); if (c && c.style.display !== 'none') c.style.display = 'none'; }
+ });
+ document.querySelectorAll('[data-vendor]').forEach(function(el){
+ if (HIDDEN.indexOf(String(el.getAttribute('data-vendor')||'').trim().toLowerCase()) > -1){ el.style.display = 'none'; }
+ });
+ }
+ hideProducts();
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', hideProducts);
+ setInterval(hideProducts, 2000);
+ if (document.body){ new MutationObserver(function(){ setTimeout(hideProducts, 100); }).observe(document.body, { childList:true, subtree:true }); }
+})();
+</script>
diff --git a/scripts/tk11186-showroom-hide/theme-deploy/product-list-item.ORIGINAL.liquid b/scripts/tk11186-showroom-hide/theme-deploy/product-list-item.ORIGINAL.liquid
new file mode 100644
index 0000000..58e6519
--- /dev/null
+++ b/scripts/tk11186-showroom-hide/theme-deploy/product-list-item.ORIGINAL.liquid
@@ -0,0 +1,234 @@
+{% 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 %}
+
+ <div class="product-info">
+ <div class="vertical-center">
+ <div>View Pattern</div>
+ {%- liquid
+ assign sku_raw = item.variants.last.sku | default: ''
+ assign sku_display = sku_raw | replace: '-Sample', '' | replace: '-SAMPLE', '' | replace: '-sample', ''
+ assign sku_display = sku_display | replace: '--', '-' | replace: '--', '-'
+ assign sku_last_char = sku_display | slice: -1, 1
+ if sku_display != blank and sku_last_char == '-'
+ assign sku_display = sku_display | remove_last: '-'
+ endif
+ -%}
+ <div>{{ sku_display }}</div>
+ </div>
+ </div>
+
+ </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 %}
+ </figure>
+
+ <div class="product-list-item-details">
+ {% if product_vendor %}
+ <p class="product-list-item-vendor">{{ item.vendor | link_to_vendor }}</p>
+ {% endif %}
+ <h2 class="product-list-item-title"><a href="{{ item.url | within: collection }}">{{ item.title }}</a></h2>
+
+ {%- comment -%}
+ DW grid-card color swatch (Carnegie-restructure fix, TK-10686).
+ Additive: renders ONLY when a valid hex metafield exists, so non-Carnegie
+ cards are unaffected. Reads hex + name across the lowercase, space-cased,
+ and swatch_hex key variants so the Carnegie Xorel restructure is covered.
+ Pure Liquid, fixed dimensions (no CLS), no shared DOM ids (grid-safe).
+ {%- endcomment -%}
+ {%- liquid
+ assign dw_hex = item.metafields.custom.color_hex.value | default: item.metafields.custom['Color Hex'].value | default: item.metafields.custom.swatch_hex.value | default: item.metafields.custom.primary_color_hex.value
+ assign dw_color_name = item.metafields.custom.color_name.value | default: item.metafields.custom.color.value | default: item.metafields.custom['Color'].value | default: item.metafields.global['Color'].value
+ assign dw_hex_body = dw_hex | strip | remove_first: '#'
+ assign dw_hex_len = dw_hex_body | size
+ assign dw_hex_ok = false
+ if dw_hex_len == 6
+ assign dw_hex_ok = true
+ elsif dw_hex_len == 3
+ assign dw_hex_ok = true
+ endif
+ -%}
+ {%- if dw_hex_ok -%}
+ <p class="product-list-item-swatch" style="display:flex;align-items:center;gap:8px;margin:4px 0 2px;line-height:1;">
+ <span class="product-list-item-swatch-dot" aria-hidden="true" style="display:inline-block;width:16px;height:16px;min-width:16px;border-radius:50%;background:#{{ dw_hex_body | escape }};border:1px solid rgba(0,0,0,0.15);box-shadow:0 1px 2px rgba(0,0,0,0.08);flex:0 0 auto;"></span>
+ {%- if dw_color_name != blank -%}
+ <span class="product-list-item-swatch-name" style="font-size:0.82em;color:#555;">{{ dw_color_name | escape }}</span>
+ {%- endif -%}
+ </p>
+ {%- endif -%}
+
+ <p class="product-list-item-price">
+ {% if item.price_varies %}
+ {% if item.price_varies %}{{ 'products.product.from' | t }}{% endif %}
+ {% if item.compare_at_price_min > item.price_min %}
+ <span class="money">{{ item.price_min | money }}</span>
+ <span class="original money">{{ item.compare_at_price_min | money }}</span>
+ {% else %}
+ <span class="money">{{ item.price_min | money }}</span>
+ {% endif %}
+ {% else %}
+ {% if item.compare_at_price_min > item.price_min %}
+ <span class="money">{{ item.price_min | money }}</span>
+ <span class="original money">{{ item.compare_at_price_min | money }}</span>
+ {% else %}
+ <span class="money">{{ item.price_min | money }}</span>
+ {% endif %}
+ {% endif %}
+ </p>
+ {% 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/scripts/tk11186-showroom-hide/theme-deploy/product-list-item.liquid b/scripts/tk11186-showroom-hide/theme-deploy/product-list-item.liquid
new file mode 100644
index 0000000..6108cd5
--- /dev/null
+++ b/scripts/tk11186-showroom-hide/theme-deploy/product-list-item.liquid
@@ -0,0 +1,257 @@
+{% comment %}
+ @param product_attributes
+ custom attributes to be applied to the product item
+ Defaults to: blank
+{% endcomment %}
+
+{%- comment %} TK-11186 render-time showroom-vendor skip: emit NO card HTML for a showroom-only
+ vendor (addressable-not-discoverable). List sourced from shop metafield / theme setting /
+ canonical showroom-vendors.json fallback — never a hardcoded vendor. {% endcomment -%}
+{%- liquid
+ assign showroom_raw = shop.metafields.custom.showroom_vendors
+ if showroom_raw == blank
+ assign showroom_raw = settings.showroom_vendors
+ endif
+ if showroom_raw == blank
+ assign showroom_raw = 'Phillip Jeffries'
+ endif
+ assign showroom_vendors = showroom_raw | split: ','
+ assign this_vendor = product.vendor | strip | downcase
+ assign is_showroom_vendor = false
+ for sv in showroom_vendors
+ assign svn = sv | strip | downcase
+ if svn != blank and svn == this_vendor
+ assign is_showroom_vendor = true
+ endif
+ endfor
+-%}
+{%- unless is_showroom_vendor -%}
+
+{% 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 %}
+
+ <div class="product-info">
+ <div class="vertical-center">
+ <div>View Pattern</div>
+ {%- liquid
+ assign sku_raw = item.variants.last.sku | default: ''
+ assign sku_display = sku_raw | replace: '-Sample', '' | replace: '-SAMPLE', '' | replace: '-sample', ''
+ assign sku_display = sku_display | replace: '--', '-' | replace: '--', '-'
+ assign sku_last_char = sku_display | slice: -1, 1
+ if sku_display != blank and sku_last_char == '-'
+ assign sku_display = sku_display | remove_last: '-'
+ endif
+ -%}
+ <div>{{ sku_display }}</div>
+ </div>
+ </div>
+
+ </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 %}
+ </figure>
+
+ <div class="product-list-item-details">
+ {% if product_vendor %}
+ <p class="product-list-item-vendor">{{ item.vendor | link_to_vendor }}</p>
+ {% endif %}
+ <h2 class="product-list-item-title"><a href="{{ item.url | within: collection }}">{{ item.title }}</a></h2>
+
+ {%- comment -%}
+ DW grid-card color swatch (Carnegie-restructure fix, TK-10686).
+ Additive: renders ONLY when a valid hex metafield exists, so non-Carnegie
+ cards are unaffected. Reads hex + name across the lowercase, space-cased,
+ and swatch_hex key variants so the Carnegie Xorel restructure is covered.
+ Pure Liquid, fixed dimensions (no CLS), no shared DOM ids (grid-safe).
+ {%- endcomment -%}
+ {%- liquid
+ assign dw_hex = item.metafields.custom.color_hex.value | default: item.metafields.custom['Color Hex'].value | default: item.metafields.custom.swatch_hex.value | default: item.metafields.custom.primary_color_hex.value
+ assign dw_color_name = item.metafields.custom.color_name.value | default: item.metafields.custom.color.value | default: item.metafields.custom['Color'].value | default: item.metafields.global['Color'].value
+ assign dw_hex_body = dw_hex | strip | remove_first: '#'
+ assign dw_hex_len = dw_hex_body | size
+ assign dw_hex_ok = false
+ if dw_hex_len == 6
+ assign dw_hex_ok = true
+ elsif dw_hex_len == 3
+ assign dw_hex_ok = true
+ endif
+ -%}
+ {%- if dw_hex_ok -%}
+ <p class="product-list-item-swatch" style="display:flex;align-items:center;gap:8px;margin:4px 0 2px;line-height:1;">
+ <span class="product-list-item-swatch-dot" aria-hidden="true" style="display:inline-block;width:16px;height:16px;min-width:16px;border-radius:50%;background:#{{ dw_hex_body | escape }};border:1px solid rgba(0,0,0,0.15);box-shadow:0 1px 2px rgba(0,0,0,0.08);flex:0 0 auto;"></span>
+ {%- if dw_color_name != blank -%}
+ <span class="product-list-item-swatch-name" style="font-size:0.82em;color:#555;">{{ dw_color_name | escape }}</span>
+ {%- endif -%}
+ </p>
+ {%- endif -%}
+
+ <p class="product-list-item-price">
+ {% if item.price_varies %}
+ {% if item.price_varies %}{{ 'products.product.from' | t }}{% endif %}
+ {% if item.compare_at_price_min > item.price_min %}
+ <span class="money">{{ item.price_min | money }}</span>
+ <span class="original money">{{ item.compare_at_price_min | money }}</span>
+ {% else %}
+ <span class="money">{{ item.price_min | money }}</span>
+ {% endif %}
+ {% else %}
+ {% if item.compare_at_price_min > item.price_min %}
+ <span class="money">{{ item.price_min | money }}</span>
+ <span class="original money">{{ item.compare_at_price_min | money }}</span>
+ {% else %}
+ <span class="money">{{ item.price_min | money }}</span>
+ {% endif %}
+ {% endif %}
+ </p>
+ {% 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>{%- endunless -%}
diff --git a/scripts/tk11186-showroom-hide/theme-deploy/push-theme-assets.mjs b/scripts/tk11186-showroom-hide/theme-deploy/push-theme-assets.mjs
new file mode 100644
index 0000000..2f65c93
--- /dev/null
+++ b/scripts/tk11186-showroom-hide/theme-deploy/push-theme-assets.mjs
@@ -0,0 +1,90 @@
+#!/usr/bin/env node
+/**
+ * TK-11186 STEP E — surgical 2-asset theme deploy to the LIVE published theme.
+ *
+ * Updates EXACTLY two snippets on the published (role=main) theme
+ * 145121607731 "carnegie-color-swatch"
+ * via the Admin API assets.json PUT — nothing else on the theme is touched (this is
+ * why we do NOT `shopify theme push`, which could clobber other files; the live card
+ * carries a Carnegie color-swatch block absent from the local theme repos).
+ *
+ * snippets/hide-browse-hidden.liquid -> data-driven (no hardcoded vendor)
+ * snippets/product-list-item.liquid -> live original + render-time showroom skip wrap
+ *
+ * DEFAULT = --dry-run (fetches live, shows the checksum diff, writes nothing).
+ * --apply PUT the two fix files, then GET-verify the new checksums.
+ * --rollback PUT the two *.ORIGINAL.liquid back (restore pre-deploy state).
+ *
+ * Reversible: --rollback restores the byte-exact originals captured at prep time.
+ * Cost: $0 (Admin API has no per-call charge). Uses SHOPIFY_FULL_ACCESS_TOKEN
+ * (write_themes; the narrow SHOPIFY_ADMIN_TOKEN 403s on the themes API).
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import crypto from 'node:crypto';
+import { fileURLToPath } from 'node:url';
+import { execFileSync } from 'node:child_process';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const VER = '2024-10';
+const THEME_ID = '145121607731'; // published (role=main) — confirm before apply
+const LOG_EXEC = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';
+
+const env = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
+const FULL = (env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m) || [])[1]?.trim();
+if (!FULL) { console.error('no SHOPIFY_FULL_ACCESS_TOKEN in secrets-manager/.env'); process.exit(1); }
+
+const args = process.argv.slice(2);
+const APPLY = args.includes('--apply');
+const ROLLBACK = args.includes('--rollback');
+const BASE = `https://${SHOP}/admin/api/${VER}/themes/${THEME_ID}/assets.json`;
+const md5 = s => crypto.createHash('md5').update(s).digest('hex');
+const H = { 'X-Shopify-Access-Token': FULL, 'Content-Type': 'application/json' };
+
+const FILES = [
+ { key: 'snippets/hide-browse-hidden.liquid',
+ fix: 'hide-browse-hidden.liquid', orig: 'hide-browse-hidden.ORIGINAL.liquid' },
+ { key: 'snippets/product-list-item.liquid',
+ fix: 'product-list-item.liquid', orig: 'product-list-item.ORIGINAL.liquid' },
+];
+
+async function getAsset(key) {
+ const u = new URL(BASE); u.searchParams.set('asset[key]', key);
+ const r = await fetch(u, { headers: H });
+ if (!r.ok) throw new Error(`GET ${key} HTTP ${r.status}`);
+ return (await r.json()).asset;
+}
+async function putAsset(key, value) {
+ const r = await fetch(BASE, { method: 'PUT', headers: H, body: JSON.stringify({ asset: { key, value } }) });
+ const j = await r.json().catch(() => ({}));
+ if (!r.ok) throw new Error(`PUT ${key} HTTP ${r.status} ${JSON.stringify(j).slice(0, 200)}`);
+ return j.asset;
+}
+function ledger(rec) { try { execFileSync('node', [LOG_EXEC], { input: JSON.stringify(rec) }); } catch (e) { console.error(' [ledger WARN]', e.message); } }
+
+const which = ROLLBACK ? 'orig' : 'fix';
+console.log(`TK-11186 STEP E theme deploy — ${ROLLBACK ? 'ROLLBACK' : APPLY ? 'APPLY' : 'DRY-RUN'} → theme ${THEME_ID}`);
+
+for (const f of FILES) {
+ const local = fs.readFileSync(path.join(__dirname, f[which]), 'utf8');
+ const live = await getAsset(f.key);
+ const liveSum = md5(live.value), localSum = md5(local);
+ console.log(`\n ${f.key}`);
+ console.log(` live md5 ${liveSum} (${live.value.length}b)`);
+ console.log(` -> ${which} md5 ${localSum} (${local.length}b) ${liveSum === localSum ? '(already matches — no-op)' : ''}`);
+ if (!APPLY && !ROLLBACK) continue;
+ if (liveSum === localSum) { console.log(' skip (identical)'); continue; }
+ const res = await putAsset(f.key, local);
+ const after = await getAsset(f.key);
+ const ok = md5(after.value) === localSum;
+ console.log(` PUT done — verify ${ok ? 'OK' : 'MISMATCH!'} (new live md5 ${md5(after.value)})`);
+ ledger({ agent: process.env.TK_AGENT || 'vp-dw-commerce', ticket: 'TK-11186',
+ action: `STEP E theme asset ${ROLLBACK ? 'ROLLBACK' : 'deploy'} ${f.key} on theme ${THEME_ID} (md5 ${liveSum} -> ${localSum})`,
+ blast_radius: 1,
+ undo_cmd: `node ${path.join(__dirname, 'push-theme-assets.mjs')} --rollback`,
+ verify: `GET assets.json?asset[key]=${f.key} checksum == ${ROLLBACK ? 'ORIGINAL' : localSum}` });
+ if (!ok) { console.error(' VERIFY FAILED — investigate before continuing'); process.exit(1); }
+}
+if (!APPLY && !ROLLBACK) console.log('\nDRY-RUN only. Re-run with --apply to deploy, or --rollback to restore originals.');
+else console.log(`\n${ROLLBACK ? 'ROLLBACK' : 'DEPLOY'} complete.`);
← df3ec4b sanderson verify: suppress deleted-tombstone false-positive
·
back to Designerwallcoverings
·
TK-11186 Step E: fix theme-deploy verify (PUT checksum + GET 15d8c9c →