@runwell/shopify-toolkit 0.23.0 → 0.24.1
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/modules/runwell-bundle-system/admin-metafields.json +4 -1
- package/modules/runwell-bundle-system/assets/runwell-bundle-quantity-builder.css +383 -0
- package/modules/runwell-bundle-system/assets/runwell-bundle-system.js +202 -0
- package/modules/runwell-bundle-system/module.json +6 -3
- 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 +3 -1
- package/modules/runwell-bundle-system/sections/runwell-bundle-quantity-builder.liquid +328 -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/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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{ "key": "bundle_pricing_model", "name": "Bundle pricing model", "type": "single_line_text_field", "validations": [{ "name": "choices", "value": "[\"tier_quantity\",\"fixed_price\",\"fixed_bundle_price\",\"percent_off_subtotal\",\"dollar_off_subtotal\"]" }] },
|
|
10
10
|
{ "key": "bundle_pricing_value", "name": "Bundle pricing value (JSON)", "type": "json", "description": "Shape varies by pricing_model. See spec.md section 2.2." },
|
|
11
11
|
{ "key": "bundle_components", "name": "Bundle components (JSON)", "type": "json", "description": "Required for multi_product / mix_match. Array of {product_handle, qty}." },
|
|
12
|
-
{ "key": "bundle_quantity_tiers", "name": "Bundle quantity tiers (JSON)", "type": "json", "description": "Required for quantity_tiers. Array of {qty, discount_pct}.
|
|
12
|
+
{ "key": "bundle_quantity_tiers", "name": "Bundle quantity tiers (JSON)", "type": "json", "description": "Required for quantity_tiers. Array of {qty, discount_pct, label?, popular?, free_shipping?, free_gift?}. label is the visible row label (e.g. '2x Sculpting Brush'); falls back to '{qty}x {product.title}' when absent. popular adds the MOST POPULAR badge. free_shipping shows the FREE SHIPPING tag on that tier. free_gift means buying this tier appends the bundle_free_gift_handle product as a $0 line item via ATC." },
|
|
13
13
|
{ "key": "bundle_show_in_catalog", "name": "Show in main catalog", "type": "boolean", "default": true },
|
|
14
14
|
{ "key": "bundle_surfaces_enabled", "name": "Surfaces enabled (JSON)", "type": "json", "description": "Per-bundle surface allowlist. Array of 1..6. Absent = all enabled at tenant level." },
|
|
15
15
|
{ "key": "bundle_copy", "name": "Per-surface copy (JSON)", "type": "json", "description": "Per-surface eyebrow/heading/cta overrides." },
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
{ "key": "bundle_cross_supplier", "name": "Cross-supplier", "type": "boolean", "default": false },
|
|
22
22
|
{ "key": "bundle_supplier_count", "name": "Supplier count", "type": "number_integer" },
|
|
23
23
|
{ "key": "bundle_savings_pct", "name": "Computed savings percent", "type": "number_decimal", "description": "Precomputed for fast banner / card render. Recomputed by runwell-shopify rebuild-bundle-index." },
|
|
24
|
+
{ "key": "bundle_sale_prefix", "name": "Sale prefix (Mode A)", "type": "single_line_text_field", "description": "Subheading copy displayed under the product title on Mode A quantity-tier surfaces. Example: 'Spring Sculpting Sale: Ends June 5'." },
|
|
25
|
+
{ "key": "bundle_rating_score", "name": "Rating score display", "type": "single_line_text_field", "description": "Rating string shown above the title (e.g. '4.8/5'). Display-only; not wired to a review provider for v1." },
|
|
26
|
+
{ "key": "bundle_rating_count", "name": "Rating count display", "type": "single_line_text_field", "description": "Review count string shown next to the rating score (e.g. '2,400+ Reviews')." },
|
|
24
27
|
{ "key": "bundle_byob_candidates", "name": "BYOB candidates", "type": "list.product_reference", "description": "Mode C only. Pool of candidate products the customer can pick from.", "v1_5": true },
|
|
25
28
|
{ "key": "bundle_byob_min_picks", "name": "BYOB min picks", "type": "number_integer", "description": "Mode C only. Minimum required selections before ATC enables.", "v1_5": true },
|
|
26
29
|
{ "key": "bundle_byob_max_picks", "name": "BYOB max picks", "type": "number_integer", "description": "Mode C only. Maximum allowed selections.", "v1_5": true },
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
.runwell-bundle-quantity-builder__grid {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-columns: 1fr 1fr;
|
|
4
|
+
gap: 2.4rem;
|
|
5
|
+
align-items: start;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Slideshow Gallery */
|
|
9
|
+
.runwell-bundle-quantity-builder__slideshow {
|
|
10
|
+
position: relative;
|
|
11
|
+
border-radius: 12px;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
background: #E8D5C4;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.runwell-bundle-quantity-builder__slide {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.runwell-bundle-quantity-builder__slide--active {
|
|
21
|
+
display: block;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.runwell-bundle-quantity-builder__img {
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: auto;
|
|
27
|
+
display: block;
|
|
28
|
+
aspect-ratio: 1 / 1;
|
|
29
|
+
object-fit: cover;
|
|
30
|
+
max-height: calc(100vh - 260px);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Thumbnail Strip - ALWAYS visible including mobile */
|
|
34
|
+
.runwell-bundle-quantity-builder__thumbnails {
|
|
35
|
+
display: flex;
|
|
36
|
+
gap: 8px;
|
|
37
|
+
margin-top: 12px;
|
|
38
|
+
overflow-x: auto;
|
|
39
|
+
scrollbar-width: thin;
|
|
40
|
+
scrollbar-color: #EADFD4 transparent;
|
|
41
|
+
padding-bottom: 4px;
|
|
42
|
+
-webkit-overflow-scrolling: touch;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.runwell-bundle-quantity-builder__thumbnails::-webkit-scrollbar {
|
|
46
|
+
height: 4px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.runwell-bundle-quantity-builder__thumbnails::-webkit-scrollbar-thumb {
|
|
50
|
+
background: #EADFD4;
|
|
51
|
+
border-radius: 2px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.runwell-bundle-quantity-builder__thumb {
|
|
55
|
+
flex: 0 0 72px;
|
|
56
|
+
height: 72px;
|
|
57
|
+
border-radius: 8px;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
border: 2px solid transparent;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
padding: 0;
|
|
62
|
+
background: #E8D5C4;
|
|
63
|
+
transition: border-color 0.2s, opacity 0.2s;
|
|
64
|
+
opacity: 0.7;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.runwell-bundle-quantity-builder__thumb--active {
|
|
68
|
+
border-color: #3F5B4C;
|
|
69
|
+
opacity: 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.runwell-bundle-quantity-builder__thumb:hover {
|
|
73
|
+
opacity: 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.runwell-bundle-quantity-builder__thumb img {
|
|
77
|
+
width: 100%;
|
|
78
|
+
height: 100%;
|
|
79
|
+
object-fit: cover;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Rating */
|
|
83
|
+
.runwell-bundle-quantity-builder__rating {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: 8px;
|
|
87
|
+
margin-bottom: 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.runwell-bundle-quantity-builder__stars {
|
|
91
|
+
color: #E8B931;
|
|
92
|
+
font-size: calc(var(--font-body-scale) * 1.6rem);
|
|
93
|
+
letter-spacing: 1px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.runwell-bundle-quantity-builder__rating-text {
|
|
97
|
+
font-size: calc(var(--font-body-scale) * 1.4rem);
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
font-style: italic;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Title */
|
|
103
|
+
.runwell-bundle-quantity-builder__title {
|
|
104
|
+
margin-top: 0.4rem;
|
|
105
|
+
margin-bottom: 1rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Sale heading */
|
|
109
|
+
.runwell-bundle-quantity-builder__sale-heading {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
gap: 1.2rem;
|
|
113
|
+
margin-bottom: 1.2rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.runwell-bundle-quantity-builder__sale-line {
|
|
117
|
+
flex: 1;
|
|
118
|
+
height: 1px;
|
|
119
|
+
background: #2A2622;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.runwell-bundle-quantity-builder__sale-text {
|
|
123
|
+
font-weight: 700;
|
|
124
|
+
font-size: calc(var(--font-body-scale) * 1.5rem);
|
|
125
|
+
white-space: nowrap;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Scarcity indicator */
|
|
129
|
+
.runwell-bundle-quantity-builder__scarcity {
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
gap: 6px;
|
|
134
|
+
font-size: calc(var(--font-body-scale) * 1.3rem);
|
|
135
|
+
font-weight: 500;
|
|
136
|
+
color: #b45309;
|
|
137
|
+
margin-bottom: 1rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.runwell-bundle-quantity-builder__scarcity-dot {
|
|
141
|
+
width: 8px;
|
|
142
|
+
height: 8px;
|
|
143
|
+
border-radius: 50%;
|
|
144
|
+
background: #b45309;
|
|
145
|
+
animation: scarcity-pulse 2s ease-in-out infinite;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@keyframes scarcity-pulse {
|
|
149
|
+
0%, 100% { opacity: 1; }
|
|
150
|
+
50% { opacity: 0.4; }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Options */
|
|
154
|
+
.runwell-bundle-quantity-builder__options {
|
|
155
|
+
display: flex;
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
gap: 10px;
|
|
158
|
+
margin-bottom: 1.2rem;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.runwell-bundle-quantity-builder__option {
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: flex-start;
|
|
164
|
+
gap: 12px;
|
|
165
|
+
padding: 14px 18px;
|
|
166
|
+
border: 2px solid #EADFD4;
|
|
167
|
+
border-radius: 10px;
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
170
|
+
position: relative;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.runwell-bundle-quantity-builder__option:hover {
|
|
174
|
+
border-color: #3F5B4C;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.runwell-bundle-quantity-builder__option--selected {
|
|
178
|
+
border-color: #2A2622;
|
|
179
|
+
box-shadow: 0 0 0 1px #2A2622;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.runwell-bundle-quantity-builder__option--popular {
|
|
183
|
+
border-color: #3F5B4C;
|
|
184
|
+
position: relative;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.runwell-bundle-quantity-builder__popular-badge {
|
|
188
|
+
position: absolute;
|
|
189
|
+
top: -10px;
|
|
190
|
+
right: 16px;
|
|
191
|
+
background: #3F5B4C;
|
|
192
|
+
color: #fff;
|
|
193
|
+
font-size: var(--runwell-body-size);
|
|
194
|
+
font-weight: 700;
|
|
195
|
+
padding: 2px 10px;
|
|
196
|
+
border-radius: 4px;
|
|
197
|
+
letter-spacing: 0.06em;
|
|
198
|
+
text-transform: uppercase;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.runwell-bundle-quantity-builder__option input {
|
|
202
|
+
position: absolute;
|
|
203
|
+
opacity: 0;
|
|
204
|
+
pointer-events: none;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.runwell-bundle-quantity-builder__option-radio {
|
|
208
|
+
width: 22px;
|
|
209
|
+
height: 22px;
|
|
210
|
+
border-radius: 50%;
|
|
211
|
+
border: 2px solid #EADFD4;
|
|
212
|
+
flex-shrink: 0;
|
|
213
|
+
margin-top: 2px;
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
justify-content: center;
|
|
217
|
+
transition: border-color 0.2s;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.runwell-bundle-quantity-builder__option--selected .runwell-bundle-quantity-builder__option-radio {
|
|
221
|
+
border-color: #2A2622;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.runwell-bundle-quantity-builder__option--selected .runwell-bundle-quantity-builder__option-radio::after {
|
|
225
|
+
content: '';
|
|
226
|
+
width: 12px;
|
|
227
|
+
height: 12px;
|
|
228
|
+
border-radius: 50%;
|
|
229
|
+
background: #2A2622;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.runwell-bundle-quantity-builder__option-content {
|
|
233
|
+
flex: 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.runwell-bundle-quantity-builder__option-header {
|
|
237
|
+
display: flex;
|
|
238
|
+
justify-content: space-between;
|
|
239
|
+
align-items: baseline;
|
|
240
|
+
gap: 12px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.runwell-bundle-quantity-builder__option-title {
|
|
244
|
+
font-weight: 700;
|
|
245
|
+
font-size: calc(var(--font-heading-scale) * 1.5rem);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.runwell-bundle-quantity-builder__option-pricing {
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: baseline;
|
|
251
|
+
gap: 8px;
|
|
252
|
+
flex-shrink: 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.runwell-bundle-quantity-builder__option-price {
|
|
256
|
+
font-weight: 700;
|
|
257
|
+
font-size: calc(var(--font-body-scale) * 1.8rem);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.runwell-bundle-quantity-builder__option-compare {
|
|
261
|
+
font-size: calc(var(--font-body-scale) * 1.3rem);
|
|
262
|
+
text-decoration: line-through;
|
|
263
|
+
opacity: 0.5;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.runwell-bundle-quantity-builder__option-badges {
|
|
267
|
+
display: flex;
|
|
268
|
+
gap: 6px;
|
|
269
|
+
margin-top: 6px;
|
|
270
|
+
flex-wrap: wrap;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.runwell-bundle-quantity-builder__badge {
|
|
274
|
+
font-size: calc(var(--font-body-scale) * 1.1rem);
|
|
275
|
+
font-weight: 700;
|
|
276
|
+
padding: 2px 8px;
|
|
277
|
+
border-radius: 4px;
|
|
278
|
+
text-transform: uppercase;
|
|
279
|
+
letter-spacing: 0.03em;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.runwell-bundle-quantity-builder__badge--save {
|
|
283
|
+
background: #3F5B4C;
|
|
284
|
+
color: #fff;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.runwell-bundle-quantity-builder__badge--shipping {
|
|
288
|
+
color: #3F5B4C;
|
|
289
|
+
font-weight: 600;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.runwell-bundle-quantity-builder__free-gift {
|
|
293
|
+
display: flex;
|
|
294
|
+
align-items: center;
|
|
295
|
+
gap: 6px;
|
|
296
|
+
margin-top: 8px;
|
|
297
|
+
padding: 8px 12px;
|
|
298
|
+
background: rgba(63, 91, 76, 0.06);
|
|
299
|
+
border-radius: 6px;
|
|
300
|
+
font-size: calc(var(--font-body-scale) * 1.3rem);
|
|
301
|
+
font-weight: 600;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.runwell-bundle-quantity-builder__gift-icon {
|
|
305
|
+
font-size: calc(var(--font-body-scale) * 1.6rem);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* ATC Button */
|
|
309
|
+
.runwell-bundle-quantity-builder__atc {
|
|
310
|
+
width: 100%;
|
|
311
|
+
padding: 1.6rem;
|
|
312
|
+
font-size: calc(var(--font-body-scale) * 1.6rem);
|
|
313
|
+
font-weight: 700;
|
|
314
|
+
letter-spacing: 0.06em;
|
|
315
|
+
text-transform: uppercase;
|
|
316
|
+
background: #2A2622;
|
|
317
|
+
color: #fff;
|
|
318
|
+
border: none;
|
|
319
|
+
border-radius: 8px;
|
|
320
|
+
cursor: pointer;
|
|
321
|
+
transition: background 0.2s;
|
|
322
|
+
margin-bottom: 1.2rem;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.runwell-bundle-quantity-builder__atc:hover {
|
|
326
|
+
background: #3F5B4C;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* Trust badges */
|
|
330
|
+
.runwell-bundle-quantity-builder__trust {
|
|
331
|
+
display: flex;
|
|
332
|
+
justify-content: center;
|
|
333
|
+
gap: 2rem;
|
|
334
|
+
font-size: calc(var(--font-body-scale) * 1.3rem);
|
|
335
|
+
opacity: 0.7;
|
|
336
|
+
flex-wrap: wrap;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* Sticky ATC on mobile */
|
|
340
|
+
@media screen and (max-width: 989px) {
|
|
341
|
+
.runwell-bundle-quantity-builder__grid {
|
|
342
|
+
grid-template-columns: 1fr;
|
|
343
|
+
gap: 2rem;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.runwell-bundle-quantity-builder__atc-wrap {
|
|
347
|
+
position: sticky;
|
|
348
|
+
bottom: 0;
|
|
349
|
+
z-index: 10;
|
|
350
|
+
background: #fff;
|
|
351
|
+
padding: 12px 0 max(12px, env(safe-area-inset-bottom));
|
|
352
|
+
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
|
|
353
|
+
margin: 0 -1.5rem;
|
|
354
|
+
padding-left: 1.5rem;
|
|
355
|
+
padding-right: 1.5rem;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@media screen and (max-width: 749px) {
|
|
360
|
+
.runwell-bundle-quantity-builder__option-header {
|
|
361
|
+
flex-direction: column;
|
|
362
|
+
gap: 4px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.runwell-bundle-quantity-builder__trust {
|
|
366
|
+
flex-direction: column;
|
|
367
|
+
align-items: center;
|
|
368
|
+
gap: 0.6rem;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* Runwell additions: placeholder states when gallery is empty */
|
|
373
|
+
.runwell-bundle-quantity-builder__img--placeholder,
|
|
374
|
+
.runwell-bundle-quantity-builder__thumb-img--placeholder {
|
|
375
|
+
width: 100%;
|
|
376
|
+
height: 100%;
|
|
377
|
+
min-height: 320px;
|
|
378
|
+
background: linear-gradient(135deg, var(--runwell-oat, #F5F0EE), var(--runwell-cream, #EDE6D8));
|
|
379
|
+
display: block;
|
|
380
|
+
}
|
|
381
|
+
.runwell-bundle-quantity-builder__thumb-img--placeholder {
|
|
382
|
+
min-height: 0;
|
|
383
|
+
}
|
|
@@ -72,6 +72,204 @@
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/* -------------------------------------------------------------------
|
|
76
|
+
* Mode A quantity builder
|
|
77
|
+
*
|
|
78
|
+
* Per-tier radio selection state + image thumbnail swap + ATC.
|
|
79
|
+
* ATC handles two cases:
|
|
80
|
+
* 1. Plain tier: standard product form POST.
|
|
81
|
+
* 2. free_gift tier: intercept the submit, build a multi-line
|
|
82
|
+
* /cart/add.js POST with the bundle qty + 1 of the free gift
|
|
83
|
+
* product (read from data-free-gift-variant-id on the section).
|
|
84
|
+
* After success: open Dawn cart drawer + dispatch cart:updated so any
|
|
85
|
+
* downstream xsell listeners refresh.
|
|
86
|
+
* ----------------------------------------------------------------- */
|
|
87
|
+
function initQuantityBuilder() {
|
|
88
|
+
document
|
|
89
|
+
.querySelectorAll('[data-runwell-bundle-quantity-builder]')
|
|
90
|
+
.forEach(function (section) {
|
|
91
|
+
// Tier selection visual state
|
|
92
|
+
section
|
|
93
|
+
.querySelectorAll('[data-runwell-tier-radio]')
|
|
94
|
+
.forEach(function (radio) {
|
|
95
|
+
radio.addEventListener('change', function () {
|
|
96
|
+
section
|
|
97
|
+
.querySelectorAll('.runwell-bundle-quantity-builder__option')
|
|
98
|
+
.forEach(function (label) {
|
|
99
|
+
const input = label.querySelector('[data-runwell-tier-radio]');
|
|
100
|
+
label.classList.toggle(
|
|
101
|
+
'runwell-bundle-quantity-builder__option--selected',
|
|
102
|
+
input ? input.checked : false
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Thumbnail swap (image gallery)
|
|
109
|
+
section.querySelectorAll('[data-thumb]').forEach(function (btn) {
|
|
110
|
+
btn.addEventListener('click', function () {
|
|
111
|
+
const idx = btn.dataset.thumb;
|
|
112
|
+
section
|
|
113
|
+
.querySelectorAll('.runwell-bundle-quantity-builder__slide')
|
|
114
|
+
.forEach(function (s) {
|
|
115
|
+
s.classList.toggle(
|
|
116
|
+
'runwell-bundle-quantity-builder__slide--active',
|
|
117
|
+
s.dataset.slide === idx
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
section
|
|
121
|
+
.querySelectorAll('.runwell-bundle-quantity-builder__thumb')
|
|
122
|
+
.forEach(function (t) {
|
|
123
|
+
t.classList.toggle(
|
|
124
|
+
'runwell-bundle-quantity-builder__thumb--active',
|
|
125
|
+
t === btn
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ATC handler
|
|
132
|
+
const form = section.querySelector('[data-runwell-bundle-form]');
|
|
133
|
+
if (!form) return;
|
|
134
|
+
const atc = form.querySelector('[data-runwell-bundle-atc]');
|
|
135
|
+
const giftVariantId = section.dataset.freeGiftVariantId || '';
|
|
136
|
+
|
|
137
|
+
form.addEventListener('submit', function (e) {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
const selected = form.querySelector(
|
|
140
|
+
'.runwell-bundle-quantity-builder__option--selected'
|
|
141
|
+
);
|
|
142
|
+
if (!selected) return;
|
|
143
|
+
const qty = parseInt(selected.dataset.tierQty || '1', 10);
|
|
144
|
+
const wantsGift = selected.dataset.tierFreeGift === 'true' && giftVariantId !== '';
|
|
145
|
+
const variantId = form.querySelector('input[name="id"]').value;
|
|
146
|
+
|
|
147
|
+
setAtcState(atc, 'loading');
|
|
148
|
+
|
|
149
|
+
const items = [{ id: parseInt(variantId, 10), quantity: qty }];
|
|
150
|
+
if (wantsGift) {
|
|
151
|
+
items.push({ id: parseInt(giftVariantId, 10), quantity: 1 });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fetch('/cart/add.js', {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
157
|
+
body: JSON.stringify({ items: items }),
|
|
158
|
+
})
|
|
159
|
+
.then(function (r) {
|
|
160
|
+
if (!r.ok) throw new Error('add failed');
|
|
161
|
+
return r.json();
|
|
162
|
+
})
|
|
163
|
+
.then(function () {
|
|
164
|
+
setAtcState(atc, 'added');
|
|
165
|
+
document.dispatchEvent(new CustomEvent('cart:updated', { bubbles: true }));
|
|
166
|
+
// Open Dawn cart drawer if present
|
|
167
|
+
const drawer = document.querySelector('cart-drawer');
|
|
168
|
+
if (drawer && typeof drawer.open === 'function') drawer.open();
|
|
169
|
+
setTimeout(function () {
|
|
170
|
+
setAtcState(atc, 'idle');
|
|
171
|
+
}, 2000);
|
|
172
|
+
})
|
|
173
|
+
.catch(function () {
|
|
174
|
+
setAtcState(atc, 'idle');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function setAtcState(atc, state) {
|
|
181
|
+
if (!atc) return;
|
|
182
|
+
const textEl = atc.querySelector('.runwell-bundle-quantity-builder__atc-text');
|
|
183
|
+
const loadEl = atc.querySelector('.runwell-bundle-quantity-builder__atc-loading');
|
|
184
|
+
if (!textEl || !loadEl) return;
|
|
185
|
+
if (state === 'loading') {
|
|
186
|
+
textEl.style.display = 'none';
|
|
187
|
+
loadEl.style.display = 'inline';
|
|
188
|
+
atc.disabled = true;
|
|
189
|
+
} else if (state === 'added') {
|
|
190
|
+
loadEl.style.display = 'none';
|
|
191
|
+
textEl.textContent = 'ADDED';
|
|
192
|
+
textEl.style.display = 'inline';
|
|
193
|
+
} else {
|
|
194
|
+
loadEl.style.display = 'none';
|
|
195
|
+
textEl.textContent = 'ADD TO CART';
|
|
196
|
+
textEl.style.display = 'inline';
|
|
197
|
+
atc.disabled = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* -------------------------------------------------------------------
|
|
202
|
+
* Cart-drawer xsell refresh
|
|
203
|
+
*
|
|
204
|
+
* The cart drawer is server-rendered once. When customers add or
|
|
205
|
+
* change items via async fetch (PDP ATC, qty controls, remove), the
|
|
206
|
+
* bundle xsell area would otherwise stay stale. We hook fetch() to
|
|
207
|
+
* detect cart mutations, then re-render #CartDrawer-XsellSlot from
|
|
208
|
+
* the current page's cart-drawer section via Section Rendering API.
|
|
209
|
+
*
|
|
210
|
+
* Tenants opt in by adding <div id="CartDrawer-XsellSlot"> around the
|
|
211
|
+
* bundle xsell in their cart-drawer snippet (or by using the
|
|
212
|
+
* snippets/runwell-bundle-cart-xsell.liquid wrapper which does this).
|
|
213
|
+
* If the slot isn't present, this is a no-op.
|
|
214
|
+
* ----------------------------------------------------------------- */
|
|
215
|
+
function initCartMutationRefresh() {
|
|
216
|
+
if (window.__runwellBundleCartRefreshInstalled) return;
|
|
217
|
+
window.__runwellBundleCartRefreshInstalled = true;
|
|
218
|
+
|
|
219
|
+
const MUTATION_RE = /\/cart\/(add|change|update|clear)(?:\.js)?(?:[?#]|$)/;
|
|
220
|
+
let pending = null;
|
|
221
|
+
|
|
222
|
+
function scheduleRefresh() {
|
|
223
|
+
if (pending) return;
|
|
224
|
+
pending = setTimeout(function () {
|
|
225
|
+
pending = null;
|
|
226
|
+
refresh();
|
|
227
|
+
}, 120);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function refresh() {
|
|
231
|
+
const slot = document.getElementById('CartDrawer-XsellSlot');
|
|
232
|
+
if (!slot) return;
|
|
233
|
+
const url =
|
|
234
|
+
window.location.pathname +
|
|
235
|
+
(window.location.search ? window.location.search + '&' : '?') +
|
|
236
|
+
'sections=cart-drawer';
|
|
237
|
+
fetch(url, { credentials: 'same-origin' })
|
|
238
|
+
.then(function (r) {
|
|
239
|
+
return r.json();
|
|
240
|
+
})
|
|
241
|
+
.then(function (data) {
|
|
242
|
+
const html = data && data['cart-drawer'];
|
|
243
|
+
if (!html) return;
|
|
244
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
245
|
+
const fresh = doc.getElementById('CartDrawer-XsellSlot');
|
|
246
|
+
if (!fresh) return;
|
|
247
|
+
slot.innerHTML = fresh.innerHTML;
|
|
248
|
+
})
|
|
249
|
+
.catch(function () {
|
|
250
|
+
/* silent; keep current xsell */
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const origFetch = window.fetch;
|
|
255
|
+
window.fetch = function () {
|
|
256
|
+
const args = arguments;
|
|
257
|
+
const p = origFetch.apply(this, args);
|
|
258
|
+
try {
|
|
259
|
+
const first = args[0];
|
|
260
|
+
const url = typeof first === 'string' ? first : (first && first.url) || '';
|
|
261
|
+
if (MUTATION_RE.test(url)) {
|
|
262
|
+
p.then(scheduleRefresh, function () {});
|
|
263
|
+
}
|
|
264
|
+
} catch (e) {}
|
|
265
|
+
return p;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
['cart:updated', 'cart:refresh', 'cart-drawer:updated'].forEach(function (name) {
|
|
269
|
+
document.addEventListener(name, scheduleRefresh);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
75
273
|
function initFilterChip() {
|
|
76
274
|
document
|
|
77
275
|
.querySelectorAll('[data-runwell-bundle-filter-chip]')
|
|
@@ -433,6 +631,8 @@
|
|
|
433
631
|
initFilterChip();
|
|
434
632
|
initCartXsell();
|
|
435
633
|
initByob();
|
|
634
|
+
initQuantityBuilder();
|
|
635
|
+
initCartMutationRefresh();
|
|
436
636
|
});
|
|
437
637
|
} else {
|
|
438
638
|
initFomo();
|
|
@@ -440,5 +640,7 @@
|
|
|
440
640
|
initFilterChip();
|
|
441
641
|
initCartXsell();
|
|
442
642
|
initByob();
|
|
643
|
+
initQuantityBuilder();
|
|
644
|
+
initCartMutationRefresh();
|
|
443
645
|
}
|
|
444
646
|
})();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runwell-bundle-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"category": "catalog",
|
|
5
5
|
"source": "runwell",
|
|
6
6
|
"base": "bundle-system",
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
"sections/runwell-bundle-home-stacks.liquid",
|
|
14
14
|
"sections/runwell-bundle-cart-xsell.liquid",
|
|
15
15
|
"sections/runwell-bundle-pdp-banner.liquid",
|
|
16
|
-
"sections/runwell-bundle-collection.liquid"
|
|
16
|
+
"sections/runwell-bundle-collection.liquid",
|
|
17
|
+
"sections/runwell-bundle-quantity-builder.liquid"
|
|
17
18
|
],
|
|
18
19
|
"snippets": [
|
|
19
20
|
"snippets/runwell-bundle-card.liquid",
|
|
21
|
+
"snippets/runwell-bundle-cart-xsell.liquid",
|
|
20
22
|
"snippets/runwell-bundle-quantity-tiers.liquid",
|
|
21
23
|
"snippets/runwell-bundle-multi-product.liquid",
|
|
22
24
|
"snippets/runwell-bundle-pricing.liquid",
|
|
@@ -32,7 +34,8 @@
|
|
|
32
34
|
],
|
|
33
35
|
"assets": [
|
|
34
36
|
"assets/runwell-bundle-system.css",
|
|
35
|
-
"assets/runwell-bundle-system.js"
|
|
37
|
+
"assets/runwell-bundle-system.js",
|
|
38
|
+
"assets/runwell-bundle-quantity-builder.css"
|
|
36
39
|
]
|
|
37
40
|
},
|
|
38
41
|
"config": {
|
|
@@ -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
|
|