@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
|
@@ -607,6 +607,252 @@
|
|
|
607
607
|
opacity: 0.7;
|
|
608
608
|
}
|
|
609
609
|
|
|
610
|
+
/* ---------- Mode C BYOB ---------- */
|
|
611
|
+
|
|
612
|
+
.runwell-bundle-system__byob-summary {
|
|
613
|
+
position: sticky;
|
|
614
|
+
top: 0;
|
|
615
|
+
z-index: 5;
|
|
616
|
+
display: flex;
|
|
617
|
+
flex-wrap: wrap;
|
|
618
|
+
align-items: center;
|
|
619
|
+
gap: 12px;
|
|
620
|
+
padding: 12px 14px;
|
|
621
|
+
border-radius: var(--runwell-radius-md, 12px);
|
|
622
|
+
background: var(--runwell-surface-card, #fff);
|
|
623
|
+
box-shadow: var(--runwell-shadow-card, 0 2px 8px rgba(0,0,0,0.04));
|
|
624
|
+
margin-bottom: 16px;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.runwell-bundle-system__byob-counter {
|
|
628
|
+
margin: 0;
|
|
629
|
+
font-size: 0.95rem;
|
|
630
|
+
font-weight: 600;
|
|
631
|
+
display: flex;
|
|
632
|
+
align-items: baseline;
|
|
633
|
+
gap: 4px;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.runwell-bundle-system__byob-counter-of,
|
|
637
|
+
.runwell-bundle-system__byob-counter-label {
|
|
638
|
+
font-weight: 400;
|
|
639
|
+
opacity: 0.75;
|
|
640
|
+
font-size: 0.85rem;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.runwell-bundle-system__byob-pricing {
|
|
644
|
+
margin-left: auto;
|
|
645
|
+
display: flex;
|
|
646
|
+
align-items: baseline;
|
|
647
|
+
gap: 8px;
|
|
648
|
+
flex-wrap: wrap;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.runwell-bundle-system__byob-pricing-subtotal {
|
|
652
|
+
opacity: 0.55;
|
|
653
|
+
font-size: 0.85rem;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.runwell-bundle-system__byob-pricing-current {
|
|
657
|
+
font-size: 1.2rem;
|
|
658
|
+
font-weight: 700;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.runwell-bundle-system__byob-pricing-badge {
|
|
662
|
+
padding: 4px 10px;
|
|
663
|
+
font-size: 0.78rem;
|
|
664
|
+
font-weight: 600;
|
|
665
|
+
border-radius: 999px;
|
|
666
|
+
background: var(--runwell-accent, currentColor);
|
|
667
|
+
color: var(--runwell-cream, #fff);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.runwell-bundle-system__byob-helper {
|
|
671
|
+
width: 100%;
|
|
672
|
+
margin: 0;
|
|
673
|
+
font-size: 0.8rem;
|
|
674
|
+
opacity: 0.75;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.runwell-bundle-system__byob-picker--grid {
|
|
678
|
+
display: grid;
|
|
679
|
+
grid-template-columns: repeat(2, 1fr);
|
|
680
|
+
gap: 10px;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
@media (min-width: 640px) {
|
|
684
|
+
.runwell-bundle-system__byob-picker--grid {
|
|
685
|
+
grid-template-columns: repeat(3, 1fr);
|
|
686
|
+
gap: 12px;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
@media (min-width: 1024px) {
|
|
691
|
+
.runwell-bundle-system__byob-picker--grid {
|
|
692
|
+
grid-template-columns: repeat(4, 1fr);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.runwell-bundle-system__byob-candidate {
|
|
697
|
+
position: relative;
|
|
698
|
+
display: flex;
|
|
699
|
+
flex-direction: column;
|
|
700
|
+
gap: 6px;
|
|
701
|
+
min-height: 44px;
|
|
702
|
+
padding: 10px;
|
|
703
|
+
border: 2px solid color-mix(in srgb, currentColor 10%, transparent);
|
|
704
|
+
border-radius: var(--runwell-radius-md, 12px);
|
|
705
|
+
background: var(--runwell-surface-card, #fff);
|
|
706
|
+
cursor: pointer;
|
|
707
|
+
transition: border-color 120ms ease, background 120ms ease;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.runwell-bundle-system__byob-candidate:hover {
|
|
711
|
+
border-color: color-mix(in srgb, var(--runwell-accent, currentColor) 50%, transparent);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.runwell-bundle-system__byob-candidate:has(.runwell-bundle-system__byob-input:checked) {
|
|
715
|
+
border-color: var(--runwell-accent, currentColor);
|
|
716
|
+
background: color-mix(in srgb, var(--runwell-accent, currentColor) 6%, var(--runwell-surface-card, #fff));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.runwell-bundle-system__byob-candidate--required {
|
|
720
|
+
border-style: dashed;
|
|
721
|
+
opacity: 0.95;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.runwell-bundle-system__byob-candidate--unavailable {
|
|
725
|
+
opacity: 0.55;
|
|
726
|
+
cursor: not-allowed;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.runwell-bundle-system__byob-input {
|
|
730
|
+
position: absolute;
|
|
731
|
+
opacity: 0;
|
|
732
|
+
pointer-events: none;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.runwell-bundle-system__byob-candidate-thumb {
|
|
736
|
+
width: 100%;
|
|
737
|
+
height: auto;
|
|
738
|
+
aspect-ratio: 1 / 1;
|
|
739
|
+
object-fit: cover;
|
|
740
|
+
border-radius: var(--runwell-radius-sm, 4px);
|
|
741
|
+
display: block;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.runwell-bundle-system__byob-candidate-body {
|
|
745
|
+
display: flex;
|
|
746
|
+
flex-direction: column;
|
|
747
|
+
gap: 2px;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.runwell-bundle-system__byob-candidate-title {
|
|
751
|
+
font-size: 0.9rem;
|
|
752
|
+
font-weight: 600;
|
|
753
|
+
line-height: 1.3;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.runwell-bundle-system__byob-candidate-price {
|
|
757
|
+
font-size: 0.85rem;
|
|
758
|
+
opacity: 0.8;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.runwell-bundle-system__byob-candidate-badge {
|
|
762
|
+
margin-top: 4px;
|
|
763
|
+
align-self: flex-start;
|
|
764
|
+
padding: 2px 8px;
|
|
765
|
+
font-size: 0.7rem;
|
|
766
|
+
font-weight: 600;
|
|
767
|
+
border-radius: 999px;
|
|
768
|
+
background: color-mix(in srgb, var(--runwell-blue, currentColor) 18%, transparent);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.runwell-bundle-system__byob-picker--accordion {
|
|
772
|
+
display: flex;
|
|
773
|
+
flex-direction: column;
|
|
774
|
+
gap: 8px;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.runwell-bundle-system__byob-accordion {
|
|
778
|
+
border: 1px solid color-mix(in srgb, currentColor 10%, transparent);
|
|
779
|
+
border-radius: var(--runwell-radius-md, 12px);
|
|
780
|
+
background: var(--runwell-surface-card, #fff);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.runwell-bundle-system__byob-accordion-summary {
|
|
784
|
+
display: flex;
|
|
785
|
+
align-items: center;
|
|
786
|
+
gap: 8px;
|
|
787
|
+
padding: 14px 16px;
|
|
788
|
+
min-height: 44px;
|
|
789
|
+
font-weight: 600;
|
|
790
|
+
cursor: pointer;
|
|
791
|
+
list-style: none;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.runwell-bundle-system__byob-accordion-summary::-webkit-details-marker {
|
|
795
|
+
display: none;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.runwell-bundle-system__byob-accordion-count {
|
|
799
|
+
margin-left: auto;
|
|
800
|
+
padding: 2px 10px;
|
|
801
|
+
font-size: 0.78rem;
|
|
802
|
+
font-weight: 600;
|
|
803
|
+
border-radius: 999px;
|
|
804
|
+
background: color-mix(in srgb, var(--runwell-accent, currentColor) 16%, transparent);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.runwell-bundle-system__byob-accordion-body {
|
|
808
|
+
display: grid;
|
|
809
|
+
grid-template-columns: repeat(2, 1fr);
|
|
810
|
+
gap: 10px;
|
|
811
|
+
padding: 0 16px 16px 16px;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
@media (min-width: 768px) {
|
|
815
|
+
.runwell-bundle-system__byob-accordion-body {
|
|
816
|
+
grid-template-columns: repeat(3, 1fr);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.runwell-bundle-system__byob-picker--radio {
|
|
821
|
+
display: flex;
|
|
822
|
+
flex-direction: column;
|
|
823
|
+
gap: 16px;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.runwell-bundle-system__byob-radio-group {
|
|
827
|
+
border: 0;
|
|
828
|
+
margin: 0;
|
|
829
|
+
padding: 0;
|
|
830
|
+
display: flex;
|
|
831
|
+
flex-direction: column;
|
|
832
|
+
gap: 8px;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.runwell-bundle-system__byob-radio-legend {
|
|
836
|
+
font-size: 0.85rem;
|
|
837
|
+
font-weight: 600;
|
|
838
|
+
letter-spacing: 0.04em;
|
|
839
|
+
text-transform: uppercase;
|
|
840
|
+
opacity: 0.75;
|
|
841
|
+
margin-bottom: 4px;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
.runwell-bundle-system__byob-candidate--radio {
|
|
845
|
+
flex-direction: row;
|
|
846
|
+
align-items: center;
|
|
847
|
+
padding: 10px 12px;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.runwell-bundle-system__byob-candidate--radio .runwell-bundle-system__byob-candidate-thumb {
|
|
851
|
+
width: 56px;
|
|
852
|
+
height: 56px;
|
|
853
|
+
aspect-ratio: 1 / 1;
|
|
854
|
+
}
|
|
855
|
+
|
|
610
856
|
/* ---------- Surface 4: cart drawer bundle cross-sell ---------- */
|
|
611
857
|
|
|
612
858
|
.runwell-bundle-cart-xsell {
|
|
@@ -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]')
|
|
@@ -271,17 +469,178 @@
|
|
|
271
469
|
});
|
|
272
470
|
}
|
|
273
471
|
|
|
472
|
+
/* ---------- Mode C BYOB ---------- */
|
|
473
|
+
|
|
474
|
+
function parsePricingValue(raw) {
|
|
475
|
+
if (raw == null || raw === '') return null;
|
|
476
|
+
try { return JSON.parse(raw); } catch (e) { return null; }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function computeByobPrice(subtotalCents, pricingModel, pricingValue) {
|
|
480
|
+
if (pricingModel === 'fixed_bundle_price' || pricingModel === 'fixed_price') {
|
|
481
|
+
const v = typeof pricingValue === 'object' && pricingValue !== null
|
|
482
|
+
? (pricingValue.amount || pricingValue.price || 0)
|
|
483
|
+
: pricingValue;
|
|
484
|
+
return Math.max(0, parseInt(v, 10) || 0);
|
|
485
|
+
}
|
|
486
|
+
if (pricingModel === 'percent_off_subtotal') {
|
|
487
|
+
const pct = typeof pricingValue === 'object' && pricingValue !== null
|
|
488
|
+
? (pricingValue.pct || pricingValue.percent || 0)
|
|
489
|
+
: pricingValue;
|
|
490
|
+
const discounted = subtotalCents * (1 - (parseFloat(pct) || 0) / 100);
|
|
491
|
+
return Math.max(0, Math.round(discounted));
|
|
492
|
+
}
|
|
493
|
+
if (pricingModel === 'dollar_off_subtotal') {
|
|
494
|
+
const off = typeof pricingValue === 'object' && pricingValue !== null
|
|
495
|
+
? (pricingValue.amount || pricingValue.off || 0)
|
|
496
|
+
: pricingValue;
|
|
497
|
+
return Math.max(0, subtotalCents - (parseInt(off, 10) || 0));
|
|
498
|
+
}
|
|
499
|
+
return subtotalCents;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function formatByobMoney(cents) {
|
|
503
|
+
return '$' + (cents / 100).toFixed(2);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function initByob() {
|
|
507
|
+
document
|
|
508
|
+
.querySelectorAll('[data-runwell-byob-picker]')
|
|
509
|
+
.forEach(function (picker) {
|
|
510
|
+
const form = picker.closest('form');
|
|
511
|
+
if (!form) return;
|
|
512
|
+
const root = form.parentElement || form;
|
|
513
|
+
const summary = root.querySelector('[data-runwell-byob-summary]');
|
|
514
|
+
const atc = form.querySelector('[data-runwell-byob-atc]');
|
|
515
|
+
|
|
516
|
+
const layout = picker.dataset.byobLayout;
|
|
517
|
+
const min = parseInt(picker.dataset.byobMin, 10) || 0;
|
|
518
|
+
const max = parseInt(picker.dataset.byobMax, 10) || 0;
|
|
519
|
+
const pricingModel = picker.dataset.byobPricingModel;
|
|
520
|
+
const pricingValue = parsePricingValue(picker.dataset.byobPricingValue);
|
|
521
|
+
|
|
522
|
+
function getSelected() {
|
|
523
|
+
return Array.from(picker.querySelectorAll('.runwell-bundle-system__byob-input:checked'))
|
|
524
|
+
.map(function (input) {
|
|
525
|
+
const label = input.closest('.runwell-bundle-system__byob-candidate');
|
|
526
|
+
return {
|
|
527
|
+
handle: label.dataset.handle,
|
|
528
|
+
price: parseInt(label.dataset.price, 10) || 0,
|
|
529
|
+
variantId: label.dataset.variantId,
|
|
530
|
+
category: label.dataset.category || null,
|
|
531
|
+
input: input
|
|
532
|
+
};
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function updateAccordionCounts() {
|
|
537
|
+
if (layout !== 'accordion') return;
|
|
538
|
+
picker.querySelectorAll('.runwell-bundle-system__byob-accordion').forEach(function (acc) {
|
|
539
|
+
const count = acc.querySelectorAll('.runwell-bundle-system__byob-input:checked').length;
|
|
540
|
+
const badge = acc.querySelector('[data-runwell-byob-cat-count]');
|
|
541
|
+
if (badge) badge.textContent = count;
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function enforceMax(selected) {
|
|
546
|
+
if (layout === 'radio') return;
|
|
547
|
+
if (selected.length <= max) return;
|
|
548
|
+
const overflow = selected.slice(max);
|
|
549
|
+
overflow.forEach(function (s) {
|
|
550
|
+
if (!s.input.disabled) s.input.checked = false;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function render() {
|
|
555
|
+
let selected = getSelected();
|
|
556
|
+
enforceMax(selected);
|
|
557
|
+
selected = getSelected();
|
|
558
|
+
|
|
559
|
+
const count = selected.length;
|
|
560
|
+
const subtotal = selected.reduce(function (acc, s) { return acc + s.price; }, 0);
|
|
561
|
+
const total = computeByobPrice(subtotal, pricingModel, pricingValue);
|
|
562
|
+
const savings = Math.max(0, subtotal - total);
|
|
563
|
+
|
|
564
|
+
if (summary) {
|
|
565
|
+
const selEl = summary.querySelector('[data-runwell-byob-selected]');
|
|
566
|
+
const totalEl = summary.querySelector('[data-runwell-byob-total]');
|
|
567
|
+
const subEl = summary.querySelector('[data-runwell-byob-subtotal]');
|
|
568
|
+
const subAmt = summary.querySelector('[data-runwell-byob-subtotal-amount]');
|
|
569
|
+
const savEl = summary.querySelector('[data-runwell-byob-savings]');
|
|
570
|
+
const savAmt = summary.querySelector('[data-runwell-byob-savings-amount]');
|
|
571
|
+
const helper = summary.querySelector('[data-runwell-byob-helper]');
|
|
572
|
+
|
|
573
|
+
if (selEl) selEl.textContent = count;
|
|
574
|
+
if (totalEl) totalEl.textContent = formatByobMoney(total);
|
|
575
|
+
const isFixed = pricingModel === 'fixed_bundle_price' || pricingModel === 'fixed_price';
|
|
576
|
+
if (savings > 0 && !isFixed) {
|
|
577
|
+
if (subEl) { subEl.hidden = false; if (subAmt) subAmt.textContent = formatByobMoney(subtotal); }
|
|
578
|
+
if (savEl) { savEl.hidden = false; if (savAmt) savAmt.textContent = formatByobMoney(savings); }
|
|
579
|
+
} else {
|
|
580
|
+
if (subEl) subEl.hidden = true;
|
|
581
|
+
if (savEl) savEl.hidden = true;
|
|
582
|
+
}
|
|
583
|
+
if (helper) helper.style.display = count >= min ? 'none' : '';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (atc) {
|
|
587
|
+
const ok = count >= min && count <= max;
|
|
588
|
+
atc.disabled = !ok;
|
|
589
|
+
}
|
|
590
|
+
updateAccordionCounts();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
picker.addEventListener('change', function (e) {
|
|
594
|
+
if (!e.target.matches('.runwell-bundle-system__byob-input')) return;
|
|
595
|
+
render();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
form.addEventListener('submit', function (e) {
|
|
599
|
+
const selected = getSelected();
|
|
600
|
+
if (selected.length < min || selected.length > max) {
|
|
601
|
+
e.preventDefault();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
e.preventDefault();
|
|
605
|
+
const items = selected.map(function (s) {
|
|
606
|
+
return { id: parseInt(s.variantId, 10), quantity: 1 };
|
|
607
|
+
});
|
|
608
|
+
fetch('/cart/add.js', {
|
|
609
|
+
method: 'POST',
|
|
610
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
611
|
+
body: JSON.stringify({ items: items })
|
|
612
|
+
})
|
|
613
|
+
.then(function (r) { return r.json(); })
|
|
614
|
+
.then(function () {
|
|
615
|
+
document.dispatchEvent(new CustomEvent('cart:refresh'));
|
|
616
|
+
window.location.href = '/cart';
|
|
617
|
+
})
|
|
618
|
+
.catch(function () {
|
|
619
|
+
if (atc) atc.disabled = false;
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
render();
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
274
627
|
if (document.readyState === 'loading') {
|
|
275
628
|
document.addEventListener('DOMContentLoaded', function () {
|
|
276
629
|
initFomo();
|
|
277
630
|
initTierSync();
|
|
278
631
|
initFilterChip();
|
|
279
632
|
initCartXsell();
|
|
633
|
+
initByob();
|
|
634
|
+
initQuantityBuilder();
|
|
635
|
+
initCartMutationRefresh();
|
|
280
636
|
});
|
|
281
637
|
} else {
|
|
282
638
|
initFomo();
|
|
283
639
|
initTierSync();
|
|
284
640
|
initFilterChip();
|
|
285
641
|
initCartXsell();
|
|
642
|
+
initByob();
|
|
643
|
+
initQuantityBuilder();
|
|
644
|
+
initCartMutationRefresh();
|
|
286
645
|
}
|
|
287
646
|
})();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runwell-bundle-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"category": "catalog",
|
|
5
5
|
"source": "runwell",
|
|
6
6
|
"base": "bundle-system",
|
|
@@ -13,21 +13,29 @@
|
|
|
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",
|
|
23
25
|
"snippets/runwell-bundle-cross-supplier.liquid",
|
|
24
26
|
"snippets/runwell-bundle-fomo.liquid",
|
|
25
27
|
"snippets/runwell-bundle-free-gift.liquid",
|
|
26
|
-
"snippets/runwell-bundle-data.liquid"
|
|
28
|
+
"snippets/runwell-bundle-data.liquid",
|
|
29
|
+
"snippets/runwell-bundle-byob-picker.liquid",
|
|
30
|
+
"snippets/runwell-bundle-byob-picker-grid.liquid",
|
|
31
|
+
"snippets/runwell-bundle-byob-picker-accordion.liquid",
|
|
32
|
+
"snippets/runwell-bundle-byob-picker-radio.liquid",
|
|
33
|
+
"snippets/runwell-bundle-byob-summary.liquid"
|
|
27
34
|
],
|
|
28
35
|
"assets": [
|
|
29
36
|
"assets/runwell-bundle-system.css",
|
|
30
|
-
"assets/runwell-bundle-system.js"
|
|
37
|
+
"assets/runwell-bundle-system.js",
|
|
38
|
+
"assets/runwell-bundle-quantity-builder.css"
|
|
31
39
|
]
|
|
32
40
|
},
|
|
33
41
|
"config": {
|
|
@@ -74,6 +82,12 @@
|
|
|
74
82
|
"url": "https://admin.shopify.com/store/{store_handle}/products/new",
|
|
75
83
|
"summary": "Use the Shopify Bundles app to create the bundle (Apps > Bundles > Create). The app creates a bundle product. SKU convention: BUNDLE-<handle>. Then open the bundle product, scroll to Metafields, and fill the runwell.bundle_* fields per the bundle's mode."
|
|
76
84
|
},
|
|
85
|
+
{
|
|
86
|
+
"id": "configure-byob-mode",
|
|
87
|
+
"label": "Configure a BYOB (Mode C) bundle product",
|
|
88
|
+
"url": "https://admin.shopify.com/store/{store_handle}/products",
|
|
89
|
+
"summary": "Mode C only. Set bundle_mode = 'byob'. Required: bundle_byob_candidates (list of product references), bundle_byob_min_picks, bundle_byob_max_picks, bundle_pricing_model (fixed_bundle_price / percent_off_subtotal / dollar_off_subtotal), bundle_pricing_value. Optional: bundle_byob_layout (grid default, accordion, radio), bundle_byob_categories (JSON), bundle_byob_required_handles. Install a Shopify Functions discount that mirrors the pricing model so the savings apply at checkout."
|
|
90
|
+
},
|
|
77
91
|
{
|
|
78
92
|
"id": "configure-quantity-tier-discount-function",
|
|
79
93
|
"label": "Create a Shopify Functions discount for quantity-tier mode (Mode A only)",
|