@runwell/shopify-toolkit 0.18.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/bin/runwell-shopify +10 -1
  2. package/lib/list.js +22 -9
  3. package/lib/qa-bundles.js +117 -0
  4. package/lib/qa.js +147 -13
  5. package/modules/INDEX.md +14 -5
  6. package/modules/bundle-builder/README.md +6 -1
  7. package/modules/bundle-builder/module.json +5 -1
  8. package/modules/cart-cross-sell/snippets/runwell-cart-xsell.liquid +16 -0
  9. package/modules/runwell-bundle-system/README.md +35 -0
  10. package/modules/runwell-bundle-system/admin-metafields.json +46 -0
  11. package/modules/runwell-bundle-system/assets/runwell-bundle-system.css +861 -0
  12. package/modules/runwell-bundle-system/assets/runwell-bundle-system.js +287 -0
  13. package/modules/runwell-bundle-system/module.json +126 -0
  14. package/modules/runwell-bundle-system/qa/mobile-checklist.md +105 -0
  15. package/modules/runwell-bundle-system/sections/runwell-bundle-cart-xsell.liquid +59 -0
  16. package/modules/runwell-bundle-system/sections/runwell-bundle-collection.liquid +121 -0
  17. package/modules/runwell-bundle-system/sections/runwell-bundle-home-stacks.liquid +77 -0
  18. package/modules/runwell-bundle-system/sections/runwell-bundle-pdp-banner.liquid +50 -0
  19. package/modules/runwell-bundle-system/sections/runwell-bundle-pdp-pairs-with.liquid +72 -0
  20. package/modules/runwell-bundle-system/sections/runwell-bundle-pdp.liquid +105 -0
  21. package/modules/runwell-bundle-system/settings.json +25 -0
  22. package/modules/runwell-bundle-system/snippets/runwell-bundle-card.liquid +70 -0
  23. package/modules/runwell-bundle-system/snippets/runwell-bundle-cross-supplier.liquid +18 -0
  24. package/modules/runwell-bundle-system/snippets/runwell-bundle-data.liquid +67 -0
  25. package/modules/runwell-bundle-system/snippets/runwell-bundle-fomo.liquid +32 -0
  26. package/modules/runwell-bundle-system/snippets/runwell-bundle-free-gift.liquid +34 -0
  27. package/modules/runwell-bundle-system/snippets/runwell-bundle-multi-product.liquid +86 -0
  28. package/modules/runwell-bundle-system/snippets/runwell-bundle-pricing.liquid +30 -0
  29. package/modules/runwell-bundle-system/snippets/runwell-bundle-quantity-tiers.liquid +73 -0
  30. package/package.json +1 -1
