@runwell/shopify-toolkit 0.21.0 → 0.24.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.
- package/lib/init.js +13 -2
- package/modules/INDEX.md +3 -3
- package/modules/runwell-bundle-system/admin-metafields.json +15 -3
- package/modules/runwell-bundle-system/assets/runwell-bundle-quantity-builder.css +383 -0
- package/modules/runwell-bundle-system/assets/runwell-bundle-system.css +246 -0
- package/modules/runwell-bundle-system/assets/runwell-bundle-system.js +359 -0
- package/modules/runwell-bundle-system/module.json +18 -4
- package/modules/runwell-bundle-system/qa/v1.5-mobile-qa-checklist.md +103 -0
- package/modules/runwell-bundle-system/sections/runwell-bundle-cart-xsell.liquid +2 -1
- package/modules/runwell-bundle-system/sections/runwell-bundle-collection.liquid +20 -8
- package/modules/runwell-bundle-system/sections/runwell-bundle-pdp-banner.liquid +2 -1
- package/modules/runwell-bundle-system/sections/runwell-bundle-pdp-pairs-with.liquid +2 -1
- package/modules/runwell-bundle-system/sections/runwell-bundle-pdp.liquid +15 -1
- package/modules/runwell-bundle-system/sections/runwell-bundle-quantity-builder.liquid +318 -0
- package/modules/runwell-bundle-system/snippets/runwell-bundle-byob-picker-accordion.liquid +84 -0
- package/modules/runwell-bundle-system/snippets/runwell-bundle-byob-picker-grid.liquid +72 -0
- package/modules/runwell-bundle-system/snippets/runwell-bundle-byob-picker-radio.liquid +77 -0
- package/modules/runwell-bundle-system/snippets/runwell-bundle-byob-picker.liquid +71 -0
- package/modules/runwell-bundle-system/snippets/runwell-bundle-byob-summary.liquid +39 -0
- package/modules/runwell-bundle-system/snippets/runwell-bundle-card.liquid +15 -2
- package/modules/runwell-bundle-system/snippets/runwell-bundle-cart-xsell.liquid +85 -0
- package/modules/runwell-bundle-system/snippets/runwell-bundle-data.liquid +8 -0
- package/modules/scratch-popup/README.md +88 -0
- package/modules/scratch-popup/SPEC.md +120 -0
- package/modules/scratch-popup/assets/runwell-scratch-popup.css +315 -0
- package/modules/scratch-popup/assets/runwell-scratch-popup.js +367 -0
- package/modules/scratch-popup/module.json +128 -0
- package/modules/scratch-popup/sections/runwell-scratch-popup.liquid +184 -0
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# v1.5 mobile QA checklist: Mode C BYOB + admin block
|
|
2
|
+
|
|
3
|
+
Re-validates the storefront at 375 px after Mode C BYOB lands, plus admin block usability on Shopify's mobile admin app. Mirrors BS-14's v1 pass; adds the new v1.5 surfaces.
|
|
4
|
+
|
|
5
|
+
## Environment
|
|
6
|
+
|
|
7
|
+
- Real iPhone (iPhone 12 or newer; iOS 17+).
|
|
8
|
+
- Safari for storefront; Shopify mobile admin app for admin block.
|
|
9
|
+
- Staging tenant with at least:
|
|
10
|
+
- 1 Mode A bundle (Lusha pattern, for regression).
|
|
11
|
+
- 1 Mode B bundle (Lushi pattern, for regression).
|
|
12
|
+
- 1 Mode C bundle per layout: grid, accordion, radio.
|
|
13
|
+
|
|
14
|
+
## Storefront checks (per Mode C layout)
|
|
15
|
+
|
|
16
|
+
### Grid layout
|
|
17
|
+
|
|
18
|
+
- [ ] Picker renders at 2 columns at 375 px.
|
|
19
|
+
- [ ] Each candidate card has a 44 px tap target.
|
|
20
|
+
- [ ] Tap toggles selection; checkmark / border state matches the rest of the bundle module.
|
|
21
|
+
- [ ] Live counter updates as you tap.
|
|
22
|
+
- [ ] Subtotal + savings refresh client-side.
|
|
23
|
+
- [ ] ATC stays disabled until min picks reached; enables when in range.
|
|
24
|
+
- [ ] Max-pick cap drops the oldest selection (or refuses the new one, depending on convention).
|
|
25
|
+
- [ ] Required candidates are pre-checked and not deselectable.
|
|
26
|
+
- [ ] Unavailable candidates render disabled, not removed.
|
|
27
|
+
- [ ] Submit posts a multi-line cart add; cart drawer reflects the picks.
|
|
28
|
+
|
|
29
|
+
### Accordion layout
|
|
30
|
+
|
|
31
|
+
- [ ] Categories render as `<details>` blocks; first one open by default.
|
|
32
|
+
- [ ] Per-category count badge updates on each pick.
|
|
33
|
+
- [ ] Total picks across categories enforced against bundle_byob_max_picks.
|
|
34
|
+
- [ ] Required candidates ship pre-checked in their parent category.
|
|
35
|
+
|
|
36
|
+
### Radio layout
|
|
37
|
+
|
|
38
|
+
- [ ] Each category renders as a radio group; first option pre-selected.
|
|
39
|
+
- [ ] One pick per category enforced (radios).
|
|
40
|
+
- [ ] Total picks equals category count; ATC enables once every category has a pick.
|
|
41
|
+
- [ ] Switching pick within a category updates subtotal + total.
|
|
42
|
+
|
|
43
|
+
## Storefront regression (v1 surfaces with a Mode C bundle in catalog)
|
|
44
|
+
|
|
45
|
+
- [ ] /bundles page lists the Mode C bundle alongside Mode A and Mode B.
|
|
46
|
+
- [ ] Bundle card hero variant renders correctly with the BYOB savings badge.
|
|
47
|
+
- [ ] PDP pairs-with widget shows the Mode C bundle when a component is the current PDP.
|
|
48
|
+
- [ ] Home page curated stacks strip includes the Mode C bundle when picked.
|
|
49
|
+
- [ ] Cart drawer cross-sell offers the Mode C bundle when a partial set is in cart.
|
|
50
|
+
- [ ] PDP banner with a Mode C bundle reads "Save N% when bundled".
|
|
51
|
+
|
|
52
|
+
## Admin block checks (Shopify mobile admin app)
|
|
53
|
+
|
|
54
|
+
Best-effort: mobile admin is rare; document any limitations.
|
|
55
|
+
|
|
56
|
+
### Composition panel (BS-205)
|
|
57
|
+
|
|
58
|
+
- [ ] Mode selector toggles between tier / component / candidate sub-UIs.
|
|
59
|
+
- [ ] Add / remove / reorder works in each mode.
|
|
60
|
+
- [ ] Validation banner surfaces when constraints are violated.
|
|
61
|
+
|
|
62
|
+
### Pricing panel (BS-206)
|
|
63
|
+
|
|
64
|
+
- [ ] Pricing model dropdown limits to allowed modes per bundle_mode.
|
|
65
|
+
- [ ] Value editor changes shape with model.
|
|
66
|
+
- [ ] Save disabled when value is empty.
|
|
67
|
+
|
|
68
|
+
### Surfaces panel (BS-207)
|
|
69
|
+
|
|
70
|
+
- [ ] All 6 surface toggles render.
|
|
71
|
+
- [ ] Toggle state persists after save.
|
|
72
|
+
|
|
73
|
+
### Cross-supplier banner (BS-208)
|
|
74
|
+
|
|
75
|
+
- [ ] Banner appears when components span multiple suppliers.
|
|
76
|
+
- [ ] Mentions the actual supplier count.
|
|
77
|
+
- [ ] Suppresses when count is 1.
|
|
78
|
+
|
|
79
|
+
### Bundle preview (BS-209)
|
|
80
|
+
|
|
81
|
+
- [ ] Iframe renders at 375 px.
|
|
82
|
+
- [ ] Preview reflects the saved state (not buffered edits).
|
|
83
|
+
|
|
84
|
+
## Cross-cutting
|
|
85
|
+
|
|
86
|
+
- [ ] No horizontal scroll on any storefront surface.
|
|
87
|
+
- [ ] All tap targets >= 44 px.
|
|
88
|
+
- [ ] Total module bytes still under 80 KB (CSS + JS, pre-minification).
|
|
89
|
+
- [ ] No console errors on storefront or admin.
|
|
90
|
+
- [ ] Lighthouse mobile score >= 80 on the BYOB PDP.
|
|
91
|
+
|
|
92
|
+
## Screenshot capture
|
|
93
|
+
|
|
94
|
+
Per surface, capture 3 states (default, mid-interaction, validation error).
|
|
95
|
+
|
|
96
|
+
Path: `runwell-shopify-toolkit/docs/bundle-system/qa/v1.5-screenshots-YYYY-MM-DD/`.
|
|
97
|
+
|
|
98
|
+
## Sign-off
|
|
99
|
+
|
|
100
|
+
- [ ] All applicable checks passed.
|
|
101
|
+
- [ ] Screenshots captured and committed.
|
|
102
|
+
- [ ] P1 failures fixed; P2 logged for v1.5 patch.
|
|
103
|
+
- [ ] QA owner + date: ___________________
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
{%- if settings.bundle_system__surface_4_cart_drawer_xsell_enabled == false -%}
|
|
21
21
|
{%- comment -%} Tenant has disabled this surface. {%- endcomment -%}
|
|
22
22
|
{%- else -%}
|
|
23
|
-
{%- assign
|
|
23
|
+
{%- assign bundle_index_instance = shop.metaobjects.bundle_index.values | first -%}
|
|
24
|
+
{%- assign bundle_index = bundle_index_instance.entries.value -%}
|
|
24
25
|
{%- assign render_slot = false -%}
|
|
25
26
|
{%- if bundle_index and bundle_index.products and bundle_index.bundles -%}
|
|
26
27
|
{%- assign render_slot = true -%}
|
|
@@ -11,15 +11,27 @@
|
|
|
11
11
|
|
|
12
12
|
{%- assign mode = section.settings.mode | default: 'grid' -%}
|
|
13
13
|
|
|
14
|
-
{%- comment -%}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
{%- comment -%}
|
|
15
|
+
Build the bundles list. Prefer the bundle_index shop metaobject when
|
|
16
|
+
populated (one read; deterministic). Fall back to scanning all_products
|
|
17
|
+
for products tagged 'bundle'.
|
|
18
|
+
{%- endcomment -%}
|
|
19
|
+
{%- assign bundle_index_instance = shop.metaobjects.bundle_index.values | first -%}
|
|
20
|
+
{%- assign bundle_index = bundle_index_instance.entries.value -%}
|
|
21
|
+
{%- assign bundles = '' | split: '' -%}
|
|
22
|
+
{%- if bundle_index and bundle_index.bundles -%}
|
|
23
|
+
{%- for entry in bundle_index.bundles -%}
|
|
24
|
+
{%- assign bp = all_products[entry[0]] -%}
|
|
25
|
+
{%- if bp != blank and bp.handle != blank -%}
|
|
26
|
+
{%- assign bundles = bundles | concat: bp -%}
|
|
27
|
+
{%- endif -%}
|
|
28
|
+
{%- endfor -%}
|
|
18
29
|
{%- endif -%}
|
|
19
|
-
|
|
20
|
-
{%- assign
|
|
21
|
-
{%-
|
|
22
|
-
{%-
|
|
30
|
+
{%- if bundles.size == 0 -%}
|
|
31
|
+
{%- assign source_products = collection.products -%}
|
|
32
|
+
{%- if source_products == blank or source_products.size == 0 -%}
|
|
33
|
+
{%- assign source_products = all_products -%}
|
|
34
|
+
{%- endif -%}
|
|
23
35
|
{%- assign bundles = source_products | where: 'tags', 'bundle' -%}
|
|
24
36
|
{%- endif -%}
|
|
25
37
|
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
{%- if settings.bundle_system__surface_5_pdp_banner_enabled == false -%}
|
|
11
11
|
{%- comment -%} Tenant has disabled this surface. {%- endcomment -%}
|
|
12
12
|
{%- else -%}
|
|
13
|
-
{%- assign
|
|
13
|
+
{%- assign bundle_index_instance = shop.metaobjects.bundle_index.values | first -%}
|
|
14
|
+
{%- assign bundle_index = bundle_index_instance.entries.value -%}
|
|
14
15
|
{%- assign matching_handles = '' -%}
|
|
15
16
|
{%- if bundle_index and bundle_index.products -%}
|
|
16
17
|
{%- assign matching_handles = bundle_index.products[product.handle] -%}
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
{%- if settings.bundle_system__surface_2_pdp_pairs_with_enabled == false -%}
|
|
13
13
|
{%- comment -%} Tenant has disabled this surface. {%- endcomment -%}
|
|
14
14
|
{%- else -%}
|
|
15
|
-
{%- assign
|
|
15
|
+
{%- assign bundle_index_instance = shop.metaobjects.bundle_index.values | first -%}
|
|
16
|
+
{%- assign bundle_index = bundle_index_instance.entries.value -%}
|
|
16
17
|
{%- assign matching_handles = '' -%}
|
|
17
18
|
{%- if bundle_index and bundle_index.products -%}
|
|
18
19
|
{%- assign matching_handles = bundle_index.products[product.handle] -%}
|
|
@@ -58,6 +58,18 @@
|
|
|
58
58
|
bundle_savings_amount: bundle_savings_amount,
|
|
59
59
|
bundle_savings_pct: bundle_savings_pct
|
|
60
60
|
%}
|
|
61
|
+
{%- elsif bundle_mode == 'byob' -%}
|
|
62
|
+
{% render 'runwell-bundle-byob-picker',
|
|
63
|
+
product: product,
|
|
64
|
+
bundle_pricing_model: bundle_pricing_model,
|
|
65
|
+
bundle_pricing_value: bundle_pricing_value,
|
|
66
|
+
bundle_byob_candidates: bundle_byob_candidates,
|
|
67
|
+
bundle_byob_min_picks: bundle_byob_min_picks,
|
|
68
|
+
bundle_byob_max_picks: bundle_byob_max_picks,
|
|
69
|
+
bundle_byob_layout: bundle_byob_layout,
|
|
70
|
+
bundle_byob_categories: bundle_byob_categories,
|
|
71
|
+
bundle_byob_required_handles: bundle_byob_required_handles
|
|
72
|
+
%}
|
|
61
73
|
{%- endif -%}
|
|
62
74
|
|
|
63
75
|
{%- if bundle_fomo_mode != 'none' and bundle_fomo_mode != blank -%}
|
|
@@ -100,6 +112,8 @@
|
|
|
100
112
|
"presets": [
|
|
101
113
|
{ "name": "Bundle PDP", "category": "Runwell" }
|
|
102
114
|
],
|
|
103
|
-
"
|
|
115
|
+
"enabled_on": {
|
|
116
|
+
"templates": ["product"]
|
|
117
|
+
}
|
|
104
118
|
}
|
|
105
119
|
{% endschema %}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
runwell-bundle-system: runwell-bundle-quantity-builder.liquid.
|
|
3
|
+
|
|
4
|
+
Mode A bundle PDP surface. Renders a product image gallery beside a
|
|
5
|
+
quantity-tier picker, sale-prefix headline, FOMO countdown + scarcity,
|
|
6
|
+
rating row, trust badges row, and an ATC that can append a free gift
|
|
7
|
+
product when the selected tier opts into one.
|
|
8
|
+
|
|
9
|
+
Everything bundle-specific is read from `product.metafields.runwell.*`:
|
|
10
|
+
bundle_quantity_tiers [{qty, discount_pct, label?, popular?,
|
|
11
|
+
free_shipping?, free_gift?}]
|
|
12
|
+
bundle_sale_prefix "Spring Sculpting Sale"
|
|
13
|
+
bundle_fomo_mode none | discount | scarcity | both
|
|
14
|
+
bundle_fomo_cycle_days number (rolling deadline cycle)
|
|
15
|
+
bundle_fomo_stock_count number
|
|
16
|
+
bundle_rating_score "4.8/5"
|
|
17
|
+
bundle_rating_count "2,400+"
|
|
18
|
+
bundle_free_gift_handle product handle (real $0 product); the section
|
|
19
|
+
adds a hidden form input + the ATC JS appends
|
|
20
|
+
it as a second /cart/add line when a free_gift
|
|
21
|
+
tier is selected.
|
|
22
|
+
|
|
23
|
+
Section settings cover what's tenant/page level (color scheme, padding,
|
|
24
|
+
trust badge copy, toggles). Per-tier data is metafield-driven so admin
|
|
25
|
+
changes flow without re-deploying the theme.
|
|
26
|
+
|
|
27
|
+
Replaces the legacy modules/bundle-builder/sections/runwell-bundle-builder
|
|
28
|
+
section. CSS class prefix kept stable (.runwell-bundle-quantity-builder)
|
|
29
|
+
with the same visual structure to preserve tenant calibration.
|
|
30
|
+
{%- endcomment -%}
|
|
31
|
+
|
|
32
|
+
{{ 'runwell-bundle-quantity-builder.css' | asset_url | stylesheet_tag }}
|
|
33
|
+
|
|
34
|
+
{%- assign product = section.settings.product -%}
|
|
35
|
+
{%- if product == blank -%}
|
|
36
|
+
<div class="runwell-bundle-quantity-builder runwell-bundle-quantity-builder--empty page-width">
|
|
37
|
+
<p>Select a product in the section editor to render the bundle builder.</p>
|
|
38
|
+
</div>
|
|
39
|
+
{%- else -%}
|
|
40
|
+
|
|
41
|
+
{%- assign mf = product.metafields.runwell -%}
|
|
42
|
+
{%- assign tiers = mf.bundle_quantity_tiers.value -%}
|
|
43
|
+
{%- assign sale_prefix = mf.bundle_sale_prefix.value | default: section.settings.sale_prefix -%}
|
|
44
|
+
{%- assign fomo_mode = mf.bundle_fomo_mode.value | default: 'none' -%}
|
|
45
|
+
{%- assign fomo_cycle_days = mf.bundle_fomo_cycle_days.value | default: 30 -%}
|
|
46
|
+
{%- assign fomo_stock_count = mf.bundle_fomo_stock_count.value | default: 0 -%}
|
|
47
|
+
{%- assign rating_score = mf.bundle_rating_score.value -%}
|
|
48
|
+
{%- assign rating_count = mf.bundle_rating_count.value -%}
|
|
49
|
+
{%- assign free_gift_handle = mf.bundle_free_gift_handle.value -%}
|
|
50
|
+
{%- assign free_gift_product = blank -%}
|
|
51
|
+
{%- if free_gift_handle != blank -%}
|
|
52
|
+
{%- assign free_gift_product = all_products[free_gift_handle] -%}
|
|
53
|
+
{%- endif -%}
|
|
54
|
+
|
|
55
|
+
<section
|
|
56
|
+
class="runwell-bundle-quantity-builder color-{{ section.settings.color_scheme }}"
|
|
57
|
+
style="padding-top: {{ section.settings.padding_top }}px; padding-bottom: {{ section.settings.padding_bottom }}px;"
|
|
58
|
+
data-runwell-bundle-quantity-builder
|
|
59
|
+
data-product-handle="{{ product.handle }}"
|
|
60
|
+
{%- if free_gift_product != blank %} data-free-gift-variant-id="{{ free_gift_product.selected_or_first_available_variant.id }}"{%- endif -%}
|
|
61
|
+
>
|
|
62
|
+
<div class="page-width">
|
|
63
|
+
<div class="runwell-bundle-quantity-builder__grid">
|
|
64
|
+
|
|
65
|
+
{%- comment -%} Gallery: prefers product.media; falls back to featured_image. {%- endcomment -%}
|
|
66
|
+
<div class="runwell-bundle-quantity-builder__media">
|
|
67
|
+
{%- assign media_items = product.media -%}
|
|
68
|
+
{%- if media_items.size > 0 -%}
|
|
69
|
+
<div class="runwell-bundle-quantity-builder__slideshow" data-slideshow>
|
|
70
|
+
{%- for media in media_items -%}
|
|
71
|
+
<div
|
|
72
|
+
class="runwell-bundle-quantity-builder__slide {% if forloop.first %}runwell-bundle-quantity-builder__slide--active{% endif %}"
|
|
73
|
+
data-slide="{{ forloop.index0 }}"
|
|
74
|
+
>
|
|
75
|
+
{{ media | image_url: width: 1200 | image_tag:
|
|
76
|
+
class: 'runwell-bundle-quantity-builder__img',
|
|
77
|
+
loading: forloop.first | default: 'eager',
|
|
78
|
+
sizes: '(min-width: 990px) 50vw, 100vw',
|
|
79
|
+
alt: media.alt | default: product.title
|
|
80
|
+
}}
|
|
81
|
+
</div>
|
|
82
|
+
{%- endfor -%}
|
|
83
|
+
</div>
|
|
84
|
+
{%- if media_items.size > 1 -%}
|
|
85
|
+
<div class="runwell-bundle-quantity-builder__thumbnails" data-thumbnails>
|
|
86
|
+
{%- for media in media_items -%}
|
|
87
|
+
<button
|
|
88
|
+
class="runwell-bundle-quantity-builder__thumb {% if forloop.first %}runwell-bundle-quantity-builder__thumb--active{% endif %}"
|
|
89
|
+
type="button"
|
|
90
|
+
data-thumb="{{ forloop.index0 }}"
|
|
91
|
+
aria-label="View image {{ forloop.index }}"
|
|
92
|
+
>
|
|
93
|
+
{{ media | image_url: width: 160, height: 160, crop: 'center' | image_tag: loading: 'lazy', alt: '' }}
|
|
94
|
+
</button>
|
|
95
|
+
{%- endfor -%}
|
|
96
|
+
</div>
|
|
97
|
+
{%- endif -%}
|
|
98
|
+
{%- elsif product.featured_image -%}
|
|
99
|
+
<div class="runwell-bundle-quantity-builder__slideshow">
|
|
100
|
+
<div class="runwell-bundle-quantity-builder__slide runwell-bundle-quantity-builder__slide--active">
|
|
101
|
+
{{ product.featured_image | image_url: width: 1200 | image_tag:
|
|
102
|
+
class: 'runwell-bundle-quantity-builder__img',
|
|
103
|
+
loading: 'eager',
|
|
104
|
+
alt: product.title
|
|
105
|
+
}}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
{%- else -%}
|
|
109
|
+
<div class="runwell-bundle-quantity-builder__slideshow">
|
|
110
|
+
<div class="runwell-bundle-quantity-builder__slide runwell-bundle-quantity-builder__slide--active">
|
|
111
|
+
<div class="runwell-bundle-quantity-builder__img runwell-bundle-quantity-builder__img--placeholder" aria-label="No image"></div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
{%- endif -%}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="runwell-bundle-quantity-builder__info">
|
|
118
|
+
|
|
119
|
+
{%- if section.settings.show_rating and rating_score != blank -%}
|
|
120
|
+
<div class="runwell-bundle-quantity-builder__rating">
|
|
121
|
+
<span class="runwell-bundle-quantity-builder__stars" aria-hidden="true">★★★★★</span>
|
|
122
|
+
<span class="runwell-bundle-quantity-builder__rating-text">
|
|
123
|
+
Rated {{ rating_score }}{%- if rating_count != blank %} ({{ rating_count }} Reviews){%- endif -%}
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
{%- endif -%}
|
|
127
|
+
|
|
128
|
+
<h2 class="runwell-bundle-quantity-builder__title h1">{{ product.title }}</h2>
|
|
129
|
+
|
|
130
|
+
{%- if fomo_mode != 'none' or sale_prefix != blank -%}
|
|
131
|
+
{%- assign show_scarcity = false -%}
|
|
132
|
+
{%- if fomo_mode == 'scarcity' or fomo_mode == 'both' -%}
|
|
133
|
+
{%- if fomo_stock_count > 0 -%}{%- assign show_scarcity = true -%}{%- endif -%}
|
|
134
|
+
{%- endif -%}
|
|
135
|
+
|
|
136
|
+
{%- assign sale_text = sale_prefix -%}
|
|
137
|
+
{%- if fomo_mode == 'discount' or fomo_mode == 'both' -%}
|
|
138
|
+
{%- assign now_epoch = 'now' | date: '%s' | plus: 0 -%}
|
|
139
|
+
{%- assign cycle_seconds = fomo_cycle_days | times: 86400 -%}
|
|
140
|
+
{%- assign cycle_position = now_epoch | modulo: cycle_seconds -%}
|
|
141
|
+
{%- assign remaining_seconds = cycle_seconds | minus: cycle_position -%}
|
|
142
|
+
{%- assign end_epoch = now_epoch | plus: remaining_seconds -%}
|
|
143
|
+
{%- assign end_date = end_epoch | date: '%B %e' | strip -%}
|
|
144
|
+
{%- if sale_prefix != blank -%}
|
|
145
|
+
{%- capture sale_text -%}{{ sale_prefix }}: Ends {{ end_date }}{%- endcapture -%}
|
|
146
|
+
{%- else -%}
|
|
147
|
+
{%- capture sale_text -%}Ends {{ end_date }}{%- endcapture -%}
|
|
148
|
+
{%- endif -%}
|
|
149
|
+
{%- endif -%}
|
|
150
|
+
|
|
151
|
+
{%- if sale_text != blank -%}
|
|
152
|
+
<div class="runwell-bundle-quantity-builder__sale-heading">
|
|
153
|
+
<span class="runwell-bundle-quantity-builder__sale-line"></span>
|
|
154
|
+
<span class="runwell-bundle-quantity-builder__sale-text">{{ sale_text }}</span>
|
|
155
|
+
<span class="runwell-bundle-quantity-builder__sale-line"></span>
|
|
156
|
+
</div>
|
|
157
|
+
{%- endif -%}
|
|
158
|
+
|
|
159
|
+
{%- if show_scarcity -%}
|
|
160
|
+
<div class="runwell-bundle-quantity-builder__scarcity">
|
|
161
|
+
<span class="runwell-bundle-quantity-builder__scarcity-dot"></span>
|
|
162
|
+
Only {{ fomo_stock_count }} left in stock
|
|
163
|
+
</div>
|
|
164
|
+
{%- endif -%}
|
|
165
|
+
{%- endif -%}
|
|
166
|
+
|
|
167
|
+
{%- if tiers and tiers.size > 0 -%}
|
|
168
|
+
{%- assign has_popular = false -%}
|
|
169
|
+
{%- for t in tiers -%}
|
|
170
|
+
{%- if t.popular -%}{%- assign has_popular = true -%}{%- endif -%}
|
|
171
|
+
{%- endfor -%}
|
|
172
|
+
|
|
173
|
+
<form
|
|
174
|
+
action="{{ routes.cart_add_url }}"
|
|
175
|
+
method="post"
|
|
176
|
+
enctype="multipart/form-data"
|
|
177
|
+
class="runwell-bundle-quantity-builder__form"
|
|
178
|
+
data-runwell-bundle-form
|
|
179
|
+
>
|
|
180
|
+
<input type="hidden" name="form_type" value="product">
|
|
181
|
+
<input type="hidden" name="utf8" value="✓">
|
|
182
|
+
<input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
|
|
183
|
+
|
|
184
|
+
<fieldset class="runwell-bundle-quantity-builder__options" aria-label="Choose quantity">
|
|
185
|
+
<legend class="visually-hidden">Choose quantity</legend>
|
|
186
|
+
{%- for t in tiers -%}
|
|
187
|
+
{%- assign is_default = false -%}
|
|
188
|
+
{%- if has_popular and t.popular -%}
|
|
189
|
+
{%- assign is_default = true -%}
|
|
190
|
+
{%- elsif has_popular == false and forloop.first -%}
|
|
191
|
+
{%- assign is_default = true -%}
|
|
192
|
+
{%- endif -%}
|
|
193
|
+
|
|
194
|
+
{%- assign tier_label = t.label -%}
|
|
195
|
+
{%- if tier_label == blank -%}
|
|
196
|
+
{%- capture tier_label -%}{{ t.qty }}x {{ product.title }}{%- endcapture -%}
|
|
197
|
+
{%- endif -%}
|
|
198
|
+
|
|
199
|
+
{%- assign discount_amount = product.price | times: t.discount_pct | divided_by: 100 -%}
|
|
200
|
+
{%- assign per_unit_price = product.price | minus: discount_amount -%}
|
|
201
|
+
{%- assign total_price = per_unit_price | times: t.qty -%}
|
|
202
|
+
{%- assign full_price = product.price | times: t.qty -%}
|
|
203
|
+
{%- assign savings = full_price | minus: total_price -%}
|
|
204
|
+
|
|
205
|
+
<label
|
|
206
|
+
class="runwell-bundle-quantity-builder__option {% if t.popular %}runwell-bundle-quantity-builder__option--popular{% endif %} {% if is_default %}runwell-bundle-quantity-builder__option--selected{% endif %}"
|
|
207
|
+
data-tier-qty="{{ t.qty }}"
|
|
208
|
+
data-tier-free-gift="{{ t.free_gift | default: false }}"
|
|
209
|
+
>
|
|
210
|
+
{%- if t.popular -%}
|
|
211
|
+
<span class="runwell-bundle-quantity-builder__popular-badge">MOST POPULAR</span>
|
|
212
|
+
{%- endif -%}
|
|
213
|
+
<input
|
|
214
|
+
type="radio"
|
|
215
|
+
name="quantity"
|
|
216
|
+
value="{{ t.qty }}"
|
|
217
|
+
class="runwell-bundle-quantity-builder__option-input"
|
|
218
|
+
data-runwell-tier-radio
|
|
219
|
+
{% if is_default %}checked{% endif %}
|
|
220
|
+
>
|
|
221
|
+
<span class="runwell-bundle-quantity-builder__option-radio" aria-hidden="true"></span>
|
|
222
|
+
<span class="runwell-bundle-quantity-builder__option-content">
|
|
223
|
+
<span class="runwell-bundle-quantity-builder__option-header">
|
|
224
|
+
<span class="runwell-bundle-quantity-builder__option-title">{{ tier_label }}</span>
|
|
225
|
+
<span class="runwell-bundle-quantity-builder__option-pricing">
|
|
226
|
+
<span class="runwell-bundle-quantity-builder__option-price">{{ total_price | money_without_trailing_zeros }}</span>
|
|
227
|
+
{%- if savings > 0 -%}
|
|
228
|
+
<span class="runwell-bundle-quantity-builder__option-compare">{{ full_price | money_without_trailing_zeros }}</span>
|
|
229
|
+
{%- endif -%}
|
|
230
|
+
</span>
|
|
231
|
+
</span>
|
|
232
|
+
{%- if savings > 0 or t.free_shipping -%}
|
|
233
|
+
<span class="runwell-bundle-quantity-builder__option-badges">
|
|
234
|
+
{%- if savings > 0 -%}
|
|
235
|
+
<span class="runwell-bundle-quantity-builder__badge runwell-bundle-quantity-builder__badge--save">SAVE {{ savings | money_without_trailing_zeros }}</span>
|
|
236
|
+
{%- endif -%}
|
|
237
|
+
{%- if t.free_shipping -%}
|
|
238
|
+
<span class="runwell-bundle-quantity-builder__badge runwell-bundle-quantity-builder__badge--shipping">FREE SHIPPING</span>
|
|
239
|
+
{%- endif -%}
|
|
240
|
+
</span>
|
|
241
|
+
{%- endif -%}
|
|
242
|
+
{%- if t.free_gift and free_gift_product != blank -%}
|
|
243
|
+
<span class="runwell-bundle-quantity-builder__free-gift">
|
|
244
|
+
<span class="runwell-bundle-quantity-builder__gift-icon" aria-hidden="true">🎁</span>
|
|
245
|
+
<span>+ Free {{ free_gift_product.title }} for a limited time.</span>
|
|
246
|
+
</span>
|
|
247
|
+
{%- endif -%}
|
|
248
|
+
</span>
|
|
249
|
+
</label>
|
|
250
|
+
{%- endfor -%}
|
|
251
|
+
</fieldset>
|
|
252
|
+
|
|
253
|
+
<button
|
|
254
|
+
type="submit"
|
|
255
|
+
class="runwell-bundle-quantity-builder__atc button button--full-width"
|
|
256
|
+
data-runwell-bundle-atc
|
|
257
|
+
{% if product.selected_or_first_available_variant.available == false %}disabled{% endif %}
|
|
258
|
+
>
|
|
259
|
+
<span class="runwell-bundle-quantity-builder__atc-text">
|
|
260
|
+
{% if product.selected_or_first_available_variant.available %}ADD TO CART{% else %}SOLD OUT{% endif %}
|
|
261
|
+
</span>
|
|
262
|
+
<span class="runwell-bundle-quantity-builder__atc-loading" style="display:none;" aria-hidden="true">Adding…</span>
|
|
263
|
+
</button>
|
|
264
|
+
</form>
|
|
265
|
+
{%- else -%}
|
|
266
|
+
<p class="runwell-bundle-quantity-builder__empty">
|
|
267
|
+
No quantity tiers configured. Set
|
|
268
|
+
<code>runwell.bundle_quantity_tiers</code> on this product
|
|
269
|
+
to enable the bundle builder.
|
|
270
|
+
</p>
|
|
271
|
+
{%- endif -%}
|
|
272
|
+
|
|
273
|
+
{%- if section.settings.show_trust_badges -%}
|
|
274
|
+
<div class="runwell-bundle-quantity-builder__trust">
|
|
275
|
+
{%- if section.settings.trust_1 != blank -%}<span>✅ {{ section.settings.trust_1 }}</span>{%- endif -%}
|
|
276
|
+
{%- if section.settings.trust_2 != blank -%}<span>✅ {{ section.settings.trust_2 }}</span>{%- endif -%}
|
|
277
|
+
{%- if section.settings.trust_3 != blank -%}<span>✅ {{ section.settings.trust_3 }}</span>{%- endif -%}
|
|
278
|
+
</div>
|
|
279
|
+
{%- endif -%}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</section>
|
|
284
|
+
|
|
285
|
+
<script src="{{ 'runwell-bundle-system.js' | asset_url }}" defer="defer"></script>
|
|
286
|
+
{%- endif -%}
|
|
287
|
+
|
|
288
|
+
{% schema %}
|
|
289
|
+
{
|
|
290
|
+
"name": "Bundle quantity builder",
|
|
291
|
+
"tag": "section",
|
|
292
|
+
"class": "section-runwell-bundle-quantity-builder",
|
|
293
|
+
"settings": [
|
|
294
|
+
{ "type": "header", "content": "Source product" },
|
|
295
|
+
{ "type": "product", "id": "product", "label": "Product", "info": "The bundle reads runwell.bundle_quantity_tiers + sale_prefix + rating + free gift handle from this product's metafields." },
|
|
296
|
+
|
|
297
|
+
{ "type": "header", "content": "Sale prefix fallback" },
|
|
298
|
+
{ "type": "text", "id": "sale_prefix", "label": "Sale prefix fallback", "info": "Used only when the product has no runwell.bundle_sale_prefix metafield set." },
|
|
299
|
+
|
|
300
|
+
{ "type": "header", "content": "Rating display" },
|
|
301
|
+
{ "type": "checkbox", "id": "show_rating", "label": "Show star rating", "default": true },
|
|
302
|
+
|
|
303
|
+
{ "type": "header", "content": "Trust badges" },
|
|
304
|
+
{ "type": "checkbox", "id": "show_trust_badges", "label": "Show trust badges", "default": true },
|
|
305
|
+
{ "type": "text", "id": "trust_1", "label": "Badge 1", "default": "90-Day Guarantee" },
|
|
306
|
+
{ "type": "text", "id": "trust_2", "label": "Badge 2", "default": "Easy Returns" },
|
|
307
|
+
{ "type": "text", "id": "trust_3", "label": "Badge 3", "default": "Free Shipping" },
|
|
308
|
+
|
|
309
|
+
{ "type": "header", "content": "Layout" },
|
|
310
|
+
{ "type": "color_scheme", "id": "color_scheme", "label": "Color scheme", "default": "scheme-1" },
|
|
311
|
+
{ "type": "range", "id": "padding_top", "label": "Top padding", "min": 0, "max": 160, "step": 4, "default": 40, "unit": "px" },
|
|
312
|
+
{ "type": "range", "id": "padding_bottom", "label": "Bottom padding", "min": 0, "max": 160, "step": 4, "default": 40, "unit": "px" }
|
|
313
|
+
],
|
|
314
|
+
"presets": [
|
|
315
|
+
{ "name": "Bundle: quantity builder", "category": "Runwell" }
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
{% endschema %}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
runwell-bundle-system: runwell-bundle-byob-picker-accordion.liquid.
|
|
3
|
+
Mode C, accordion layout. Candidates grouped by bundle_byob_categories.
|
|
4
|
+
One category open at a time. Total picks across all categories
|
|
5
|
+
enforced via JS.
|
|
6
|
+
|
|
7
|
+
Inputs:
|
|
8
|
+
bundle_byob_candidates, bundle_byob_min_picks, bundle_byob_max_picks,
|
|
9
|
+
bundle_byob_categories ([{label, handles}]),
|
|
10
|
+
bundle_byob_required_handles, bundle_pricing_model, bundle_pricing_value.
|
|
11
|
+
{%- endcomment -%}
|
|
12
|
+
|
|
13
|
+
{%- if bundle_byob_categories and bundle_byob_categories.size > 0 -%}
|
|
14
|
+
{%- assign required_handles = '' -%}
|
|
15
|
+
{%- if bundle_byob_required_handles -%}
|
|
16
|
+
{%- capture required_handles -%}
|
|
17
|
+
{%- for r in bundle_byob_required_handles -%}{{ r.handle }},{%- endfor -%}
|
|
18
|
+
{%- endcapture -%}
|
|
19
|
+
{%- endif -%}
|
|
20
|
+
|
|
21
|
+
<div
|
|
22
|
+
class="runwell-bundle-system__byob-picker runwell-bundle-system__byob-picker--accordion"
|
|
23
|
+
data-runwell-byob-picker
|
|
24
|
+
data-byob-layout="accordion"
|
|
25
|
+
data-byob-min="{{ bundle_byob_min_picks }}"
|
|
26
|
+
data-byob-max="{{ bundle_byob_max_picks }}"
|
|
27
|
+
data-byob-pricing-model="{{ bundle_pricing_model }}"
|
|
28
|
+
data-byob-pricing-value="{{ bundle_pricing_value | json | escape }}"
|
|
29
|
+
>
|
|
30
|
+
{%- for category in bundle_byob_categories -%}
|
|
31
|
+
<details class="runwell-bundle-system__byob-accordion" {% if forloop.first %}open{% endif %}>
|
|
32
|
+
<summary class="runwell-bundle-system__byob-accordion-summary">
|
|
33
|
+
<span class="runwell-bundle-system__byob-accordion-label">{{ category.label }}</span>
|
|
34
|
+
<span class="runwell-bundle-system__byob-accordion-count" data-runwell-byob-cat-count>0</span>
|
|
35
|
+
</summary>
|
|
36
|
+
<div class="runwell-bundle-system__byob-accordion-body">
|
|
37
|
+
{%- for handle in category.handles -%}
|
|
38
|
+
{%- assign candidate = all_products[handle] -%}
|
|
39
|
+
{%- if candidate != blank -%}
|
|
40
|
+
{%- assign is_required = false -%}
|
|
41
|
+
{%- if required_handles contains handle -%}{%- assign is_required = true -%}{%- endif -%}
|
|
42
|
+
<label
|
|
43
|
+
class="runwell-bundle-system__byob-candidate{% if is_required %} runwell-bundle-system__byob-candidate--required{% endif %}"
|
|
44
|
+
data-handle="{{ candidate.handle }}"
|
|
45
|
+
data-price="{{ candidate.price }}"
|
|
46
|
+
data-variant-id="{{ candidate.selected_or_first_available_variant.id }}"
|
|
47
|
+
data-category="{{ category.label | escape }}"
|
|
48
|
+
>
|
|
49
|
+
<input
|
|
50
|
+
type="checkbox"
|
|
51
|
+
class="runwell-bundle-system__byob-input"
|
|
52
|
+
name="byob-pick"
|
|
53
|
+
value="{{ candidate.handle }}"
|
|
54
|
+
{% if is_required %}checked disabled{% endif %}
|
|
55
|
+
{% unless candidate.available %}disabled{% endunless %}
|
|
56
|
+
>
|
|
57
|
+
<span class="runwell-bundle-system__byob-candidate-media">
|
|
58
|
+
{%- if candidate.featured_image -%}
|
|
59
|
+
{{ candidate.featured_image | image_url: width: 240 | image_tag:
|
|
60
|
+
width: 120,
|
|
61
|
+
height: 120,
|
|
62
|
+
loading: 'lazy',
|
|
63
|
+
class: 'runwell-bundle-system__byob-candidate-thumb',
|
|
64
|
+
alt: candidate.title
|
|
65
|
+
}}
|
|
66
|
+
{%- endif -%}
|
|
67
|
+
</span>
|
|
68
|
+
<span class="runwell-bundle-system__byob-candidate-body">
|
|
69
|
+
<span class="runwell-bundle-system__byob-candidate-title">{{ candidate.title }}</span>
|
|
70
|
+
<span class="runwell-bundle-system__byob-candidate-price">{{ candidate.price | money }}</span>
|
|
71
|
+
{%- if is_required -%}
|
|
72
|
+
<span class="runwell-bundle-system__byob-candidate-badge">Always included</span>
|
|
73
|
+
{%- endif -%}
|
|
74
|
+
</span>
|
|
75
|
+
</label>
|
|
76
|
+
{%- endif -%}
|
|
77
|
+
{%- endfor -%}
|
|
78
|
+
</div>
|
|
79
|
+
</details>
|
|
80
|
+
{%- endfor -%}
|
|
81
|
+
</div>
|
|
82
|
+
{%- else -%}
|
|
83
|
+
<p class="runwell-bundle-system__empty">Accordion layout requires bundle_byob_categories. Falling back to grid layout via runwell-bundle-byob-picker.liquid.</p>
|
|
84
|
+
{%- endif -%}
|