zz-shopify-components 0.24.1-beta.2 → 0.24.1-beta.4

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.
Binary file
@@ -419,7 +419,8 @@ body.zz-dialog-open {
419
419
  position: fixed;
420
420
  inset: 0;
421
421
  display: grid;
422
- place-items: center;
422
+ place-items: start center;
423
+ padding-top: 20%;
423
424
  pointer-events: none;
424
425
  /* 不阻挡点击 */
425
426
  z-index: 9999;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-shopify-components",
3
- "version": "0.24.1-beta.2",
3
+ "version": "0.24.1-beta.4",
4
4
  "description": "Reusable Shopify components for theme projects",
5
5
  "keywords": [
6
6
  "shopify",
@@ -16,35 +16,53 @@
16
16
  {% for block in accessories_points %}
17
17
  {% if block.settings.swiper_index == accessories_points_index %}
18
18
  <div
19
- onclick='window.accessoriesPointClickHandler({{ block.settings.accessories_product.selected_or_first_available_variant.id }})'
20
- data-point-swiper-index='{{ block.settings.swiper_index }}'
21
- class='product-accessories-point-item tw-absolute tw-rounded-full tw-backdrop-blur-[20px] tw-gap-[12px] tw-flex tw-items-center tw-justify-center tw-bg-[rgba(255,255,255,0.8)] lg:tw-py-[6px] tw-py-[4px] lg:tw-pl-[8px] tw-pl-[4px] tw-pr-[20px] tw-z-50 tw-cursor-pointer'
22
- style='left: {{ block.settings.point_x }}%; top: {{ block.settings.point_y }}%;'
23
- >
24
- <svg
25
- width='36'
26
- height='36'
27
- viewBox='0 0 36 36'
28
- fill='none'
29
- xmlns='http://www.w3.org/2000/svg'
19
+ onclick='window.accessoriesPointClickHandler(event, {{ block.settings.accessories_product.variants[0].id }})'
20
+ data-point-swiper-index='{{ block.settings.swiper_index }}'
21
+ class='product-accessories-point-item tw-absolute tw-rounded-full tw-backdrop-blur-[20px] tw-gap-[12px] tw-flex tw-items-center tw-justify-center tw-bg-[rgba(255,255,255,0.8)] lg:tw-py-[6px] tw-py-[4px] lg:tw-pl-[8px] tw-pl-[4px] tw-pr-[20px] tw-z-50 tw-cursor-pointer'
22
+ style='left: {{ block.settings.point_x }}%; top: {{ block.settings.point_y }}%;'
30
23
  >
31
- <rect width="36" height="36" rx="18" fill="#FC6C0F"/>
32
- <path d="M8 10H9.51521C10.2098 10 10.8135 10.4769 10.9744 11.1526L11.4143 13M11.4143 13L13.3341 21.0632C13.5486 21.9642 14.3536 22.6 15.2797 22.6H23.786C24.7278 22.6 25.542 21.943 25.7409 21.0225L26.9511 15.4225C27.2204 14.1765 26.2709 13 24.9962 13H11.4143Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
33
- <circle cx="24.584" cy="26" r="0.6" fill="white" stroke="white" stroke-width="0.8"/>
34
- <circle cx="14.916" cy="26" r="0.6" fill="white" stroke="white" stroke-width="0.8"/>
35
- <path d="M17.4746 17.7998H21.4746" stroke="white" stroke-width="1.3" stroke-linecap="round"/>
36
- <path d="M19.4746 19.7998V15.7998" stroke="white" stroke-width="1.3" stroke-linecap="round"/>
37
- </svg>
38
- <div class=" tw-flex tw-flex-col tw-gap-[4px] ">
39
- <div class="lg:tw-text-[16px] tw-text-[12px] tw-leading-none tw-font-bold">
40
- {{
41
- block.settings.accessories_title
42
- | default: block.settings.accessories_product.title
43
- }}
44
- </div>
45
- <div class="lg:tw-text-[14px] tw-text-[12px] tw-leading-none tw-font-medium">
46
- {{ block.settings.accessories_product.price | money_without_trailing_zeros }}
47
- </div>
24
+ <svg
25
+ width='36'
26
+ height='36'
27
+ viewBox='0 0 36 36'
28
+ fill='none'
29
+ xmlns='http://www.w3.org/2000/svg'
30
+ class="tw-block buy-icon tw-w-[36px] tw-h-[36px]"
31
+ >
32
+ <rect width="36" height="36" rx="18" fill="#FC6C0F"/>
33
+ <path d="M8 10H9.51521C10.2098 10 10.8135 10.4769 10.9744 11.1526L11.4143 13M11.4143 13L13.3341 21.0632C13.5486 21.9642 14.3536 22.6 15.2797 22.6H23.786C24.7278 22.6 25.542 21.943 25.7409 21.0225L26.9511 15.4225C27.2204 14.1765 26.2709 13 24.9962 13H11.4143Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
34
+ <circle cx="24.584" cy="26" r="0.6" fill="white" stroke="white" stroke-width="0.8"/>
35
+ <circle cx="14.916" cy="26" r="0.6" fill="white" stroke="white" stroke-width="0.8"/>
36
+ <path d="M17.4746 17.7998H21.4746" stroke="white" stroke-width="1.3" stroke-linecap="round"/>
37
+ <path d="M19.4746 19.7998V15.7998" stroke="white" stroke-width="1.3" stroke-linecap="round"/>
38
+ </svg>
39
+
40
+ <svg
41
+ width='30'
42
+ height='30'
43
+ viewBox='0 0 30 30'
44
+ fill='none'
45
+ xmlns='http://www.w3.org/2000/svg'
46
+ class="tw-hidden buy-done-icon tw-w-[36px] tw-h-[36px]"
47
+ >
48
+ <rect width="30" height="30" rx="15" fill="#FC6C0F"/>
49
+ <rect x="1" y="1" width="28" height="28" rx="14" stroke="white" stroke-opacity="0.3" stroke-width="2"/>
50
+ <path d="M10.4168 15.0586L13.9523 18.5941L19.8449 12.7016" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
51
+ </svg>
52
+
53
+ <div class=' tw-flex tw-flex-col '>
54
+ <div class='lg:tw-text-[16px] tw-text-[12px] tw-leading-[1.2] tw-font-bold'>
55
+ {{
56
+ block.settings.accessories_title
57
+ | default: block.settings.accessories_product.title
58
+ }}
59
+ </div>
60
+ <div class='lg:tw-text-[14px] tw-text-[12px] tw-leading-[1.5] tw-font-medium'>
61
+ {{
62
+ block.settings.accessories_product.price
63
+ | money_without_trailing_zeros
64
+ }}
65
+ </div>
48
66
  </div>
49
67
  </div>
50
68
  {% endif %}
@@ -71,7 +89,7 @@
71
89
  | image_url: width: 200
72
90
  | image_tag:
73
91
  loading: 'lazy',
74
- class: 'lg:tw-w-[50px] tw-w-[40px] tw-aspect-square tw-object-cover tw-rounded-[6px] ',
92
+ class: 'lg:tw-w-[40px] tw-w-[30px] tw-aspect-square tw-object-cover tw-rounded-[6px] ',
75
93
  sizes: '(min-width: 750px) 50px, calc(100vw - 30px)',
76
94
  widths: '50, 100, 150'
77
95
  }}
