[object Object]

← back to Designer Wallcoverings

Dev-theme push: pull→surgical-edit→push (drift-safe), target 'NewWall Template [Dev]' by name

8c327f87db610eef8efb92cb60f5e2a3ead3ed77 · 2026-06-21 18:37:41 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 8c327f87db610eef8efb92cb60f5e2a3ead3ed77
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jun 21 18:37:41 2026 -0700

    Dev-theme push: pull→surgical-edit→push (drift-safe), target 'NewWall Template [Dev]' by name
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 shopify/push-variant-ux-to-dev-theme.sh | 190 +++++++++++++++++++++++---------
 1 file changed, 136 insertions(+), 54 deletions(-)

diff --git a/shopify/push-variant-ux-to-dev-theme.sh b/shopify/push-variant-ux-to-dev-theme.sh
index b9b94706..45d3f980 100755
--- a/shopify/push-variant-ux-to-dev-theme.sh
+++ b/shopify/push-variant-ux-to-dev-theme.sh
@@ -1,70 +1,152 @@
 #!/usr/bin/env bash
-# Push the latest UX (variant buttons + price-restore hardening) from
-# _cwVERIFY/layout/theme.liquid to the Shopify DEV theme (never the live/main theme).
-# Backs up the dev theme's current theme.liquid first, then verifies the push.
+# Apply the latest UX (variant buttons + price-restore hardening) to the Shopify
+# DEV theme "Updated copy of NewWall Template [Dev]" — drift-safe.
 #
-# Run:  bash ~/Projects/Designer-Wallcoverings/shopify/push-variant-ux-to-dev-theme.sh
+# Instead of overwriting theme.liquid with a local copy, this PULLS the dev
+# theme's current layout/theme.liquid, applies ONLY the two surgical edits to it,
+# backs up the original, and pushes the result back. Never touches the live/main
+# theme.
+#
+# Needs a theme-scoped token (Shopify Admin → Apps → "Theme Access" → Add password):
+#   THEME_TOKEN=shptka_xxx bash ~/pd.sh
 set -euo pipefail
 cd "$(dirname "$0")"
 
 STORE="designer-laboratory-sandbox.myshopify.com"
 API="2024-10"
-SRC="_cwVERIFY/layout/theme.liquid"
 SECRETS="$HOME/Projects/secrets-manager/.env"
+TARGET_NAME="Updated copy of NewWall Template [Dev]"
 