@@ -0,0 +1,34 @@
1
+ {%- comment -%}
2
+ runwell-bundle-system: runwell-bundle-free-gift.liquid.
3
+ Wraps the gift-with-purchase module. The bundle's free-gift handle is
4
+ passed in. Display only here; the actual auto-add to cart logic lives
5
+ in the gift-with-purchase module's runwell-gwp.js.
6
+
7
+ Inputs:
8
+ bundle_free_gift_handle (product handle of the gift)
9
+ {%- endcomment -%}
10
+
11
+ {%- if bundle_free_gift_handle != blank -%}
12
+ {%- assign gift_product = all_products[bundle_free_gift_handle] -%}
13
+ {%- if gift_product != blank -%}
14
+ <div class="runwell-bundle-system__free-gift" data-runwell-bundle-free-gift data-gift-handle="{{ bundle_free_gift_handle }}">
15
+ <p class="runwell-bundle-system__free-gift-label">Plus a free gift:</p>
16
+ <div class="runwell-bundle-system__free-gift-product">
17
+ {%- if gift_product.featured_image -%}
18
+ {{ gift_product.featured_image | image_url: width: 80 | image_tag:
19
+ width: 60,
20
+ height: 60,
21
+ loading: 'lazy',
22
+ class: 'runwell-bundle-system__free-gift-thumb',
23
+ alt: gift_product.title
24
+ }}
25
+ {%- endif -%}
26
+ <span class="runwell-bundle-system__free-gift-name">{{ gift_product.title }}</span>
27
+ <span class="runwell-bundle-system__free-gift-price">
28
+ <s>{{ gift_product.price | money }}</s>
29
+ <strong>Free</strong>
30
+ </span>
31
+ </div>
32
+ </div>
33
+ {%- endif -%}
34
+ {%- endif -%}
@@ -0,0 +1,86 @@
1
+ {%- comment -%}
2
+ runwell-bundle-system: runwell-bundle-multi-product.liquid.
3
+ Mode B render. Lists the bundle's component products (thumbnail, qty,
4
+ name, individual price), the bundle pricing block (delegated to
5
+ runwell-bundle-pricing), and the single Add to Cart button that adds
6
+ the bundle product as one line item.
7
+
8
+ Inputs:
9
+ product
10
+ bundle_pricing_model
11
+ bundle_components (array of {product_handle, qty})
12
+ bundle_subtotal (sum of component prices)
13
+ bundle_price (the bundle product's price)
14
+ bundle_savings_amount
15
+ bundle_savings_pct
16
+ {%- endcomment -%}
17
+
18
+ {% render 'runwell-bundle-pricing',
19
+ bundle_pricing_model: bundle_pricing_model,
20
+ bundle_price: bundle_price,
21
+ bundle_subtotal: bundle_subtotal,
22
+ bundle_savings_amount: bundle_savings_amount,
23
+ bundle_savings_pct: bundle_savings_pct
24
+ %}
25
+
26
+ <div class="runwell-bundle-system__components">
27
+ <p class="runwell-bundle-system__components-heading">Includes:</p>
28
+ <ul class="runwell-bundle-system__components-list" role="list">
29
+ {%- if bundle_components and bundle_components.size > 0 -%}
30
+ {%- for component in bundle_components -%}
31
+ {%- assign comp_product = all_products[component.product_handle] -%}
32
+ {%- assign comp_qty = component.qty | default: 1 -%}
33
+ <li class="runwell-bundle-system__component">
34
+ {%- if comp_product != blank and comp_product.featured_image -%}
35
+ {{ comp_product.featured_image | image_url: width: 120 | image_tag:
36
+ width: 60,
37
+ height: 60,
38
+ loading: 'lazy',
39
+ class: 'runwell-bundle-system__component-thumb',
40
+ alt: comp_product.title
41
+ }}
42
+ {%- else -%}
43
+ <span class="runwell-bundle-system__component-thumb runwell-bundle-system__component-thumb--placeholder" aria-hidden="true"></span>
44
+ {%- endif -%}
45
+
46
+ <span class="runwell-bundle-system__component-qty">{{ comp_qty }}x</span>
47
+
48
+ <span class="runwell-bundle-system__component-name">
49
+ {%- if comp_product != blank -%}
50
+ {{ comp_product.title }}
51
+ {%- else -%}
52
+ <em>(unavailable)</em>
53
+ {%- endif -%}
54
+ </span>
55
+
56
+ {%- if comp_product != blank -%}
57
+ <span class="runwell-bundle-system__component-price">{{ comp_product.price | money }}</span>
58
+ {%- endif -%}
59
+ </li>
60
+ {%- endfor -%}
61
+ {%- else -%}
62
+ <li class="runwell-bundle-system__component runwell-bundle-system__component--empty">
63
+ <em>No components configured. Set the runwell.bundle_components metafield on this bundle product.</em>
64
+ </li>
65
+ {%- endif -%}
66
+ </ul>
67
+ </div>
68
+
69
+ {%- form 'product', product, class: 'runwell-bundle-system__form', novalidate: 'novalidate' -%}
70
+ <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
71
+ <input type="hidden" name="quantity" value="1">
72
+ <button
73
+ type="submit"
74
+ name="add"
75
+ class="runwell-bundle-system__atc button button--primary button--full-width"
76
+ {% if product.available == false %}disabled{% endif %}
77
+ >
78
+ <span class="runwell-bundle-system__atc-label">
79
+ {%- if product.available -%}
80
+ Add bundle to cart
81
+ {%- else -%}
82
+ Sold out
83
+ {%- endif -%}
84
+ </span>
85
+ </button>
86
+ {%- endform -%}
@@ -0,0 +1,30 @@
1
+ {%- comment -%}
2
+ runwell-bundle-system: runwell-bundle-pricing.liquid.
3
+ Shared price + strikethrough subtotal + savings badge. Used by Mode B
4
+ (multi-product) and bundle cards on Surfaces 1 to 5.
5
+
6
+ Inputs:
7
+ bundle_price (the bundle product's selling price, in cents)
8
+ bundle_subtotal (sum of component prices, in cents; 0 if not multi-product)
9
+ bundle_savings_amount (subtotal - price, in cents; 0 if no savings)
10
+ bundle_savings_pct (savings_amount / subtotal * 100; 0 if no savings)
11
+ bundle_pricing_model (used to choose dollar vs percent badge format)
12
+ {%- endcomment -%}
13
+
14
+ <div class="runwell-bundle-system__pricing">
15
+ <span class="runwell-bundle-system__pricing-current">{{ bundle_price | money }}</span>
16
+
17
+ {%- if bundle_subtotal > bundle_price -%}
18
+ <span class="runwell-bundle-system__pricing-strikethrough">{{ bundle_subtotal | money }}</span>
19
+ {%- endif -%}
20
+
21
+ {%- if bundle_savings_amount > 0 -%}
22
+ <span class="runwell-bundle-system__pricing-badge">
23
+ {%- if bundle_pricing_model == 'percent_off_subtotal' -%}
24
+ Save {{ bundle_savings_pct }}%
25
+ {%- else -%}
26
+ Save {{ bundle_savings_amount | money_without_trailing_zeros }}
27
+ {%- endif -%}
28
+ </span>
29
+ {%- endif -%}
30
+ </div>
@@ -0,0 +1,73 @@
1
+ {%- comment -%}
2
+ runwell-bundle-system: runwell-bundle-quantity-tiers.liquid.
3
+ Mode A radio picker. Renders one row per tier in bundle_quantity_tiers
4
+ metafield. Default selection is the highest discount_pct tier (last in
5
+ the array, by convention). Display only; the discount itself is applied
6
+ by a Shopify Function (see module.json admin_steps:
7
+ configure-quantity-tier-discount-function).
8
+
9
+ Inputs (passed via render):
10
+ product
11
+ bundle_pricing_model
12
+ bundle_quantity_tiers (array of {qty, discount_pct})
13
+ section
14
+ {%- endcomment -%}
15
+
16
+ {%- if bundle_quantity_tiers and bundle_quantity_tiers.size > 0 -%}
17
+ {%- assign default_tier_index = bundle_quantity_tiers.size | minus: 1 -%}
18
+
19
+ {%- form 'product', product, class: 'runwell-bundle-system__form', novalidate: 'novalidate' -%}
20
+ <fieldset class="runwell-bundle-system__tiers" aria-label="Choose quantity">
21
+ <legend class="visually-hidden">Choose quantity</legend>
22
+ {%- for tier in bundle_quantity_tiers -%}
23
+ {%- assign discount_amount = product.price | times: tier.discount_pct | divided_by: 100 -%}
24
+ {%- assign per_unit_price = product.price | minus: discount_amount -%}
25
+ {%- assign total_price = per_unit_price | times: tier.qty -%}
26
+ {%- assign full_price = product.price | times: tier.qty -%}
27
+ {%- assign savings = full_price | minus: total_price -%}
28
+
29
+ <label class="runwell-bundle-system__tier" data-qty="{{ tier.qty }}" data-discount-pct="{{ tier.discount_pct }}">
30
+ <input
31
+ type="radio"
32
+ name="quantity"
33
+ value="{{ tier.qty }}"
34
+ class="runwell-bundle-system__tier-input"
35
+ {% if forloop.index0 == default_tier_index %}checked{% endif %}
36
+ >
37
+ <span class="runwell-bundle-system__tier-row">
38
+ <span class="runwell-bundle-system__tier-qty">{{ tier.qty }}{% if tier.qty == 1 %} unit{% else %} units{% endif %}</span>
39
+ {%- if tier.discount_pct > 0 -%}
40
+ <span class="runwell-bundle-system__tier-savings-badge">Save {{ tier.discount_pct }}%</span>
41
+ {%- endif -%}
42
+ </span>
43
+ <span class="runwell-bundle-system__tier-row runwell-bundle-system__tier-row--prices">
44
+ <span class="runwell-bundle-system__tier-total">{{ total_price | money }}</span>
45
+ {%- if tier.discount_pct > 0 -%}
46
+ <span class="runwell-bundle-system__tier-strikethrough">{{ full_price | money }}</span>
47
+ {%- endif -%}
48
+ <span class="runwell-bundle-system__tier-per-unit">{{ per_unit_price | money }} / unit</span>
49
+ </span>
50
+ </label>
51
+ {%- endfor -%}
52
+ </fieldset>
53
+
54
+ <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
55
+
56
+ <button
57
+ type="submit"
58
+ name="add"
59
+ class="runwell-bundle-system__atc button button--primary button--full-width"
60
+ {% if product.selected_or_first_available_variant.available == false %}disabled{% endif %}
61
+ >
62
+ <span class="runwell-bundle-system__atc-label">
63
+ {%- if product.selected_or_first_available_variant.available -%}
64
+ Add to cart
65
+ {%- else -%}
66
+ Sold out
67
+ {%- endif -%}
68
+ </span>
69
+ </button>
70
+ {%- endform -%}
71
+ {%- else -%}
72
+ <p class="runwell-bundle-system__empty">No quantity tiers configured for this bundle.</p>
73
+ {%- endif -%}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runwell/shopify-toolkit",
3
- "version": "0.18.0",
3
+ "version": "0.21.0",
4
4
  "description": "Reusable Shopify theme modules from Runwell. Replaces typically app-driven features (reviews, wishlist, urgency, FAQ, post-purchase upsell, exit popups, free-ship progress, sticky ATC, testimonials, badges, bundles) with native Liquid + JS + CSS that ship across multiple client themes via a config-driven sync CLI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",