@@ -80,8 +98,6 @@
80
98
  </div>
81
99
  </div>
82
100
  </div>
83
-
84
-
85
101
  </div>
86
102
 
87
103
  <style>
@@ -190,33 +206,69 @@
190
206
  });
191
207
  });
192
208
 
193
- window.accessoriesPointClickHandler = async function(variantId) {
194
- try {
195
- const res = await fetch(window.routes.cart_add_url, {
196
- method: 'POST',
197
- headers: {
198
- 'Content-Type': 'application/json',
199
- 'Accept': 'application/json'
209
+ window.accessoriesPointClickHandler = async function(eventOrElement, variantId) {
210
+ const element =
211
+ eventOrElement?.currentTarget ||
212
+ (eventOrElement instanceof Event ? eventOrElement.target : eventOrElement);
213
+
214
+ if (!(element instanceof Element)) {
215
+ console.warn('accessoriesPointClickHandler: expected an Element but got', eventOrElement);
216
+ return;
217
+ }
218
+
219
+ const buyIcon = element.querySelector('.buy-icon');
220
+ const buyDoneIcon = element.querySelector('.buy-done-icon');
221
+ if (buyIcon) buyIcon.style.display = 'none';
222
+ if (buyDoneIcon) buyDoneIcon.style.display = 'block';
223
+ const data = {
224
+ items: [
225
+ {
226
+ id: variantId,
227
+ quantity: 1,
200
228
  },
201
- body: JSON.stringify({
202
- items: [{ id: variantId, quantity: 1 }]
203
- })
229
+ ],
230
+ sections: 'cart-drawer,cart-icon-bubble',
231
+ };
232
+ const cart = document.querySelector('cart-drawer');
233
+
234
+ await fetch(`${routes.cart_add_url}`, {
235
+ method: 'POST',
236
+ headers: {
237
+ 'Content-Type': 'application/json',
238
+ Accept: 'application/javascript',
239
+ },
240
+ body: JSON.stringify(data),
241
+ })
242
+ .then((response) => response.json())
243
+ .then((response) => {
244
+ if (response.status) {
245
+ publish(PUB_SUB_EVENTS.cartError, {
246
+ source: 'product-form',
247
+ productVariantId: variantId,
248
+ errors: response.errors || response.description,
249
+ message: response.message,
250
+ });
251
+ }
252
+
253
+ publish(PUB_SUB_EVENTS.cartUpdate, {
254
+ source: 'product-form',
255
+ productVariantId: variantId,
256
+ cartData: response,
257
+ });
258
+ cart.renderContents(response, false);
259
+ zzShowToast('1 item added to Cart', { type: 'success' });
260
+
261
+ })
262
+ .catch((e) => {
263
+ console.error(e);
264
+ })
265
+ .finally(() => {
266
+ setTimeout(() => {
267
+ if (buyIcon) buyIcon.style.display = 'block';
268
+ if (buyDoneIcon) buyDoneIcon.style.display = 'none';
269
+ }, 3000);
270
+ if (cart && cart.classList.contains('is-empty'))
271
+ cart.classList.remove('is-empty');
204
272
  });
205
- const data = await res.json();
206
- if (data && data.status) {
207
- console.error('Add to cart error:', data);
208
- alert(data.message || 'Failed to add to cart');
209
- return;
210
- }
211
- const cartDrawer = document.querySelector('cart-drawer');
212
- if (cartDrawer && typeof cartDrawer.renderContents === 'function') {
213
- cartDrawer.renderContents(data);
214
- if (typeof cartDrawer.open === 'function') cartDrawer.open();
215
- } else {
216
- window.location.href = window.routes.cart_url;
217
- }
218
- } catch (e) {
219
- console.error(e);
220
- }
221
273
  };
222
274
  </script>
@@ -1,6 +1,10 @@
1
- <gallery-tab >
1
+ <gallery-tab>
2
2
  <div class='gallery-tab-container lg:tw-mt-[20px] tw-mt-[12px] tw-absolute bottom-0 left-0 tw-w-full tw-z-10 tw-text-center tw-flex tw-items-center tw-justify-center lg:tw-gap-[12px] tw-gap-[4px]'>
3
- <div class='gallery-tab-item active' data-test-locator="galleryTabItemPhoto" data-test-value="photo">
3
+ <div
4
+ class='gallery-tab-item active'
5
+ data-test-locator='galleryTabItemPhoto'
6
+ data-test-value='photo'
7
+ >
4
8
  <div class='gallery-tab-item-text'>
5
9
  <svg
6
10
  xmlns='http://www.w3.org/2000/svg'
@@ -18,66 +22,78 @@
18
22
  </div>
19
23
  </div>
20
24
  {% if show_accessories_point %}
21
- <div class='gallery-tab-item' data-test-locator="galleryTabItemAccessories" data-test-value="accessories">
22
- <div class='gallery-tab-item-text'>
23
- <svg
24
- width='20'
25
- height='20'
26
- viewBox='0 0 20 20'
27
- fill='none'
28
- xmlns='http://www.w3.org/2000/svg'
29
- >
30
- <path d="M6.88574 3.83398H13.1035C13.9019 3.83401 14.5757 4.42847 14.6748 5.2207L15.8027 14.249C15.9207 15.1938 15.1845 16.028 14.2324 16.0283H5.75781C4.80554 16.0283 4.06858 15.1939 4.18652 14.249L5.31445 5.2207C5.41357 4.4285 6.08736 3.83406 6.88574 3.83398Z" stroke="black"/>
31
- <path d="M12.7724 7.15332C12.7724 8.68744 11.5287 9.9311 9.99457 9.9311C8.46045 9.9311 7.2168 8.68744 7.2168 7.15332" stroke="black" stroke-linecap="round"/>
32
- </svg>
25
+ <div
26
+ class='gallery-tab-item'
27
+ data-test-locator='galleryTabItemAccessories'
28
+ data-test-value='accessories'
29
+ >
30
+ <div class='gallery-tab-item-text'>
31
+ <svg
32
+ width='20'
33
+ height='20'
34
+ viewBox='0 0 20 20'
35
+ fill='none'
36
+ xmlns='http://www.w3.org/2000/svg'
37
+ >
38
+ <path d="M6.88574 3.83398H13.1035C13.9019 3.83401 14.5757 4.42847 14.6748 5.2207L15.8027 14.249C15.9207 15.1938 15.1845 16.028 14.2324 16.0283H5.75781C4.80554 16.0283 4.06858 15.1939 4.18652 14.249L5.31445 5.2207C5.41357 4.4285 6.08736 3.83406 6.88574 3.83398Z" stroke="black"/>
39
+ <path d="M12.7724 7.15332C12.7724 8.68744 11.5287 9.9311 9.99457 9.9311C8.46045 9.9311 7.2168 8.68744 7.2168 7.15332" stroke="black" stroke-linecap="round"/>
40
+ </svg>
33
41
 
34
- <span class='gallery-tab-item-text-span'> Accessories </span>
42
+ <span class='gallery-tab-item-text-span'> Accessories </span>
43
+ </div>
35
44
  </div>
36
- </div>
37
45
  {% endif %}
38
46
  {% if show_video %}
39
- <div class='gallery-tab-item' data-test-locator="galleryTabItemVideo" data-test-value="video">
40
- <div class='gallery-tab-item-text'>
41
- <svg
42
- width='16'
43
- height='16'
44
- viewBox='0 0 16 16'
45
- fill='none'
46
- xmlns='http://www.w3.org/2000/svg'
47
- >
48
- <rect x="0.5" y="2.5" width="15" height="11" rx="1.5" stroke="black"/>
49
- <path d="M6.26348 6.16606C6.26362 5.85833 6.59736 5.66564 6.86394 5.81938L10.1644 7.72531C10.4306 7.87943 10.4304 8.26404 10.1639 8.41793L6.86389 10.3232C6.59737 10.4771 6.2642 10.2849 6.26385 9.97726L6.26348 6.16606Z" stroke="black"/>
50
- </svg>
51
- <span class='gallery-tab-item-text-span'> Video </span>
47
+ <div
48
+ class='gallery-tab-item'
49
+ data-test-locator='galleryTabItemVideo'
50
+ data-test-value='video'
51
+ >
52
+ <div class='gallery-tab-item-text'>
53
+ <svg
54
+ width='16'
55
+ height='16'
56
+ viewBox='0 0 16 16'
57
+ fill='none'
58
+ xmlns='http://www.w3.org/2000/svg'
59
+ >
60
+ <rect x="0.5" y="2.5" width="15" height="11" rx="1.5" stroke="black"/>
61
+ <path d="M6.26348 6.16606C6.26362 5.85833 6.59736 5.66564 6.86394 5.81938L10.1644 7.72531C10.4306 7.87943 10.4304 8.26404 10.1639 8.41793L6.86389 10.3232C6.59737 10.4771 6.2642 10.2849 6.26385 9.97726L6.26348 6.16606Z" stroke="black"/>
62
+ </svg>
63
+ <span class='gallery-tab-item-text-span'> Video </span>
64
+ </div>
52
65
  </div>
53
- </div>
54
66
  {% endif %}
55
67
  {% if show_model %}
56
- <div class='gallery-tab-item' data-test-locator="galleryTabItem3D" data-test-value="3d">
57
- <div class='gallery-tab-item-text'>
58
- <svg
59
- width='16'
60
- height='16'
61
- viewBox='0 0 16 16'
62
- fill='none'
63
- xmlns='http://www.w3.org/2000/svg'
64
- >
65
- <path d="M12.6963 3.78809V8.71094L8 11.4229L3.30371 8.71094V3.78809L8 1.07617L12.6963 3.78809Z" stroke="black"/>
66
- <path d="M12.6346 3.75198L8.03554 6.40723L3.43652 3.75198" stroke="black"/>
67
- <path d="M8 11.375V5.875" stroke="black" stroke-linejoin="round"/>
68
- <path d="M6.41406 12.5996L7.82828 14.0138L6.41406 15.428" stroke="black" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
69
- <path d="M3.30431 7C1.59439 7.73314 0.5 8.858 0.5 10.1192C0.5 12.2387 3.59098 13.9732 7.5 14.1104M12.6957 7C14.4056 7.73314 15.5 8.858 15.5 10.1192C15.5 12.0543 12.9234 13.6685 9.5 14.0392" stroke="black"/>
70
- </svg>
71
- <span class='gallery-tab-item-text-span'> 3D </span>
68
+ <div
69
+ class='gallery-tab-item'
70
+ data-test-locator='galleryTabItem3D'
71
+ data-test-value='3d'
72
+ >
73
+ <div class='gallery-tab-item-text'>
74
+ <svg
75
+ width='16'
76
+ height='16'
77
+ viewBox='0 0 16 16'
78
+ fill='none'
79
+ xmlns='http://www.w3.org/2000/svg'
80
+ >
81
+ <path d="M12.6963 3.78809V8.71094L8 11.4229L3.30371 8.71094V3.78809L8 1.07617L12.6963 3.78809Z" stroke="black"/>
82
+ <path d="M12.6346 3.75198L8.03554 6.40723L3.43652 3.75198" stroke="black"/>
83
+ <path d="M8 11.375V5.875" stroke="black" stroke-linejoin="round"/>
84
+ <path d="M6.41406 12.5996L7.82828 14.0138L6.41406 15.428" stroke="black" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
85
+ <path d="M3.30431 7C1.59439 7.73314 0.5 8.858 0.5 10.1192C0.5 12.2387 3.59098 13.9732 7.5 14.1104M12.6957 7C14.4056 7.73314 15.5 8.858 15.5 10.1192C15.5 12.0543 12.9234 13.6685 9.5 14.0392" stroke="black"/>
86
+ </svg>
87
+ <span class='gallery-tab-item-text-span'> 3D </span>
88
+ </div>
72
89
  </div>
73
- </div>
74
90
  {% endif %}
75
91
  </div>
76
92
  </gallery-tab>
77
93
 
78
94
  <style>
79
95
  .gallery-tab-item {
80
- padding: 8px 16px;
96
+ padding: 3px 16px;
81
97
  cursor: pointer;
82
98
  display: inline-block;
83
99
  border-radius: 100px;
@@ -89,12 +105,13 @@
89
105
 
90
106
  @media screen and (max-width: 1024px) {
91
107
  .gallery-tab-item {
92
- padding: 2px 8px;
108
+ padding: 8px 10px;
93
109
  font-size: 12px !important;
110
+ line-height: 12px;
94
111
  }
95
112
  }
96
113
  .gallery-tab-item:hover {
97
- background: #F5F5F5;
114
+ background: #f5f5f5;
98
115
  }
99
116
  .gallery-tab-item.active {
100
117
  color: #fff;
@@ -107,106 +124,134 @@
107
124
  gap: 8px;
108
125
  }
109
126
  .gallery-tab-item svg {
110
- transition:
111
- filter 0.2s ease,
112
- fill 0.2s ease,
113
- stroke 0.2s ease;
127
+ transition: filter 0.2s ease, fill 0.2s ease, stroke 0.2s ease;
114
128
  }
115
129
  .gallery-tab-item.active svg {
116
130
  filter: invert(1);
117
131
  }
118
132
  .gallery-tab-item-text-span {
119
133
  }
134
+ @media screen and (max-width: 1024px) {
135
+ .gallery-tab-item-text svg {
136
+ width: 12px;
137
+ height: 12px;
138
+ }
139
+ .gallery-tab-item-text {
140
+ font-size: 12px;
141
+ gap: 6px;
142
+ }
143
+ }
120
144
  </style>
121
145
 
122
146
  <script>
123
- ;(() => {
124
- class GalleryTab extends HTMLElement {
125
- static get observedAttributes() {
126
- return ['value'];
127
- }
128
- constructor() {
129
- super();
130
- this._items = [];
131
- this._suppressChange = false;
132
- this._onItemClick = this._onItemClick.bind(this);
133
- }
134
- connectedCallback() {
135
- this._items = Array.from(this.querySelectorAll('.gallery-tab-item'));
136
- this._items.forEach((item) => {
137
- item.addEventListener('click', this._onItemClick, false);
138
- item.setAttribute('role', 'tab');
139
- });
140
- const activeFromDom = this.querySelector('.gallery-tab-item.active');
141
- const activeFromDomValue = activeFromDom ? (activeFromDom.dataset.testValue || activeFromDom.getAttribute('data-test-value')) : null;
142
- const firstItemValue = this._items[0] ? (this._items[0].dataset.testValue || this._items[0].getAttribute('data-test-value')) : null;
143
- const initialValue = this.getAttribute('value') || activeFromDomValue || firstItemValue || '';
144
- this._suppressChange = true;
145
- this.setAttribute('value', String(initialValue));
146
- this._syncActive(initialValue);
147
- this._suppressChange = false;
148
- }
149
- disconnectedCallback() {
150
- this._items.forEach((item) => {
151
- item.removeEventListener('click', this._onItemClick, false);
152
- });
153
- }
154
- attributeChangedCallback(name, oldValue, newValue) {
155
- if (name !== 'value' || oldValue === newValue) return;
156
- this._syncActive(newValue);
157
- if (!this._suppressChange) {
158
- this.dispatchEvent(
159
- new CustomEvent('change', {
160
- detail: { value: newValue },
161
- bubbles: true
162
- })
163
- );
164
- document.querySelectorAll('.product-preview-item').forEach((item) => {
165
- if (item.dataset.galleryValue === newValue) {
166
- item.classList.add('active');
167
- } else {
168
- item.classList.remove('active');
169
- }
147
+ (() => {
148
+ class GalleryTab extends HTMLElement {
149
+ static get observedAttributes() {
150
+ return ['value'];
151
+ }
152
+ constructor() {
153
+ super();
154
+ this._items = [];
155
+ this._suppressChange = false;
156
+ this._onItemClick = this._onItemClick.bind(this);
157
+ }
158
+ connectedCallback() {
159
+ this._items = Array.from(this.querySelectorAll('.gallery-tab-item'));
160
+ this._items.forEach((item) => {
161
+ item.addEventListener('click', this._onItemClick, false);
162
+ item.setAttribute('role', 'tab');
170
163
  });
164
+ const activeFromDom = this.querySelector('.gallery-tab-item.active');
165
+ const activeFromDomValue = activeFromDom
166
+ ? activeFromDom.dataset.testValue ||
167
+ activeFromDom.getAttribute('data-test-value')
168
+ : null;
169
+ const firstItemValue = this._items[0]
170
+ ? this._items[0].dataset.testValue ||
171
+ this._items[0].getAttribute('data-test-value')
172
+ : null;
173
+ const initialValue =
174
+ this.getAttribute('value') ||
175
+ activeFromDomValue ||
176
+ firstItemValue ||
177
+ '';
178
+ this._suppressChange = true;
179
+ this.setAttribute('value', String(initialValue));
180
+ this._syncActive(initialValue);
181
+ this._suppressChange = false;
171
182
  }
172
- }
173
- get value() {
174
- return this.getAttribute('value');
175
- }
176
- set value(next) {
177
- if (next == null) {
178
- this.removeAttribute('value');
179
- } else {
180
- this.setAttribute('value', String(next));
183
+ disconnectedCallback() {
184
+ this._items.forEach((item) => {
185
+ item.removeEventListener('click', this._onItemClick, false);
186
+ });
181
187
  }
182
- }
183
- _onItemClick(event) {
184
- const item = event.currentTarget;
185
- const nextValue = item ? (item.dataset.testValue || item.getAttribute('data-test-value')) : '';
186
- if (!nextValue || nextValue === this.value) return;
187
- this.value = nextValue;
188
- }
189
- _syncActive(value) {
190
- const normalized = String(value || '');
191
- let matched = false;
192
- this._items.forEach((item) => {
193
- const itemValue = item.dataset.testValue || item.getAttribute('data-test-value') || '';
194
- const isActive = itemValue === normalized;
195
- item.classList.toggle('active', isActive);
196
- item.setAttribute('aria-selected', isActive ? 'true' : 'false');
197
- if (isActive) matched = true;
198
- });
199
- if (!matched && this._items[0]) {
200
- this._items.forEach((item, index) => {
201
- const isActive = index === 0;
188
+ attributeChangedCallback(name, oldValue, newValue) {
189
+ if (name !== 'value' || oldValue === newValue) return;
190
+ this._syncActive(newValue);
191
+ if (!this._suppressChange) {
192
+ this.dispatchEvent(
193
+ new CustomEvent('change', {
194
+ detail: { value: newValue },
195
+ bubbles: true,
196
+ })
197
+ );
198
+ document.querySelectorAll('.product-preview-item').forEach((item) => {
199
+ if (item.dataset.galleryValue === newValue) {
200
+ item.classList.add('active');
201
+ } else {
202
+ item.classList.remove('active');
203
+ }
204
+ });
205
+ }
206
+ document
207
+ .querySelectorAll('product-video-tab video')
208
+ .forEach((video) => {
209
+ video.pause();
210
+ video.currentTime = 0;
211
+ });
212
+ }
213
+ get value() {
214
+ return this.getAttribute('value');
215
+ }
216
+ set value(next) {
217
+ if (next == null) {
218
+ this.removeAttribute('value');
219
+ } else {
220
+ this.setAttribute('value', String(next));
221
+ }
222
+ }
223
+ _onItemClick(event) {
224
+ const item = event.currentTarget;
225
+ const nextValue = item
226
+ ? item.dataset.testValue || item.getAttribute('data-test-value')
227
+ : '';
228
+ if (!nextValue || nextValue === this.value) return;
229
+ this.value = nextValue;
230
+ }
231
+ _syncActive(value) {
232
+ const normalized = String(value || '');
233
+ let matched = false;
234
+ this._items.forEach((item) => {
235
+ const itemValue =
236
+ item.dataset.testValue ||
237
+ item.getAttribute('data-test-value') ||
238
+ '';
239
+ const isActive = itemValue === normalized;
202
240
  item.classList.toggle('active', isActive);
203
241
  item.setAttribute('aria-selected', isActive ? 'true' : 'false');
242
+ if (isActive) matched = true;
204
243
  });
244
+ if (!matched && this._items[0]) {
245
+ this._items.forEach((item, index) => {
246
+ const isActive = index === 0;
247
+ item.classList.toggle('active', isActive);
248
+ item.setAttribute('aria-selected', isActive ? 'true' : 'false');
249
+ });
250
+ }
205
251
  }
206
252
  }
207
- }
208
- if (!customElements.get('gallery-tab')) {
209
- customElements.define('gallery-tab', GalleryTab);
210
- }
211
- })();
212
- </script>
253
+ if (!customElements.get('gallery-tab')) {
254
+ customElements.define('gallery-tab', GalleryTab);
255
+ }
256
+ })();
257
+ </script>
@@ -1,8 +1,8 @@
1
- <div class='tw-w-full tw-max-w-[1680px] tw-mx-auto'>
1
+ <div class='tw-w-full tw-mx-auto'>
2
2
  <div class='product-container tw-flex tw-items-center tw-justify-center'>
3
3
  {% comment %} 左侧产品预览 {% endcomment %}
4
4
  <div
5
- class='product-preview tw-w-full tw-aspect-square md:tw-aspect-[16/9]'
5
+ class='product-preview tw-w-full lg:tw-max-w-[550px] 2xl:tw-max-w-[650px] tw-aspect-square lg:tw-rounded-[16px] tw-overflow-hidden'
6
6
  data-test-locator='productPreview'
7
7
  >
8
8
  <div
@@ -23,10 +23,10 @@
23
23
  </div>
24
24
  <div class='swiper-pagination'></div>
25
25
  <div class='product-swiper-button-prev'>
26
- {% render 'zz-prev-next-btn', type: 'prev', color_type: 'dark' %}
26
+ {% render 'zz-prev-next-blur-icon', type: 'prev', %}
27
27
  </div>
28
28
  <div class='product-swiper-button-next'>
29
- {% render 'zz-prev-next-btn', type: 'next', color_type: 'dark' %}
29
+ {% render 'zz-prev-next-blur-icon', type: 'next' %}
30
30
  </div>
31
31
  </div>
32
32
 
@@ -40,7 +40,7 @@
40
40
  | image_url: width: 200
41
41
  | image_tag:
42
42
  loading: 'lazy',
43
- class: 'lg:tw-w-[50px] tw-w-[40px] tw-aspect-square tw-object-cover tw-rounded-[6px] ',
43
+ class: 'lg:tw-w-[40px] tw-w-[30px] tw-aspect-square tw-object-cover tw-rounded-[6px] ',
44
44
  sizes: '(min-width: 750px) 50px, calc(100vw - 30px)',
45
45
  widths: '50, 100, 150'
46
46
  }}
@@ -52,6 +52,10 @@
52
52
  </zz-product-swiper>
53
53
 
54
54
  <style>
55
+ zz-product-swiper .product-swiper-button-prev path,
56
+ zz-product-swiper .product-swiper-button-next path {
57
+ fill: white;
58
+ }
55
59
  .product-swiper-container .swiper-pagination {
56
60
  --swiper-pagination-color: black;
57
61
  --swiper-pagination-bottom: 10px;
@@ -1,44 +1,152 @@
1
1
  {%- assign intro_video = product.metafields.custom.intro_video.value -%}
2
+ {%- assign livestream_id = product.metafields.custom.livestream_id.value -%}
2
3
  {%- assign livestream_video = product.metafields.custom.livestream_video.value -%}
4
+ <script
5
+ async
6
+ type='text/javascript'
7
+ src='//asset.fwcdn3.com/js/integrations/shopify.js'
8
+ ></script>
9
+ <script
10
+ async
11
+ type='text/javascript'
12
+ src='//asset.fwcdn3.com/js/fwn-async.js'
13
+ ></script>
14
+ <product-video-tab
15
+ class=' tw-w-full tw-h-full '
16
+ {%- if intro_video -%}
17
+ data-default-tab='intro'
18
+ {%- elsif livestream_video -%}
19
+ data-default-tab='livestream'
20
+ {%- endif -%}
21
+ >
22
+ {%- if intro_video
23
+ and intro_video.sources
24
+ and intro_video.sources.size > 0
25
+ -%}
26
+ <video
27
+ id='product-video-intro-{{ product.id }}'
28
+ data-type='intro'
29
+ src='{{ intro_video.sources[0].url }}'
30
+ poster='{{ intro_video.preview_image | image_url: width: 1000 }}'
31
+ controls
32
+ playsinline
33
+ muted
34
+ preload='metadata'
35
+ class=' tw-w-full tw-h-full tw-bg-black '
36
+ ></video>
37
+ {%- endif -%}
3
38
 
4
- <product-video-tab class=" tw-w-full tw-h-full ">
5
- <video data-type="intro" src='{{ intro_video.sources[0].url }}' poster='{{intro_video.preview_image | image_url: width: 1000}}' controls class=" tw-w-full tw-h-full tw-bg-black tw-hidden"></video>
6
- <video data-type="livestream" src='{{ livestream_video.sources[0].url }}' poster='{{intro_video.preview_image | image_url: width: 1000}}' controls class=" tw-w-full tw-h-full tw-bg-black tw-hidden"></video>
7
- <div class=" product-video-tabs tw-absolute tw-left-1/2 tw--translate-x-1/2 tw-top-[30px] tw-bg-white ">
8
- <div class=" tw-grid tw-grid-cols-2 tw-relative">
9
- <button type="button" class="product-video-tab-item" data-tab="intro">Introduction</button>
10
- <button type="button" class="product-video-tab-item" data-tab="livestream">Livestream</button>
11
- <div class="product-video-tab-slider tw-z-1 tw-rounded-full !tw-block tw-absolute tw-top-0 tw-w-1/2 tw-h-full tw-bg-white"></div>
39
+ {%- if livestream_id -%}
40
+ <div
41
+ id='firework-video-{{ product.id }}'
42
+ data-type='livestream'
43
+ class='tw-w-full tw-h-full tw-bg-black'
44
+ role='tabpanel'
45
+ aria-labelledby='product-video-tab-livestream-{{ product.id }}'
46
+ >
47
+ <fw-storyblock
48
+ channel='hover'
49
+ playlist='{{ livestream_id }}'
50
+ autoplay='true'
51
+ class='tw-h-full tw-w-full'
52
+ >
53
+ </fw-storyblock>
12
54
  </div>
13
- </div>
14
- </product-video-tab>
55
+ {% elsif livestream_video %}
56
+ <video
57
+ id='product-video-intro-{{ product.id }}'
58
+ role='tabpanel'
59
+ aria-labelledby='product-video-tab-livestream-{{ product.id }}'
60
+ data-type='livestream'
61
+ src='{{ livestream_video.sources[0].url }}'
62
+ poster='{{ livestream_video.preview_image | image_url: width: 1000 }}'
63
+ controls
64
+ playsinline
65
+ muted
66
+ preload='metadata'
67
+ class=' tw-w-full tw-h-full tw-bg-black '
68
+ ></video>
69
+ {%- endif -%}
70
+
71
+ {%- assign tabs_count = 0 -%}
72
+ {%- if intro_video
73
+ and intro_video.sources
74
+ and intro_video.sources.size > 0
75
+ -%}
76
+ {%- assign tabs_count = tabs_count | plus: 1 -%}
77
+ {%- endif -%}
78
+ {%- if livestream_video -%}
79
+ {%- assign tabs_count = tabs_count | plus: 1 -%}
80
+ {%- endif -%}
15
81
 
82
+ {%- if tabs_count > 1 -%}
83
+ <div
84
+ class=' product-video-tabs tw-absolute tw-left-1/2 tw--translate-x-1/2 tw-top-[30px] tw-bg-white '
85
+ role='tablist'
86
+ aria-label='Product video tabs'
87
+ >
88
+ <div class=' tw-grid tw-grid-cols-2 tw-relative'>
89
+ {%- if intro_video
90
+ and intro_video.sources
91
+ and intro_video.sources.size > 0
92
+ -%}
93
+ <button
94
+ type='button'
95
+ id='product-video-tab-intro-{{ product.id }}'
96
+ class='product-video-tab-item'
97
+ data-tab='intro'
98
+ role='tab'
99
+ aria-controls='product-video-intro-{{ product.id }}'
100
+ aria-selected='true'
101
+ >
102
+ Introduction
103
+ </button>
104
+ {%- endif -%}
105
+ {%- if livestream_video -%}
106
+ <button
107
+ type='button'
108
+ id='product-video-tab-livestream-{{ product.id }}'
109
+ class='product-video-tab-item'
110
+ data-tab='livestream'
111
+ role='tab'
112
+ aria-controls='firework-video-{{ product.id }}'
113
+ aria-selected='false'
114
+ >
115
+ Livestream
116
+ </button>
117
+ {%- endif -%}
118
+ <div
119
+ class='product-video-tab-slider tw-z-1 tw-rounded-full !tw-block tw-absolute tw-top-0 tw-w-1/2 tw-h-full tw-bg-white'
120
+ ></div>
121
+ </div>
122
+ </div>
123
+ {%- endif -%}
124
+ </product-video-tab>
16
125
 
17
126
  <style>
18
- .product-video-tabs {
19
- border-radius: 999px;
20
- background-color: rgba(255, 255, 255, 0.3);
21
- backdrop-filter: blur(10px);
22
- }
127
+ .product-video-tabs {
128
+ border-radius: 999px;
129
+ background-color: rgba(255, 255, 255, 0.3);
130
+ backdrop-filter: blur(10px);
131
+ }
132
+ .product-video-tab-item {
133
+ color: #fff;
134
+ z-index: 10;
135
+ text-align: center;
136
+ padding: 6px 20px;
137
+ cursor: pointer;
138
+ font-size: 16px;
139
+ transition: all 0.3s ease;
140
+ }
141
+ @media screen and (max-width: 1024px) {
23
142
  .product-video-tab-item {
24
- color: #fff;
25
- z-index: 10;
26
- text-align: center;
27
- padding: 6px 20px;
28
- cursor: pointer;
29
- font-size: 16px;
30
- transition: all 0.3s ease;
31
-
32
- }
33
- @media screen and (max-width: 1024px) {
34
- .product-video-tab-item {
35
- padding: 4px 12px;
36
- font-size: 10px;
37
- }
38
- }
39
- .product-video-tab-slider {
40
- transition: all 0.3s ease;
143
+ padding: 4px 12px;
144
+ font-size: 10px;
41
145
  }
146
+ }
147
+ .product-video-tab-slider {
148
+ transition: transform 0.3s ease;
149
+ }
42
150
  </style>
43
151
 
44
152
  <script>
@@ -46,24 +154,30 @@
46
154
  class ProductVideoTab extends HTMLElement {
47
155
  constructor() {
48
156
  super();
49
- this.currentTab = 'intro';
157
+ this.currentTab = this.dataset.defaultTab || 'intro';
50
158
  }
51
159
 
52
160
  connectedCallback() {
53
161
  this.cacheDom();
54
- if (!this.buttons.length || !this.videos.length) return;
162
+ if (!this.panels.length) return;
163
+ if (this.panels.length < 2 && this.tabsContainer) {
164
+ this.tabsContainer.style.display = 'none';
165
+ }
55
166
  this.changeTab(true);
56
167
  this.attachEvents();
57
168
  }
58
169
 
59
170
  cacheDom() {
60
- this.buttons = Array.from(this.querySelectorAll('.product-video-tab-item'));
61
- this.videos = Array.from(this.querySelectorAll('video[data-type]'));
171
+ this.tabsContainer = this.querySelector('.product-video-tabs');
172
+ this.buttons = Array.from(
173
+ this.querySelectorAll('.product-video-tab-item')
174
+ );
175
+ this.panels = Array.from(this.querySelectorAll('[data-type]'));
62
176
  this.slider = this.querySelector('.product-video-tab-slider');
63
177
  }
64
178
 
65
179
  attachEvents() {
66
- this.buttons.forEach(button => {
180
+ this.buttons.forEach((button) => {
67
181
  button.addEventListener('click', () => {
68
182
  const nextTab = button.dataset.tab;
69
183
  if (!nextTab || nextTab === this.currentTab) return;
@@ -73,37 +187,48 @@
73
187
  });
74
188
  }
75
189
 
190
+ isMedia(el) {
191
+ return el && (el.tagName === 'VIDEO' || el.tagName === 'AUDIO');
192
+ }
193
+
76
194
  changeTab(isInitial = false) {
77
- this.videos.forEach(video => {
78
- const isActive = video.dataset.type === this.currentTab;
79
- video.style.display = isActive ? 'block' : 'none';
80
- if (isActive) {
81
- const playPromise = video.play();
82
- if (playPromise && typeof playPromise.catch === 'function' && !isInitial) {
83
- playPromise.catch(() => {});
84
- }
85
- } else {
86
- video.pause();
195
+ this.panels.forEach((panel) => {
196
+ const isActive = panel.dataset.type === this.currentTab;
197
+ panel.style.display = isActive ? 'block' : 'none';
198
+ if (this.isMedia(panel)) {
199
+ try {
200
+ if (isActive) {
201
+ panel.muted = true;
202
+ const playPromise = panel.play && panel.play();
203
+ if (
204
+ playPromise &&
205
+ typeof playPromise.catch === 'function' &&
206
+ !isInitial
207
+ ) {
208
+ playPromise.catch(() => {});
209
+ }
210
+ } else {
211
+ panel.pause && panel.pause();
212
+ }
213
+ } catch (e) {}
87
214
  }
88
215
  });
89
216
 
90
- this.buttons.forEach(button => {
217
+ this.buttons.forEach((button) => {
91
218
  const isActive = button.dataset.tab === this.currentTab;
92
219
  button.style.color = isActive ? '#000' : '#fff';
220
+ button.setAttribute('aria-selected', isActive ? 'true' : 'false');
93
221
  });
94
222
 
95
223
  if (!this.slider) return;
96
224
  if (this.currentTab === 'intro') {
97
- this.slider.style.left = '0';
98
- this.slider.style.right = 'auto';
225
+ this.slider.style.transform = 'translateX(0%)';
99
226
  } else {
100
- this.slider.style.right = '0';
101
- this.slider.style.left = 'auto';
227
+ this.slider.style.transform = 'translateX(100%)';
102
228
  }
103
229
  }
104
230
  }
105
231
 
106
232
  customElements.define('product-video-tab', ProductVideoTab);
107
233
  })();
108
-
109
- </script>
234
+ </script>