zz-shopify-components 0.35.1-beta.6 → 0.35.1-beta.8

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.
@@ -311,51 +311,52 @@ body.zz-dialog-open {
311
311
  .zz-btn.zz-btn-default.zz-btn-black {
312
312
  background: transparent;
313
313
  color: #2D2D2D;
314
- border: 1.5px solid #2D2D2D;
314
+ border: 1px solid #2D2D2D;
315
315
  }
316
316
 
317
317
  .zz-btn.zz-btn-default.zz-btn-white {
318
318
  background: transparent;
319
319
  color: #fff;
320
- border: 1.5px solid #FFFFFF4D;
320
+ border: 1px solid #FFFFFF;
321
+ font-weight: 400;
321
322
  }
322
323
 
323
324
  .zz-btn.zz-btn-default.zz-btn-orange {
324
325
  background: transparent;
325
326
  color: #FC6C0F;
326
- border: 1.5px solid #FC6C0F;
327
+ border: 1px solid #FC6C0F;
327
328
  }
328
329
 
329
330
  .zz-btn.zz-btn-default.zz-btn-yellow {
330
331
  background: transparent;
331
332
  color: #000;
332
- border: 1.5px solid #FFEB00;
333
+ border: 1px solid #FFEB00;
333
334
  }
334
335
 
335
336
  .zz-btn.zz-btn-default.zz-btn-blue {
336
337
  background: transparent;
337
338
  color: #378DDD;
338
- border: 1.5px solid #378DDD;
339
+ border: 1px solid #378DDD;
339
340
  }
340
341
 
341
342
  .zz-btn.zz-btn-round.zz-btn-black {
342
343
  background: transparent;
343
344
  color: #2D2D2D;
344
- border: 1.5px solid #2D2D2D;
345
+ border: 1px solid #2D2D2D;
345
346
  border-radius: 30px;
346
347
  }
347
348
 
348
349
  .zz-btn.zz-btn-round.zz-btn-white {
349
350
  background: transparent;
350
351
  color: #fff;
351
- border: 1.5px solid #FFFFFF4D;
352
+ border: 1px solid #FFFFFF4D;
352
353
  border-radius: 30px;
353
354
  }
354
355
 
355
356
  .zz-btn.zz-btn-round.zz-btn-orange {
356
357
  background: transparent;
357
358
  color: #ff9900;
358
- border: 1.5px solid #ff9900;
359
+ border: 1px solid #ff9900;
359
360
  border-radius: 30px;
360
361
  }
361
362
 
@@ -408,6 +409,11 @@ body.zz-dialog-open {
408
409
  font-size: 14px;
409
410
  }
410
411
 
412
+ .zz-btn.zz-btn-large.zz-btn-default {
413
+ padding: 0px 1.6rem;
414
+
415
+ }
416
+
411
417
  @media (min-width: 1024px) {
412
418
  .zz-btn {
413
419
  height: 40px;
@@ -434,6 +440,10 @@ body.zz-dialog-open {
434
440
  .zz-btn-link.zz-btn-link-large {
435
441
  font-size: 16px;
436
442
  }
443
+
444
+ .zz-btn.zz-btn-large.zz-btn-default {
445
+ padding: 0px 30px;
446
+ }
437
447
  }
438
448
 
439
449
  .zz-video-btn.zz-btn.zz-btn-white {
@@ -1,6 +1,7 @@
1
1
  {% schema %}
2
2
  {
3
3
  "name": "Button v2",
4
+ "class": "zz-button-v2",
4
5
  "settings": [
5
6
  {
6
7
  "type": "text",
@@ -102,6 +103,18 @@
102
103
  ],
103
104
  "default": "black",
104
105
  },
106
+ {
107
+ "type": "color",
108
+ "id": "custom_background_color",
109
+ "label": "自定义背景颜色",
110
+ "info": "按钮颜色有限,颜色自定义"
111
+ },
112
+ {
113
+ "type": "color",
114
+ "id": "custom_text_color",
115
+ "label": "自定义文字颜色",
116
+ "info": "按钮文字颜色有限,颜色自定义"
117
+ },
105
118
  {
106
119
  "type": "select",
107
120
  "id": "size",
@@ -213,6 +226,18 @@
213
226
  }
214
227
  }
215
228
  {% endif %}
229
+
230
+ {% if block.settings.custom_background_color != blank %}
231
+ #shopify-block-{{ block.id }} .{{ btn_class }} {
232
+ background: {{ block.settings.custom_background_color }} !important;
233
+ }
234
+ {% endif %}
235
+
236
+ {% if block.settings.custom_text_color != blank %}
237
+ #shopify-block-{{ block.id }} .{{ btn_class }} {
238
+ color: {{ block.settings.custom_text_color }} !important;
239
+ }
240
+ {% endif %}
216
241
  </style>
217
242
 
218
243
  {% render 'zz-button-v2',
@@ -0,0 +1,255 @@
1
+ <style>
2
+ #shopify-block-{{ block.id }} .zz-countdown {
3
+ display: flex;
4
+ }
5
+
6
+ #shopify-block-{{ block.id }} .zz-countdown-title {
7
+ font-weight: 700;
8
+ font-size: 20px;
9
+ line-height: 120%;
10
+ letter-spacing: 0%;
11
+ {% if block.settings.theme_color == 'white' %}
12
+ color: #FFFFFF;
13
+ {% else %}
14
+ color: #2D2D2D !important;
15
+ {% endif %}
16
+
17
+ {% if block.settings.title_position == 'front' %}
18
+ margin-right: 24px;
19
+ {% else %}
20
+ margin-left: 24px;
21
+ {% endif %}
22
+ }
23
+
24
+ #shopify-block-{{ block.id }} .zz-countdown-timer {
25
+ display: flex;
26
+ gap: 20px;
27
+
28
+ }
29
+
30
+ #shopify-block-{{ block.id }} .zz-countdown-unit {
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: center;
34
+ gap: 4px;
35
+ position: relative;
36
+ }
37
+
38
+ #shopify-block-{{ block.id }} .zz-countdown-number {
39
+ width: 48px;
40
+ height: 48px;
41
+ display: flex;
42
+
43
+ align-items: center;
44
+ justify-content: center;
45
+ {% if block.settings.theme_color == 'white' %}
46
+ background: #FFFFFF33;
47
+ color: #FFFFFF;
48
+ {% else %}
49
+ background: #2D2D2D66;
50
+ color:#2D2D2D;
51
+ {% endif %}
52
+
53
+ border-radius: 4px;
54
+ font-size: 18px;
55
+ font-weight: 600;
56
+ }
57
+
58
+ #shopify-block-{{ block.id }} .zz-countdown-label {
59
+ font-size: 12px;
60
+ font-weight: 400;
61
+ {% if block.settings.theme_color == 'white' %}
62
+ color: #F5F5F5B2;
63
+ {% else %}
64
+ color: #2D2D2D;
65
+ {% endif %}
66
+ }
67
+
68
+ #shopify-block-{{ block.id }} .zz-countdown-separator {
69
+ font-size: 12px;
70
+ font-weight: 700;
71
+ {% if block.settings.theme_color == 'white' %}
72
+ color: #FFFFFF;
73
+ {% else %}
74
+ color: #2D2D2D;
75
+ {% endif %}
76
+ margin-top: 14px;
77
+ }
78
+
79
+ /* 响应式设计 */
80
+ @media screen and (max-width: 1024px) {
81
+ #shopify-block-{{ block.id }} .zz-countdown-title {
82
+ font-size: 20px;
83
+ }
84
+
85
+ #shopify-block-{{ block.id }} .zz-countdown-timer {
86
+ gap: 12px;
87
+ }
88
+
89
+ #shopify-block-{{ block.id }} .zz-countdown-number {
90
+ width: 34px;
91
+ height: 34px;
92
+ font-size: 14px;
93
+ }
94
+
95
+ #shopify-block-{{ block.id }} .zz-countdown-label {
96
+ font-size: 10px;
97
+ }
98
+
99
+ #shopify-block-{{ block.id }} .zz-countdown-separator {
100
+ font-size: 12px;
101
+ font-weight: 700;
102
+ margin-top: 10px;
103
+ }
104
+ #shopify-block-{{ block.id }} .zz-countdown-title {
105
+ font-size: 14px;
106
+ }
107
+
108
+
109
+ }
110
+
111
+
112
+ </style>
113
+
114
+ <div
115
+ class="zz-countdown tw-flex {% if block.settings.title_position == 'back' %} tw-flex-row-reverse {% endif %}">
116
+ <div class="zz-countdown-title">{{ block.settings.title }}</div>
117
+ <div class="zz-countdown-timer">
118
+ <div class="zz-countdown-unit">
119
+ <div class="zz-countdown-number" id="zz-countdown-days">00</div>
120
+ <div class="zz-countdown-label">{{ block.settings.day_label | default: 'Day' }}</div>
121
+ </div>
122
+ <div class="zz-countdown-separator">:</div>
123
+ <div class="zz-countdown-unit">
124
+ <div class="zz-countdown-number" id="zz-countdown-hours">00</div>
125
+ <div class="zz-countdown-label">{{ block.settings.hour_label | default: 'Hour' }}</div>
126
+ </div>
127
+ <div class="zz-countdown-separator">:</div>
128
+ <div class="zz-countdown-unit">
129
+ <div class="zz-countdown-number" id="zz-countdown-minutes">00</div>
130
+ <div class="zz-countdown-label">{{ block.settings.minute_label | default: 'Minute' }}</div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <script>
136
+ document.addEventListener('DOMContentLoaded', function() {
137
+ // 获取结束时间
138
+ const endTimeStr = '{{ block.settings.end_time | date: "%s" }}';
139
+
140
+ // 解析结束时间
141
+ const endTime = Number(endTimeStr) * 1000;
142
+ if (isNaN(endTime)) {
143
+ console.error('Invalid end time format');
144
+ return;
145
+ }
146
+
147
+ // 获取倒计时元素
148
+ const daysElement = document.getElementById('zz-countdown-days');
149
+ const hoursElement = document.getElementById('zz-countdown-hours');
150
+ const minutesElement = document.getElementById('zz-countdown-minutes');
151
+
152
+ // 补零函数
153
+ function padZero(num) {
154
+ return num < 10 ? '0' + num : num;
155
+ }
156
+
157
+ // 更新倒计时函数
158
+ function updateCountdown() {
159
+ const now = Date.now();
160
+ const distance = endTime - now;
161
+
162
+ // 如果倒计时结束
163
+ if (distance < 0) {
164
+ daysElement.textContent = '00';
165
+ hoursElement.textContent = '00';
166
+ minutesElement.textContent = '00';
167
+ return;
168
+ }
169
+
170
+ // 计算剩余时间
171
+ const days = Math.floor(distance / (1000 * 60 * 60 * 24));
172
+ const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
173
+ const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
174
+
175
+ // 更新UI
176
+ daysElement.textContent = padZero(days);
177
+ hoursElement.textContent = padZero(hours);
178
+ minutesElement.textContent = padZero(minutes);
179
+ }
180
+
181
+ // 初始更新
182
+ updateCountdown();
183
+
184
+ // 每分钟更新一次
185
+ const timer = setInterval(updateCountdown, 60000);
186
+
187
+ // 清理定时器
188
+ window.addEventListener('beforeunload', function() {
189
+ clearInterval(timer);
190
+ });
191
+ });
192
+ </script>
193
+
194
+ {% schema %}
195
+ {
196
+ "name": "Count Down",
197
+ "settings": [
198
+ {
199
+ "type": "text",
200
+ "id": "end_time",
201
+ "default": "2026-04-28 23:59:59",
202
+ "label": "结束时间",
203
+ "info": "shopify后台配置的时区,格式:YYYY-MM-DD HH:MM:SS"
204
+ },
205
+ {
206
+ "type": "richtext",
207
+ "id": "title",
208
+ "label": "标题"
209
+ },
210
+ {
211
+ "type": "select",
212
+ "id": "theme_color",
213
+ "label": "文案颜色模式",
214
+ "options": [
215
+ { "value": "black", "label": "黑色模式" },
216
+ { "value": "white", "label": "白色模式" },
217
+ ],
218
+ "default": "white",
219
+ },
220
+ {
221
+ "type": "select",
222
+ "id": "title_position",
223
+ "label": "标题位置",
224
+ "options": [
225
+ { "value": "front", "label": "前置" },
226
+ { "value": "back", "label": "后置" },
227
+ ],
228
+ "default": "front",
229
+ },
230
+ {
231
+ "type": "text",
232
+ "id": "day_label",
233
+ "label": "天数标签",
234
+ "default": "Day"
235
+ },
236
+ {
237
+ "type": "text",
238
+ "id": "hour_label",
239
+ "label": "小时标签",
240
+ "default": "Hour"
241
+ },
242
+ {
243
+ "type": "text",
244
+ "id": "minute_label",
245
+ "label": "分钟标签",
246
+ "default": "Minute"
247
+ }
248
+ ],
249
+ "presets": [
250
+ {
251
+ "name": "Count Down"
252
+ }
253
+ ]
254
+ }
255
+ {% endschema %}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-shopify-components",
3
- "version": "0.35.1-beta.6",
3
+ "version": "0.35.1-beta.8",
4
4
  "description": "Reusable Shopify components for theme projects",