-# Theme-scoped token. CONTENT token is the candidate (themes = content scope);
-# override by exporting THEME_TOKEN=... before running if you have a Theme Access pw.
-TOK="${THEME_TOKEN:-$(grep -hE '^SHOPIFY_DW_ADMIN_TOKEN_CONTENT=' "$SECRETS" | head -1 | cut -d= -f2- | tr -d '"'"'"' ')}"
-[ -z "$TOK" ] && { echo "No theme token (SHOPIFY_DW_ADMIN_TOKEN_CONTENT) found; export THEME_TOKEN=… and retry"; exit 1; }
-[ -f "$SRC" ] || { echo "missing $SRC"; exit 1; }
-echo "token …${TOK: -4}  | source $SRC ($(wc -c <"$SRC") bytes)"
-
-echo "=== themes (id role name) ==="
-THEMES_JSON=$(curl -s "https://$STORE/admin/api/$API/themes.json" -H "X-Shopify-Access-Token: $TOK")
-echo "$THEMES_JSON" | python3 -c 'import sys,json
-d=json.load(sys.stdin)
-if "themes" not in d: print("API ERROR:",d); sys.exit(1)
-for t in d["themes"]: print(" ",t["id"],t["role"],"|",t["name"])'
-
-# Pick DEV theme: role=development, else newest non-main (unpublished). NEVER main.
-DEV_ID=$(echo "$THEMES_JSON" | python3 -c 'import sys,json
-ts=json.load(sys.stdin)["themes"]
-dev=[t for t in ts if t["role"]=="development"]
-if not dev: dev=sorted([t for t in ts if t["role"]!="main"], key=lambda t:t.get("updated_at",""), reverse=True)
-print(dev[0]["id"] if dev else "")')
-[ -z "$DEV_ID" ] && { echo "No dev/unpublished theme found. Create one in Online Store → Themes → Add theme, or push to a named theme."; exit 1; }
-echo "=== target DEV theme: $DEV_ID (NOT live) ==="
+TOK="${THEME_TOKEN:-$(grep -hE '^SHOPIFY_THEME_TOKEN=' "$SECRETS" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"'"'"' ')}"
+[ -z "$TOK" ] && { echo "No theme token. Create a Shopify 'Theme Access' password and run:  THEME_TOKEN=shptka_xxx bash ~/pd.sh"; exit 1; }
+echo "token …${TOK: -4}"
 
 mkdir -p theme-backups
-BK="theme-backups/theme.liquid.dev-$DEV_ID.$(date +%Y%m%d-%H%M%S).bak"
-curl -s "https://$STORE/admin/api/$API/themes/$DEV_ID/assets.json?asset[key]=layout/theme.liquid" \
-  -H "X-Shopify-Access-Token: $TOK" \
-  | python3 -c 'import sys,json;a=json.load(sys.stdin).get("asset");open(sys.argv[1],"w").write(a["value"]) if a else print("no existing asset (new theme)")' "$BK" || true
-[ -s "$BK" ] && echo "backup: $BK ($(wc -c <"$BK") bytes)" || echo "no prior theme.liquid backed up (theme may be empty)"
+python3 - "$STORE" "$API" "$TOK" "$TARGET_NAME" "theme-backups" <<'PY'
+import sys, json, urllib.request, urllib.parse, datetime
+store, api, tok, target_name, bkdir = sys.argv[1:6]
+H = {"X-Shopify-Access-Token": tok, "Content-Type": "application/json"}
+
+def get(url):
+    return json.load(urllib.request.urlopen(urllib.request.Request(url, headers=H)))
 
-echo "=== PUT layout/theme.liquid → dev theme $DEV_ID ==="
-python3 - "$STORE" "$API" "$DEV_ID" "$TOK" "$SRC" <<'PY'
-import sys,json,urllib.request
-store,api,tid,tok,src=sys.argv[1:6]
-val=open(src).read()
-body=json.dumps({"asset":{"key":"layout/theme.liquid","value":val}}).encode()
-req=urllib.request.Request(f"https://{store}/admin/api/{api}/themes/{tid}/assets.json",
-    data=body, method="PUT",
-    headers={"X-Shopify-Access-Token":tok,"Content-Type":"application/json"})
+# 1. find the dev theme by exact name (fallback: role=development), never main
 try:
-    r=urllib.request.urlopen(req)
-    print("PUT HTTP",r.status,"OK")
+    themes = get(f"https://{store}/admin/api/{api}/themes.json")["themes"]
 except urllib.error.HTTPError as e:
-    print("PUT FAILED HTTP",e.code,e.read().decode()[:300]); sys.exit(1)
-PY
+    print("THEMES LIST FAILED HTTP", e.code, e.read().decode()[:300]); sys.exit(1)
+print("themes:")
+for t in themes: print("  ", t["id"], t["role"], "|", t["name"])
+match = [t for t in themes if t["name"] == target_name]
+if not match: match = [t for t in themes if t["role"] == "development"]
+match = [t for t in match if t["role"] != "main"]
+if not match:
+    print(f'Target theme "{target_name}" not found among non-main themes.'); sys.exit(1)
+theme = match[0]; tid = theme["id"]
+print(f'=== target: {tid} "{theme["name"]}" (role={theme["role"]}) — live untouched ===')
+
+# 2. pull current theme.liquid
+key = "layout/theme.liquid"
+q = urllib.parse.urlencode({"asset[key]": key})
+cur = get(f"https://{store}/admin/api/{api}/themes/{tid}/assets.json?{q}")["asset"]["value"]
+stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
+bk = f"{bkdir}/theme.liquid.dev-{tid}.{stamp}.bak"
+open(bk, "w").write(cur)
+print(f"backup: {bk} ({len(cur)} bytes)")
+
+src = cur
+changes = []
 
-echo "=== verify (re-GET asset, check for new UX markers) ==="
-curl -s "https://$STORE/admin/api/$API/themes/$DEV_ID/assets.json?asset[key]=layout/theme.liquid" \
-  -H "X-Shopify-Access-Token: $TOK" \
-  | python3 -c 'import sys,json;v=json.load(sys.stdin)["asset"]["value"];print("dw-variant-btns:", v.count("dw-variant-btns"), "| theme-agnostic restore:", v.count("shopify-section-template")>0)'
+# 3a. price-restore selector hardening (only if the dead selector is present)
+old_restore = ('#shopify-section-product [class*="price"],\n'
+               '#shopify-section-product span.money,')
+new_restore = ('[id^="shopify-section-template"][id$="__main"] .product-price-minimum,\n'
+               '[id^="shopify-section-template"][id$="__main"] [class*="product-price"],\n'
+               '[id^="shopify-section-template"][id$="__main"] span.money,\n'
+               '.product__price .money,')
+if "shopify-section-template" not in src and old_restore in src:
+    src = src.replace(old_restore, new_restore, 1); changes.append("price-restore-hardening")
+elif "shopify-section-template" in src:
+    changes.append("price-restore-already-present")
 
-echo
-echo "PREVIEW the dev theme:"
-echo "  https://$STORE/?preview_theme_id=$DEV_ID"
-echo "  (or a product: https://www.designerwallcoverings.com/products/cirrus-jade-dwdx-220230?preview_theme_id=$DEV_ID )"
-echo "Revert if needed: PUT the backup file $BK back to layout/theme.liquid on theme $DEV_ID."
+# 3b. variant buttons block, inserted after the first "<!-- /DW -->"
+BLOCK = """
+{% if template contains 'product' %}
+<!-- DW: variant option buttons — replace native dropdown with side-by-side name-only buttons (2026-06-21) -->
+<style>
+  .dw-variant-btns{display:flex;gap:10px;margin:10px 0 4px;flex-wrap:wrap}
+  .dw-variant-btns button{flex:1 1 0;min-width:120px;padding:12px 14px;border:1px solid #111;
+    background:#fff;color:#111;font:inherit;letter-spacing:.04em;text-transform:uppercase;
+    font-size:12px;cursor:pointer;border-radius:2px;transition:background .12s,color .12s}
+  .dw-variant-btns button:hover{background:#f2f2f2}
+  .dw-variant-btns button[aria-pressed="true"],
+  .dw-variant-btns button[aria-pressed="true"]:hover{background:#111;color:#fff}
+  .dw-hide-native{display:none !important}
+</style>
+<script>
+(function(){
+  function build(){
+    var sels=document.querySelectorAll('.single-option-selector,[id^="single-option-"]');
+    Array.prototype.forEach.call(sels,function(sel){
+      if(sel.dataset.dwBtnDone) return;
+      if(!sel.options||sel.options.length<2){ sel.dataset.dwBtnDone='1'; return; }
+      sel.dataset.dwBtnDone='1';
+      var wrap=document.createElement('div'); wrap.className='dw-variant-btns';
+      Array.prototype.forEach.call(sel.options,function(opt){
+        var b=document.createElement('button');
+        b.type='button';
+        b.textContent=(opt.text||'').trim();
+        b.setAttribute('data-value',opt.value);
+        b.setAttribute('aria-pressed', opt.selected?'true':'false');
+        b.addEventListener('click',function(){
+          sel.value=opt.value;
+          sel.dispatchEvent(new Event('change',{bubbles:true}));
+          Array.prototype.forEach.call(wrap.querySelectorAll('button'),function(x){
+            x.setAttribute('aria-pressed', x===b?'true':'false');
+          });
+        });
+        wrap.appendChild(b);
+      });
+      var host=sel.closest('.select-wrapper')||sel.closest('.selector-wrapper')||sel.parentElement;
+      host.classList.add('dw-hide-native');
+      host.parentElement.insertBefore(wrap,host);
+    });
+  }
+  function init(){ build(); setTimeout(build,400); setTimeout(build,1200); }
+  if(document.readyState!=='loading') init();
+  else document.addEventListener('DOMContentLoaded',init);
+})();
+</script>
+<!-- /DW variant buttons -->
+{% endif %}
+"""
+if "dw-variant-btns" in src:
+    changes.append("variant-buttons-already-present")
+elif "<!-- /DW -->" in src:
+    src = src.replace("<!-- /DW -->", "<!-- /DW -->\n" + BLOCK, 1); changes.append("variant-buttons-added")
+else:
+    # no DW anchor: inject before </head>
+    src = src.replace("</head>", BLOCK + "\n</head>", 1); changes.append("variant-buttons-added-before-head")
+
+print("changes:", ", ".join(changes))
+if src == cur:
+    print("nothing to change (already applied). done."); sys.exit(0)
+
+# 4. push transformed content back
+body = json.dumps({"asset": {"key": key, "value": src}}).encode()
+req = urllib.request.Request(f"https://{store}/admin/api/{api}/themes/{tid}/assets.json",
+                            data=body, method="PUT", headers=H)
+try:
+    r = urllib.request.urlopen(req); print("PUT HTTP", r.status, "OK", f"({len(src)} bytes)")
+except urllib.error.HTTPError as e:
+    print("PUT FAILED HTTP", e.code, e.read().decode()[:300]); sys.exit(1)
+
+# 5. verify
+ver = get(f"https://{store}/admin/api/{api}/themes/{tid}/assets.json?{q}")["asset"]["value"]
+print("verify: dw-variant-btns x", ver.count("dw-variant-btns"),
+      "| theme-agnostic restore:", "shopify-section-template" in ver)
+print()
+print("PREVIEW:")
+print(f"  https://www.designerwallcoverings.com/products/cirrus-jade-dwdx-220230?preview_theme_id={tid}")
+print(f"  revert: PUT {bk} back to {key} on theme {tid}")
+PY

← 7cf36ba5 Add dev-theme push script for variant-button UX (targets dev  ·  back to Designer Wallcoverings  ·  auto-save: 2026-06-21T19:01:26 (1 files) — shopify/scripts/g 9f4a59ef →