zz-shopify-components 0.35.1-beta.6 → 0.35.1-beta.7
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.
|
@@ -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
|
@@ -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>
|