5
5
  "keywords": [
6
6
  "shopify",
@@ -0,0 +1,417 @@
1
+ {% schema %}
2
+ {
3
+ "name": "ZZ Product Subnav",
4
+ "class": "zz-product-subnav-section",
5
+ "settings": [
6
+ {
7
+ "type": "number",
8
+ "id": "top_offset",
9
+ "label": "吸附偏移量 (sticky top)",
10
+ "default": 50
11
+ },
12
+ {
13
+ "type": "number",
14
+ "id": "scroll_offset_desktop",
15
+ "label": "点击滚动偏移量 (PC)",
16
+ "default": 120
17
+ },
18
+ {
19
+ "type": "number",
20
+ "id": "scroll_offset_mobile",
21
+ "label": "点击滚动偏移量 (Mob)",
22
+ "default": 100
23
+ },
24
+ {
25
+ "type": "text",
26
+ "id": "max_width",
27
+ "label": "内容最大宽度 (PC, CSS value)",
28
+ "default": "1520px",
29
+ "info": "例如:1520px / 1660px / 100%"
30
+ },
31
+ {
32
+ "type": "color",
33
+ "id": "background_color",
34
+ "label": "背景颜色",
35
+ "default": "#FDBD33"
36
+ },
37
+ {
38
+ "type": "select",
39
+ "id": "text_color_mode",
40
+ "label": "文案颜色模式",
41
+ "default": "black",
42
+ "options": [
43
+ {
44
+ "value": "black",
45
+ "label": "黑色"
46
+ },
47
+ {
48
+ "value": "white",
49
+ "label": "白色"
50
+ }
51
+ ]
52
+ }
53
+ ],
54
+ "blocks": [
55
+ {
56
+ "type": "tab",
57
+ "name": "Tab",
58
+ "settings": [
59
+ {
60
+ "type": "text",
61
+ "id": "label",
62
+ "label": "Tab 文案",
63
+ "default": "X1 PRO"
64
+ },
65
+ {
66
+ "type": "text",
67
+ "id": "target_module_name",
68
+ "label": "目标模块 data-zz-module-name",
69
+ "info": "用于定位页面模块,如:x1-pro(匹配 [data-zz-module-name='x1-pro'])"
70
+ },
71
+ {
72
+ "type": "text",
73
+ "id": "target_selector",
74
+ "label": "目标选择器 (可选)",
75
+ "info": "优先级高于模块名。例如:#x1-pro 或 .my-section 或 [data-zz-module-name~='x1-pro']"
76
+ }
77
+ ]
78
+ }
79
+ ],
80
+ "presets": [
81
+ {
82
+ "name": "ZZ Product Subnav"
83
+ }
84
+ ]
85
+ }
86
+ {% endschema %}
87
+
88
+ {%- assign top_offset = section.settings.top_offset | default: 50 -%}
89
+ {%- assign text_color_mode = section.settings.text_color_mode | default: 'black' -%}
90
+ {%- if text_color_mode == 'white' -%}
91
+ {%- assign tab_text_color = 'rgba(255,255,255,0.8)' -%}
92
+ {%- assign tab_active_text_color = '#FFFFFF' -%}
93
+ {%- else -%}
94
+ {%- assign tab_text_color = 'rgba(45,45,45,0.8)' -%}
95
+ {%- assign tab_active_text_color = '#2D2D2D' -%}
96
+ {%- endif -%}
97
+
98
+ <nav class="zz-product-subnav" aria-label="Product sub navigation">
99
+ <div class="zz-product-subnav__wrap">
100
+ <div class="zz-product-subnav__track" role="tablist">
101
+ {%- for block in section.blocks -%}
102
+ {%- assign label = block.settings.label | default: '' -%}
103
+ {%- assign module_name = block.settings.target_module_name | default: '' -%}
104
+ {%- assign selector = block.settings.target_selector | default: '' -%}
105
+ <button
106
+ type="button"
107
+ class="zz-product-subnav__tab{% if forloop.first %} is-active{% endif %}"
108
+ role="tab"
109
+ aria-selected="{% if forloop.first %}true{% else %}false{% endif %}"
110
+ data-zz-subnav-label="{{ label | escape }}"
111
+ data-zz-subnav-module="{{ module_name | escape }}"
112
+ data-zz-subnav-selector="{{ selector | escape }}"
113
+ {{ block.shopify_attributes }}
114
+ >
115
+ <span class="zz-product-subnav__tabText">{{ label }}</span>
116
+ </button>
117
+ {%- endfor -%}
118
+ </div>
119
+ </div>
120
+ </nav>
121
+
122
+ <style>
123
+ #shopify-section-{{ section.id }} {
124
+ position: sticky;
125
+ top: {{ top_offset }}px;
126
+ z-index: 30;
127
+ }
128
+
129
+ #shopify-section-{{ section.id }} .zz-product-subnav {
130
+ -webkit-backdrop-filter: blur(10px);
131
+ backdrop-filter: blur(10px);
132
+ background: {{ section.settings.background_color | default: '#FDBD33' }};
133
+ }
134
+
135
+ #shopify-section-{{ section.id }} .zz-product-subnav__wrap {
136
+ max-width: {{ section.settings.max_width | default: '1520px' }};
137
+ margin: 0 auto;
138
+ padding: 0 20px;
139
+ }
140
+
141
+ #shopify-section-{{ section.id }} .zz-product-subnav__track {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 60px;
145
+ justify-content: center;
146
+ overflow-x: auto;
147
+ overflow-y: hidden;
148
+ scrollbar-width: none;
149
+ -ms-overflow-style: none;
150
+ -webkit-overflow-scrolling: touch;
151
+ }
152
+
153
+ #shopify-section-{{ section.id }} .zz-product-subnav__track::-webkit-scrollbar {
154
+ width: 0;
155
+ height: 0;
156
+ display: none;
157
+ }
158
+
159
+ #shopify-section-{{ section.id }} .zz-product-subnav__tab {
160
+ appearance: none;
161
+ background: transparent;
162
+ border: 0;
163
+ padding: 16px 0;
164
+ cursor: pointer;
165
+ flex: 0 0 auto;
166
+ display: inline-flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ border-bottom: 3px solid transparent;
170
+ }
171
+
172
+ #shopify-section-{{ section.id }} .zz-product-subnav__tabText {
173
+ font-family: Poppins, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
174
+ font-size: 16px;
175
+ line-height: 24px;
176
+ font-weight: 500;
177
+ color: {{ tab_text_color }};
178
+ white-space: nowrap;
179
+ }
180
+
181
+ #shopify-section-{{ section.id }} .zz-product-subnav__tab.is-active {
182
+ border-bottom-color: {{ tab_active_text_color }};
183
+ }
184
+
185
+ #shopify-section-{{ section.id }} .zz-product-subnav__tab.is-active .zz-product-subnav__tabText {
186
+ font-weight: 700;
187
+ color: {{ tab_active_text_color }};
188
+ }
189
+
190
+ @media (max-width: 1023px) {
191
+ #shopify-section-{{ section.id }} .zz-product-subnav__wrap {
192
+ padding: 0 20px;
193
+ }
194
+
195
+ #shopify-section-{{ section.id }} .zz-product-subnav__track {
196
+ justify-content: flex-start;
197
+ gap: 24px;
198
+ }
199
+
200
+ #shopify-section-{{ section.id }} .zz-product-subnav__tab {
201
+ padding: 12px 0;
202
+ }
203
+
204
+ #shopify-section-{{ section.id }} .zz-product-subnav__tabText {
205
+ font-size: 14px;
206
+ }
207
+ }
208
+ </style>
209
+
210
+ <script>
211
+ (function () {
212
+ const sectionEl = document.getElementById('shopify-section-{{ section.id }}');
213
+ if (!sectionEl) return;
214
+
215
+ // Prevent duplicate bindings (theme editor / section re-render)
216
+ if (typeof sectionEl.__zzProductSubnavCleanup === 'function') {
217
+ sectionEl.__zzProductSubnavCleanup();
218
+ sectionEl.__zzProductSubnavCleanup = null;
219
+ }
220
+
221
+ const trackEl = sectionEl.querySelector('.zz-product-subnav__track');
222
+ const tabs = Array.from(sectionEl.querySelectorAll('.zz-product-subnav__tab'));
223
+ if (!trackEl || tabs.length === 0) return;
224
+
225
+ const scrollOffsetDesktop = Number({{ section.settings.scroll_offset_desktop | default: 120 }}) || 0;
226
+ const scrollOffsetMobile = Number({{ section.settings.scroll_offset_mobile | default: 100 }}) || 0;
227
+ const mqDesktop = window.matchMedia('(min-width: 1024px)');
228
+
229
+ const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
230
+ const getScrollOffset = () => (mqDesktop.matches ? scrollOffsetDesktop : scrollOffsetMobile);
231
+
232
+ function resolveTarget(tabEl) {
233
+ const selector = (tabEl.getAttribute('data-zz-subnav-selector') || '').trim();
234
+ const moduleName = (tabEl.getAttribute('data-zz-subnav-module') || '').trim();
235
+
236
+ if (selector) {
237
+ try {
238
+ return document.querySelector(selector);
239
+ } catch (e) {
240
+ return null;
241
+ }
242
+ }
243
+
244
+ if (moduleName) {
245
+ return document.querySelector(`[data-zz-module-name="${CSS.escape(moduleName)}"]`);
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ function setActive(tabEl, shouldScrollIntoView) {
252
+ tabs.forEach((t) => {
253
+ const isActive = t === tabEl;
254
+ t.classList.toggle('is-active', isActive);
255
+ t.setAttribute('aria-selected', isActive ? 'true' : 'false');
256
+ });
257
+
258
+ if (shouldScrollIntoView) {
259
+ const containerWidth = trackEl.clientWidth;
260
+ const tabLeft = tabEl.offsetLeft;
261
+ const tabWidth = tabEl.offsetWidth;
262
+ const targetLeft = tabLeft - (containerWidth / 2) + (tabWidth / 2);
263
+ const nextLeft = clamp(targetLeft, 0, Math.max(0, trackEl.scrollWidth - containerWidth));
264
+ trackEl.scrollTo({ left: nextLeft, behavior: 'smooth' });
265
+ }
266
+ }
267
+
268
+ function scrollToTarget(tabEl) {
269
+ const target = resolveTarget(tabEl);
270
+ if (!target) return;
271
+ const rect = target.getBoundingClientRect();
272
+ const offsetTop = rect.top + window.scrollY - getScrollOffset();
273
+ window.scrollTo({ top: offsetTop, behavior: 'smooth' });
274
+ }
275
+
276
+ // Click to scroll
277
+ tabs.forEach((tabEl) => {
278
+ tabEl.addEventListener('click', () => {
279
+ setActive(tabEl, true);
280
+ scrollToTarget(tabEl);
281
+ });
282
+ });
283
+
284
+ // Scroll-spy (activate when target crosses viewport center)
285
+ const ac = new AbortController();
286
+ let targets = [];
287
+ let rafId = 0;
288
+ let domObserver = null;
289
+
290
+ function buildTargets() {
291
+ targets = tabs
292
+ .map((tabEl) => ({ tabEl, target: resolveTarget(tabEl) }))
293
+ .filter((x) => !!x.target);
294
+ return targets;
295
+ }
296
+
297
+ function computeActiveByViewportCenter() {
298
+ if (targets.length === 0) return;
299
+ const mid = window.innerHeight / 2;
300
+
301
+ let best = null;
302
+ let bestDist = Infinity;
303
+
304
+ for (const x of targets) {
305
+ const r = x.target.getBoundingClientRect();
306
+ if (!r || r.height <= 0) continue;
307
+
308
+ // Primary rule: viewport center is inside the target region
309
+ if (r.top <= mid && r.bottom >= mid) {
310
+ const center = r.top + r.height / 2;
311
+ const dist = Math.abs(center - mid);
312
+ if (dist < bestDist) {
313
+ best = x;
314
+ bestDist = dist;
315
+ }
316
+ }
317
+ }
318
+
319
+ // Fallback: choose the last section above the center
320
+ if (!best) {
321
+ let above = null;
322
+ let aboveTop = -Infinity;
323
+ for (const x of targets) {
324
+ const r = x.target.getBoundingClientRect();
325
+ if (!r || r.height <= 0) continue;
326
+ if (r.top <= mid && r.top > aboveTop) {
327
+ above = x;
328
+ aboveTop = r.top;
329
+ }
330
+ }
331
+ best = above;
332
+ }
333
+
334
+ if (!best) return;
335
+
336
+ const currentActive = sectionEl.querySelector('.zz-product-subnav__tab.is-active');
337
+ if (currentActive !== best.tabEl) {
338
+ setActive(best.tabEl, true);
339
+ }
340
+ }
341
+
342
+ function scheduleCompute() {
343
+ if (rafId) return;
344
+ rafId = window.requestAnimationFrame(() => {
345
+ rafId = 0;
346
+ computeActiveByViewportCenter();
347
+ });
348
+ }
349
+
350
+ function startSpy() {
351
+ if (buildTargets().length === 0) return false;
352
+
353
+ // Keep it simple: scroll/resize-driven, rAF-throttled
354
+ window.addEventListener('scroll', scheduleCompute, { passive: true, signal: ac.signal });
355
+ window.addEventListener('resize', scheduleCompute, { passive: true, signal: ac.signal });
356
+ mqDesktop.addEventListener('change', scheduleCompute, { signal: ac.signal });
357
+
358
+ // Initial sync
359
+ scheduleCompute();
360
+ return true;
361
+ }
362
+
363
+ function tryStartSpyWithWait() {
364
+ if (startSpy()) return;
365
+
366
+ // Targets not in DOM yet: wait for insertion
367
+ if (domObserver) return;
368
+ domObserver = new MutationObserver(() => {
369
+ if (startSpy()) {
370
+ domObserver.disconnect();
371
+ domObserver = null;
372
+ }
373
+ });
374
+
375
+ const root = document.body || document.documentElement;
376
+ if (root) {
377
+ domObserver.observe(root, { childList: true, subtree: true });
378
+ }
379
+
380
+ // Also retry a few times in case mutations are not observed (e.g. large async render)
381
+ let attempts = 0;
382
+ const maxAttempts = 20;
383
+ const tick = () => {
384
+ if (startSpy()) {
385
+ if (domObserver) domObserver.disconnect();
386
+ domObserver = null;
387
+ return;
388
+ }
389
+ attempts += 1;
390
+ if (attempts >= maxAttempts) return;
391
+ window.setTimeout(tick, 150);
392
+ };
393
+ window.setTimeout(tick, 0);
394
+ }
395
+
396
+ // If this script runs before the rest of the page modules mount, defer spy start.
397
+ if (document.readyState === 'loading') {
398
+ document.addEventListener(
399
+ 'DOMContentLoaded',
400
+ () => {
401
+ tryStartSpyWithWait();
402
+ },
403
+ { once: true, signal: ac.signal }
404
+ );
405
+ } else {
406
+ tryStartSpyWithWait();
407
+ }
408
+
409
+ sectionEl.__zzProductSubnavCleanup = () => {
410
+ ac.abort();
411
+ if (rafId) window.cancelAnimationFrame(rafId);
412
+ rafId = 0;
413
+ if (domObserver) domObserver.disconnect();
414
+ domObserver = null;
415
+ };
416
+ })();
417
+ </script>