zz-shopify-components 0.3.1-beta.1 → 0.3.1-beta.3

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.
@@ -0,0 +1,395 @@
1
+ /*
2
+ - Uses <dialog> when available for native a11y and focus management
3
+ - Shadow DOM for internal styles; slots for user content (tailwind utilities apply to slotted nodes)
4
+ - Attribute reflection and programmatic API: show(), hide(), toggle()
5
+ - Options via attributes: close-on-esc, close-on-backdrop, scroll-lock, inert-others
6
+ - Global open/close triggers: [data-zz-modal-target], [data-zz-modal-close]
7
+ */
8
+
9
+ (() => {
10
+ if (customElements.get('zz-modal')) return;
11
+
12
+ const STYLE_TEXT = `
13
+ :host {
14
+ position: relative;
15
+ display: contents; /* do not affect layout where placed */
16
+ }
17
+
18
+ dialog[part="dialog"] {
19
+ border: none;
20
+ padding: 0;
21
+ margin: 0;
22
+ width: auto;
23
+ max-width: var(--zz-modal-max-width, 90vw);
24
+ max-height: var(--zz-modal-max-height, 85vh);
25
+ background: transparent; /* panel carries background */
26
+ overflow: visible;
27
+ }
28
+
29
+ /* Backdrop for native <dialog>. */
30
+ dialog[part="dialog"]::backdrop {
31
+ background: var(--zz-modal-backdrop, rgba(0,0,0,0.5));
32
+ backdrop-filter: var(--zz-modal-backdrop-filter, blur(0px));
33
+ }
34
+
35
+ .panel {
36
+ position: fixed;
37
+ inset: 0;
38
+ z-index: var(--zz-modal-z-index, 9999);
39
+ display: grid;
40
+ place-items: center;
41
+ pointer-events: none; /* enable backdrop clicks via dialog */
42
+ }
43
+
44
+ .panel-inner {
45
+ pointer-events: auto;
46
+ background: var(--zz-modal-background, #ffffff);
47
+ color: inherit;
48
+ border-radius: var(--zz-modal-radius, 12px);
49
+ box-shadow: var(--zz-modal-shadow, 0 20px 60px rgba(0,0,0,0.2));
50
+ width: var(--zz-modal-width, min(720px, 92vw));
51
+ max-height: var(--zz-modal-max-height, 85vh);
52
+ display: flex;
53
+ flex-direction: column;
54
+ overflow: hidden;
55
+ transform-origin: center center;
56
+ opacity: 0;
57
+ transform: translateY(8px) scale(0.98);
58
+ transition: none;
59
+ }
60
+
61
+ :host([open]) .panel-inner {
62
+ opacity: 1;
63
+ transform: translateY(0) scale(1);
64
+ }
65
+
66
+ @media (prefers-reduced-motion: no-preference) {
67
+ .panel-inner {
68
+ transition: opacity 160ms ease, transform 160ms ease;
69
+ }
70
+ }
71
+
72
+ .header, .footer {
73
+ padding: var(--zz-modal-padding, 16px);
74
+ flex: 0 0 auto;
75
+ }
76
+ :host([no-header]) .header, :host([no-header-auto]) .header { display: none; padding: 0; }
77
+ :host([no-footer]) .footer, :host([no-footer-auto]) .footer { display: none; padding: 0; }
78
+ .body {
79
+ padding: var(--zz-modal-padding, 16px);
80
+ overflow: auto;
81
+ flex: 1 1 auto;
82
+ }
83
+ .close-btn {
84
+ all: unset;
85
+ cursor: pointer;
86
+ position: absolute;
87
+ top: 8px;
88
+ right: 8px;
89
+ width: 32px;
90
+ height: 32px;
91
+ border-radius: 9999px;
92
+ display: grid;
93
+ place-items: center;
94
+ }
95
+ .close-btn:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
96
+ `;
97
+
98
+ const TEMPLATE = document.createElement('template');
99
+ TEMPLATE.innerHTML = `
100
+ <dialog part="dialog" aria-modal="true">
101
+ <div class="panel" part="backdrop">
102
+ <div class="panel-inner" role="document" part="panel">
103
+ <button class="close-btn" part="close-button" aria-label="Close" data-zz-modal-close>
104
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
105
+ </button>
106
+ <header class="header" part="header"><slot name="header"></slot></header>
107
+ <section class="body" part="body"><slot></slot></section>
108
+ <footer class="footer" part="footer"><slot name="footer"></slot></footer>
109
+ </div>
110
+ </div>
111
+ </dialog>
112
+ `;
113
+
114
+ class ZZModal extends HTMLElement {
115
+ static get observedAttributes() { return ['open']; }
116
+
117
+ constructor() {
118
+ super();
119
+ this._onKeydown = this._onKeydown.bind(this);
120
+ this._onNativeClose = this._onNativeClose.bind(this);
121
+ this._onMouseDown = this._onMouseDown.bind(this);
122
+ this._onClick = this._onClick.bind(this);
123
+ this._previousActive = null;
124
+ this._mouseDownInsidePanel = false;
125
+
126
+ const shadow = this.attachShadow({ mode: 'open' });
127
+ const style = document.createElement('style');
128
+ style.textContent = STYLE_TEXT;
129
+ shadow.appendChild(style);
130
+ shadow.appendChild(TEMPLATE.content.cloneNode(true));
131
+
132
+ this._dialog = shadow.querySelector('dialog');
133
+ this._panel = shadow.querySelector('.panel-inner');
134
+ this._closeBtn = shadow.querySelector('[data-zz-modal-close]');
135
+ this._slotHeader = shadow.querySelector('slot[name="header"]');
136
+ this._slotFooter = shadow.querySelector('slot[name="footer"]');
137
+ }
138
+
139
+ connectedCallback() {
140
+ // Delegated internal events
141
+ this._closeBtn.addEventListener('click', () => this.hide());
142
+ this._dialog.addEventListener('close', this._onNativeClose);
143
+ this._dialog.addEventListener('mousedown', this._onMouseDown);
144
+ this._dialog.addEventListener('click', this._onClick);
145
+
146
+ // Reflect open state if attribute present at mount
147
+ if (this.hasAttribute('open')) {
148
+ // Defer to ensure upgraded before open
149
+ queueMicrotask(() => this.show());
150
+ }
151
+
152
+ // Auto hide empty header/footer
153
+ this._slotHeader.addEventListener('slotchange', () => this._updateSlotVisibility());
154
+ this._slotFooter.addEventListener('slotchange', () => this._updateSlotVisibility());
155
+ // Initial
156
+ this._updateSlotVisibility();
157
+ }
158
+
159
+ disconnectedCallback() {
160
+ this._dialog.removeEventListener('close', this._onNativeClose);
161
+ this._dialog.removeEventListener('mousedown', this._onMouseDown);
162
+ this._dialog.removeEventListener('click', this._onClick);
163
+ document.removeEventListener('keydown', this._onKeydown);
164
+ }
165
+
166
+ attributeChangedCallback(name, oldValue, newValue) {
167
+ if (name === 'open' && oldValue !== newValue) {
168
+ if (this.hasAttribute('open')) {
169
+ this.show();
170
+ } else {
171
+ this.hide();
172
+ }
173
+ }
174
+ }
175
+
176
+ get open() { return this._dialog?.open || this.hasAttribute('open'); }
177
+ set open(val) { if (val) this.setAttribute('open', ''); else this.removeAttribute('open'); }
178
+
179
+ show() { this._openInternal(true); }
180
+ hide() { this._closeInternal(true); }
181
+ toggle() { this.open ? this.hide() : this.show(); }
182
+
183
+ // Aliases
184
+ showModal() { this.show(); }
185
+ close() { this.hide(); }
186
+
187
+ _emit(name, detail = {}) {
188
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
189
+ }
190
+
191
+ _openInternal(emit = false) {
192
+ if (this._dialog.open) return;
193
+ this._previousActive = document.activeElement;
194
+
195
+ // Scroll lock
196
+ if (!this.hasAttribute('no-scroll-lock')) {
197
+ this._lockScroll();
198
+ }
199
+
200
+ // Inert others
201
+ if (this.hasAttribute('inert-others')) {
202
+ this._toggleInert(true);
203
+ }
204
+
205
+ // Native dialog path
206
+ try {
207
+ if ('showModal' in HTMLDialogElement.prototype) {
208
+ this._dialog.showModal();
209
+ } else {
210
+ // Fallback
211
+ this._dialog.setAttribute('open', '');
212
+ }
213
+ } catch (_) {
214
+ this._dialog.setAttribute('open', '');
215
+ }
216
+
217
+ // Sync attribute for CSS animations
218
+ this.setAttribute('open', '');
219
+
220
+ // Listeners
221
+ document.addEventListener('keydown', this._onKeydown);
222
+
223
+ // Focus first focusable within panel
224
+ this._focusInitial();
225
+
226
+ if (emit) this._emit('zz-modal:open');
227
+ }
228
+
229
+ _closeInternal(emit = false) {
230
+ if (!this._dialog.open && !this.hasAttribute('open')) return;
231
+
232
+ // Remove listeners
233
+ document.removeEventListener('keydown', this._onKeydown);
234
+
235
+ // Native dialog close
236
+ try {
237
+ if (this._dialog.open) this._dialog.close();
238
+ } catch (_) {
239
+ // ignore
240
+ }
241
+ this.removeAttribute('open');
242
+
243
+ // Restore scroll and inert
244
+ if (!this.hasAttribute('no-scroll-lock')) {
245
+ this._unlockScroll();
246
+ }
247
+ if (this.hasAttribute('inert-others')) {
248
+ this._toggleInert(false);
249
+ }
250
+
251
+ // Restore focus
252
+ if (this._previousActive && typeof this._previousActive.focus === 'function') {
253
+ this._previousActive.focus({ preventScroll: true });
254
+ }
255
+ this._previousActive = null;
256
+
257
+ if (emit) this._emit('zz-modal:close');
258
+ }
259
+
260
+ _onKeydown(e) {
261
+ const escAllowed = !this.hasAttribute('no-esc-close');
262
+ if (e.key === 'Escape' && escAllowed) {
263
+ e.stopPropagation();
264
+ this.hide();
265
+ }
266
+
267
+ // Basic focus trap for fallback scenario
268
+ if (e.key === 'Tab' && !('showModal' in HTMLDialogElement.prototype)) {
269
+ const focusables = this._getFocusable();
270
+ if (focusables.length === 0) return;
271
+ const first = focusables[0];
272
+ const last = focusables[focusables.length - 1];
273
+ const active = this.shadowRoot.activeElement || document.activeElement;
274
+ if (e.shiftKey) {
275
+ if (active === first || !this.contains(active)) {
276
+ e.preventDefault();
277
+ last.focus();
278
+ }
279
+ } else {
280
+ if (active === last || !this.contains(active)) {
281
+ e.preventDefault();
282
+ first.focus();
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ _onNativeClose() {
289
+ // Ensure attribute sync and cleanup
290
+ this._closeInternal(true);
291
+ }
292
+
293
+ _onMouseDown(e) {
294
+ // Track whether mousedown originated inside the panel to differentiate drags
295
+ const path = e.composedPath();
296
+ this._mouseDownInsidePanel = path.includes(this._panel);
297
+ }
298
+
299
+ _onClick(e) {
300
+ const backdropClose = !this.hasAttribute('no-backdrop-close');
301
+ if (!backdropClose) return;
302
+
303
+ // If click ended outside panel and started outside panel -> treat as backdrop click
304
+ const path = e.composedPath();
305
+ const clickInsidePanel = path.includes(this._panel);
306
+ if (!clickInsidePanel && !this._mouseDownInsidePanel) {
307
+ this.hide();
308
+ }
309
+ this._mouseDownInsidePanel = false;
310
+ }
311
+
312
+ _updateSlotVisibility() {
313
+ try {
314
+ const hasHeader = (this._slotHeader.assignedNodes({ flatten: true }).filter(n => n.nodeType === 1 || (n.nodeType === 3 && n.textContent.trim())).length) > 0;
315
+ const hasFooter = (this._slotFooter.assignedNodes({ flatten: true }).filter(n => n.nodeType === 1 || (n.nodeType === 3 && n.textContent.trim())).length) > 0;
316
+ if (!hasHeader) this.setAttribute('no-header-auto', ''); else this.removeAttribute('no-header-auto');
317
+ if (!hasFooter) this.setAttribute('no-footer-auto', ''); else this.removeAttribute('no-footer-auto');
318
+ } catch (_) {
319
+ // ignore
320
+ }
321
+ }
322
+
323
+ _getFocusable() {
324
+ const root = this.shadowRoot;
325
+ const within = root.querySelector('.panel-inner');
326
+ return Array.from(within.querySelectorAll([
327
+ 'a[href]','area[href]','input:not([disabled])','select:not([disabled])','textarea:not([disabled])',
328
+ 'button:not([disabled])','iframe','object','embed','[contenteditable]','[tabindex]:not([tabindex="-1"])'
329
+ ].join(',')));
330
+ }
331
+
332
+ _focusInitial() {
333
+ const focusables = this._getFocusable();
334
+ if (focusables.length > 0) {
335
+ focusables[0].focus({ preventScroll: true });
336
+ } else {
337
+ // Focus the dialog so that ESC works consistently
338
+ this._dialog.focus({ preventScroll: true });
339
+ }
340
+ }
341
+
342
+ _lockScroll() {
343
+ const body = document.body;
344
+ this._prevBodyOverflow = body.style.overflow;
345
+ body.style.overflow = 'hidden';
346
+ body.setAttribute('data-zz-modal-open', '');
347
+ }
348
+
349
+ _unlockScroll() {
350
+ const body = document.body;
351
+ body.style.overflow = this._prevBodyOverflow || '';
352
+ body.removeAttribute('data-zz-modal-open');
353
+ }
354
+
355
+ _toggleInert(isInert) {
356
+ const toInert = [];
357
+ const root = document.body;
358
+ for (const child of Array.from(root.children)) {
359
+ if (child === this.parentElement) continue;
360
+ toInert.push(child);
361
+ }
362
+ toInert.forEach((el) => {
363
+ if (isInert) el.setAttribute('inert', ''); else el.removeAttribute('inert');
364
+ });
365
+ }
366
+ }
367
+
368
+ customElements.define('zz-modal', ZZModal);
369
+
370
+ // Global open/close triggers
371
+ document.addEventListener('click', (e) => {
372
+ const openTrigger = e.target.closest('[data-zz-modal-target]');
373
+ if (openTrigger) {
374
+ const selector = openTrigger.getAttribute('data-zz-modal-target');
375
+ if (selector) {
376
+ const modal = document.querySelector(selector);
377
+ if (modal && modal.show) {
378
+ e.preventDefault();
379
+ modal.show();
380
+ }
381
+ }
382
+ return;
383
+ }
384
+ const closeTrigger = e.target.closest('[data-zz-modal-close]');
385
+ if (closeTrigger) {
386
+ const hostModal = closeTrigger.closest('zz-modal');
387
+ if (hostModal && hostModal.hide) {
388
+ e.preventDefault();
389
+ hostModal.hide();
390
+ }
391
+ }
392
+ }, { capture: true });
393
+ })();
394
+
395
+
@@ -9,6 +9,16 @@
9
9
  "label": "按钮文字",
10
10
  "default": "按钮"
11
11
  },
12
+ {
13
+ "type": "text",
14
+ "id": "btn_id",
15
+ "label": "按钮id属性值",
16
+ },
17
+ {
18
+ "type": "text",
19
+ "id": "modal_id",
20
+ "label": "按钮触发modal的id",
21
+ },
12
22
  {
13
23
  "type": "select",
14
24
  "id": "function_type",
@@ -188,7 +198,9 @@
188
198
  icon_left_margin: block.settings.icon_left_margin,
189
199
  icon_right_margin: block.settings.icon_right_margin,
190
200
  width: block.settings.mobile_width,
191
- class_name: btn_class
201
+ class_name: btn_class,
202
+ btn_id: block.settings.btn_id,
203
+ modal_id: block.settings.modal_id
192
204
  %}
193
205
 
194
206
 
@@ -0,0 +1,170 @@
1
+ 轻量、高性能、可扩展、可访问(a11y)的 Web Component 弹窗,基于原生 `<dialog>` 能力并内置降级方案。
2
+
3
+ ### 特性
4
+ - 原生 `<dialog>`:内建焦点管理、`aria-modal`,退化时提供焦点陷阱与交互保障
5
+ - 高性能:零依赖、轻量动画、滚动锁、Backdrop 点击关闭、ESC 关闭
6
+ - 可扩展:Shadow DOM、`::part(...)`、CSS 变量、事件、编程式 API
7
+ - 易用:全局触发器 `[data-zz-modal-target]`、`[data-zz-modal-close]`
8
+
9
+ ---
10
+
11
+ ### 快速开始(Shopify / Theme Section)
12
+ 在 Section 中确保加载脚本(`sections/zz-modal.liquid` 已内置):
13
+
14
+ ```liquid
15
+ <script defer id="zz-modal-js" src="{{ 'zz-modal.js' | asset_url }}"></script>
16
+ ```
17
+
18
+ 无 JS 安全:
19
+ ```css
20
+ zz-modal:not(:defined) { display: none !important; }
21
+ ```
22
+
23
+ Section 中通过 CSS 变量响应式配置:
24
+ ```css
25
+ #your-modal-id {
26
+ --zz-modal-radius: {{ section.settings.radius_mobile }}px;
27
+ --zz-modal-padding: {{ section.settings.padding_mobile }}px;
28
+ --zz-modal-width: {{ section.settings.width_mobile }};
29
+ }
30
+ @media (min-width: 768px) {
31
+ #your-modal-id {
32
+ --zz-modal-radius: {{ section.settings.radius_pc }}px;
33
+ --zz-modal-padding: {{ section.settings.padding_pc }}px;
34
+ --zz-modal-width: {{ section.settings.width_pc }};
35
+ }
36
+ }
37
+ ```
38
+
39
+ Section 设置项(已实现):
40
+ - 文案:`button_text`
41
+ - Header/Footer 显示:`show_header`、`show_footer`
42
+ - 宽度:`width_mobile`、`width_pc`(支持 `px` 或 `vw`)
43
+ - 圆角:`radius_mobile`、`radius_pc`(单位 px)
44
+ - 内边距:`padding_mobile`、`padding_pc`(单位 px)
45
+
46
+ > 当 slot 为空时,组件会自动隐藏 Header/Footer(属性 `no-header-auto` / `no-footer-auto`)。
47
+
48
+ ---
49
+
50
+ ### 基本用法
51
+
52
+ 触发按钮 + 弹窗结构:
53
+ ```html
54
+ <button data-zz-modal-target="#demo-modal">Open Modal</button>
55
+
56
+ <zz-modal id="demo-modal">
57
+ <div slot="header">标题</div>
58
+ <div>这里是内容</div>
59
+ <div slot="footer">
60
+ <button data-zz-modal-close>取消</button>
61
+ <button data-zz-modal-close>确定</button>
62
+ </div>
63
+ <!-- 内置关闭按钮也可用:shadow 内提供 part="close-button" 按钮 -->
64
+ <!-- 任何带 data-zz-modal-close 的元素都会关闭当前弹窗 -->
65
+ <!-- 任意元素 data-zz-modal-target="#id" 可打开对应弹窗 -->
66
+ ```
67
+
68
+ ---
69
+
70
+ ### 编程式 API
71
+ ```js
72
+ const modal = document.querySelector('#demo-modal');
73
+ modal.show(); // 打开
74
+ modal.hide(); // 关闭
75
+ modal.toggle(); // 切换
76
+ // 等价别名:modal.showModal() / modal.close()
77
+ ```
78
+
79
+ ---
80
+
81
+ ### 属性(Attributes)
82
+ - `open`:受控显示状态(加上即显示,移除即关闭)
83
+ - `no-esc-close`:禁用 ESC 关闭
84
+ - `no-backdrop-close`:禁用点击遮罩关闭
85
+ - `no-scroll-lock`:禁用页面滚动锁
86
+ - `inert-others`:打开时让页面其它区域 inert(更强的可访问隔离)
87
+ - `no-header` / `no-footer`:强制隐藏 Header/Footer(无视 slot 内容)
88
+ - `no-header-auto` / `no-footer-auto`:组件自动加的属性,用于在对应 slot 为空时隐藏区域
89
+
90
+ ---
91
+
92
+ ### 事件(Events)
93
+ - `zz-modal:open`
94
+ - `zz-modal:close`
95
+
96
+ 监听示例:
97
+ ```js
98
+ document.querySelector('#demo-modal')
99
+ .addEventListener('zz-modal:open', () => console.log('opened'));
100
+ ```
101
+
102
+ ---
103
+
104
+ ### 样式定制
105
+
106
+ CSS 变量:
107
+ - `--zz-modal-width`(默认 `min(720px, 92vw)`)
108
+ - `--zz-modal-max-width`(默认 `90vw`)
109
+ - `--zz-modal-max-height`(默认 `85vh`)
110
+ - `--zz-modal-radius`(默认 `12px`)
111
+ - `--zz-modal-padding`(默认 `16px`)
112
+ - `--zz-modal-background`(默认 `#fff`)
113
+ - `--zz-modal-shadow`(默认 `0 20px 60px rgba(0,0,0,0.2)`)
114
+ - `--zz-modal-backdrop`(默认 `rgba(0,0,0,0.5)`)
115
+ - `--zz-modal-backdrop-filter`(默认 `blur(0px)`)
116
+ - `--zz-modal-z-index`(默认 `9999`)
117
+
118
+ Shadow Parts:
119
+ - `dialog`、`backdrop`、`panel`、`header`、`body`、`footer`、`close-button`
120
+
121
+ 示例:
122
+ ```css
123
+ /* 外部覆盖内部部件样式 */
124
+ zz-modal::part(dialog) { max-width: 720px; }
125
+ zz-modal::part(panel) { border-radius: 16px; }
126
+
127
+ /* 使用变量 */
128
+ #demo-modal { --zz-modal-background: #111; color: #fff; }
129
+ ```
130
+
131
+ ---
132
+
133
+ ### 可访问性(a11y)
134
+ - 原生 `<dialog>` 优先,具备 `aria-modal`,ESC 关闭、焦点管理
135
+ - 降级路径提供基本焦点陷阱与键盘导航保障
136
+ - `inert-others` 可在打开时禁用页面其他区域交互
137
+
138
+ ---
139
+
140
+ ### 无 JS 安全
141
+ 网络不佳或脚本未加载时隐藏 Light DOM,避免内容外露:
142
+ ```css
143
+ zz-modal:not(:defined) { display: none !important; }
144
+ ```
145
+
146
+ ---
147
+
148
+ ### 多实例
149
+ ```html
150
+ <button data-zz-modal-target="#m1">打开 1</button>
151
+ <button data-zz-modal-target="#m2">打开 2</button>
152
+
153
+ <zz-modal id="m1">...</zz-modal>
154
+ <zz-modal id="m2">...</zz-modal>
155
+ ```
156
+
157
+ ---
158
+
159
+ ### 常见问题
160
+ - 遮罩点击无效?检查是否设置了 `no-backdrop-close`。
161
+ - 页面滚动未锁定?移除 `no-scroll-lock` 或确认 CSS 未覆盖 `overflow`。
162
+ - Header/Footer 不显示?若未显式设置 `show_header/show_footer`,请确保 slot 有内容;或移除 `no-header/no-footer`。
163
+
164
+ ---
165
+
166
+ ### 变更位置
167
+ - 组件实现:`assets/zz-modal.js`
168
+ - Section:`sections/zz-modal.liquid`
169
+
170
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-shopify-components",
3
- "version": "0.3.1-beta.1",
3
+ "version": "0.3.1-beta.3",
4
4
  "description": "Reusable Shopify components for theme projects",
5
5
  "keywords": [
6
6
  "shopify",
@@ -0,0 +1,147 @@
1
+ <style>
2
+ zz-modal:not(:defined) { display: none !important; }
3
+ /* Responsive modal variables from section settings */
4
+ #{{ section.settings.modal_id }} {
5
+ --zz-modal-radius: {{ section.settings.radius_mobile }}px;
6
+ --zz-modal-padding: {{ section.settings.padding_mobile }}px;
7
+ --zz-modal-width: {{ section.settings.width_mobile }};
8
+ }
9
+ @media (min-width: 768px) {
10
+ #{{ section.settings.modal_id }} {
11
+ --zz-modal-radius: {{ section.settings.radius_pc }}px;
12
+ --zz-modal-padding: {{ section.settings.padding_pc }}px;
13
+ --zz-modal-width: {{ section.settings.width_pc }};
14
+ }
15
+ }
16
+ </style>
17
+
18
+ <zz-modal {% if section.settings.no_backdrop_close %} no-backdrop-close {% endif %} id="{{ section.settings.modal_id }}"{% unless section.settings.show_header %} no-header{% endunless %}{% unless section.settings.show_footer %} no-footer{% endunless %}>
19
+ {% if section.settings.show_header %}
20
+ <div slot="header" class="tw-text-[24px] tw-font-semibold">{{ section.settings.header_text }}</div>
21
+ {% endif %}
22
+ {% content_for 'blocks' %}
23
+ {% if section.settings.show_footer %}
24
+ <div slot="footer" class="tw-flex tw-justify-end tw-gap-2 tw-pt-8">
25
+ <button class=" tw-text-[16px] " data-zz-modal-close>{{ section.settings.footer_text }}</button>
26
+ </div>
27
+ {% endif %}
28
+ </zz-modal>
29
+
30
+ <script>
31
+ (function(){
32
+ var id = 'zz-modal-js';
33
+ if (!document.getElementById(id)) {
34
+ var s = document.createElement('script');
35
+ s.id = id;
36
+ s.src = '{{ 'zz-modal.js' | asset_url }}';
37
+ s.defer = true;
38
+ document.head.appendChild(s);
39
+ }
40
+ })();
41
+ </script>
42
+
43
+ {% schema %}
44
+ {
45
+ "name": "ZZ Modal",
46
+ "settings": [
47
+ {
48
+ "type": "text",
49
+ "id": "modal_id",
50
+ "label": "弹窗id (必填)",
51
+ "default": "demo-modal"
52
+ },
53
+ {
54
+ "type": "checkbox",
55
+ "id": "show_header",
56
+ "label": "显示 Header",
57
+ "default": false
58
+ },
59
+ {
60
+ "type": "checkbox",
61
+ "id": "show_footer",
62
+ "label": "显示 Footer",
63
+ "default": false
64
+ },
65
+ {
66
+ "type": "checkbox",
67
+ "id": "no_backdrop_close",
68
+ "label": "是否禁用遮罩点击关闭",
69
+ "default": false
70
+ },
71
+ {
72
+ "type": "text",
73
+ "id": "header_text",
74
+ "label": "弹窗标题",
75
+ "default": "弹窗标题"
76
+ },
77
+ {
78
+ "type": "text",
79
+ "id": "width_mobile",
80
+ "label": "移动端宽度(如 92vw 或 360px)",
81
+ "default": "92vw"
82
+ },
83
+ {
84
+ "type": "text",
85
+ "id": "width_pc",
86
+ "label": "PC 端宽度(如 720px 或 60vw)",
87
+ "default": "720px"
88
+ },
89
+ {
90
+ "type": "range",
91
+ "id": "radius_mobile",
92
+ "label": "移动端圆角(px)",
93
+ "min": 0,
94
+ "max": 40,
95
+ "step": 1,
96
+ "default": 12
97
+ },
98
+ {
99
+ "type": "range",
100
+ "id": "radius_pc",
101
+ "label": "PC 端圆角(px)",
102
+ "min": 0,
103
+ "max": 40,
104
+ "step": 1,
105
+ "default": 12
106
+ },
107
+ {
108
+ "type": "range",
109
+ "id": "padding_mobile",
110
+ "label": "移动端内边距(px)",
111
+ "min": 0,
112
+ "max": 48,
113
+ "step": 2,
114
+ "default": 16
115
+ },
116
+ {
117
+ "type": "range",
118
+ "id": "padding_pc",
119
+ "label": "PC 端内边距(px)",
120
+ "min": 0,
121
+ "max": 64,
122
+ "step": 2,
123
+ "default": 20
124
+ },
125
+ {
126
+ "type": "text",
127
+ "id": "footer_text",
128
+ "label": "弹窗底部按钮文案",
129
+ "default": "Close"
130
+ }
131
+ ],
132
+ "blocks": [
133
+ {
134
+ "type": "@theme",
135
+ }
136
+ ],
137
+ "presets": [
138
+ {
139
+ "name": "ZZ Modal",
140
+ "category": "Custom"
141
+ }
142
+ ]
143
+ }
144
+ {% endschema %}
145
+
146
+
147
+
@@ -143,9 +143,6 @@
143
143
  <div class='swiper zz-shopping-cart-list-swiper'>
144
144
  <div class='swiper-wrapper'>
145
145
  {% for block in section.blocks %}
146
- {%- comment %}<locksmith:881a>{% endcomment -%}
147
- {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: block.settings.product, subject_parent: section, variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% else %}{% continue %}{% endif -%}
148
- {%- comment %}</locksmith:881a>{% endcomment -%}
149
146
  <div class='swiper-slide {% if block_size > 1 %} max-lg:tw-pb-[60px] {% endif %}'>
150
147
  <div class='banner-item tw-rounded-[10px] lg:tw-rounded-[16px] tw-flex tw-flex-col tw-justify-between'>
151
148
  <div class='tw-h-[168px] lg:tw-h-[214px] tw-overflow-hidden tw-rounded-t-[10px] lg:tw-rounded-t-[16px] tw-flex-shrink-0'>
@@ -243,9 +243,6 @@
243
243
  <div class="collapse-title-desc fade-in-box tw-flex tw-flex-col tw-justify-center">
244
244
 
245
245
  {% for block in non_collapsible_blocks %}
246
- {%- comment %}<locksmith:0b82>{% endcomment -%}
247
- {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: block, variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% else %}{% continue %}{% endif -%}
248
- {%- comment %}</locksmith:0b82>{% endcomment -%}
249
246
  {% case block.type %}
250
247
  {% when 'title' %}
251
248
  {% render 'zz-h2',
@@ -292,9 +289,6 @@
292
289
  <div class="tw-w-[68%]">
293
290
  {% assign swiper_blocks = section.blocks | where: 'type', 'collapsible_row' %}
294
291
  {% for collapsible_row in swiper_blocks %}
295
- {%- comment %}<locksmith:9ed7>{% endcomment -%}
296
- {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: collapsible_row, variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% else %}{% continue %}{% endif -%}
297
- {%- comment %}</locksmith:9ed7>{% endcomment -%}
298
292
  <div class='collasps-video-box tw-hidden tw-w-full tw-rounded-[16px] tw-overflow-hidden'>
299
293
  {% comment %} pc端配置视频 {% endcomment %}
300
294
  {% if collapsible_row.settings.video_pc != blank %}
@@ -322,15 +316,9 @@
322
316
  </div>
323
317
  <div class="tw-w-[27%]">
324
318
  <div class="tw-daisy-join tw-daisy-join-vertical tw-w-full">
325
- {%- comment %}<locksmith:8575>{% endcomment -%}
326
- {%- assign locksmith_a30a_forloop__size = 0 %}{% for collapsible_row in swiper_blocks %}{% capture var %}{% render 'locksmith-variables', scope: 'subject', subject: collapsible_row, variable: 'transparent' %}{% endcapture %}{% if var == 'true' %}{% assign locksmith_a30a_forloop__size = locksmith_a30a_forloop__size | plus: 1 %}{% endif %}{% endfor %}{% assign locksmith_a30a_forloop__index = nil -%}
327
- {%- comment %}</locksmith:8575>{% endcomment -%}
328
319
  {% for collapsible_row in swiper_blocks %}
329
- {%- comment %}<locksmith:7221>{% endcomment -%}
330
- {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: collapsible_row, variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% if locksmith_a30a_forloop__index == nil %}{% assign locksmith_a30a_forloop__index = 1 %}{% assign locksmith_a30a_forloop__index0 = 0 %}{% else %}{% assign locksmith_a30a_forloop__index = locksmith_a30a_forloop__index | plus: 1 %}{% assign locksmith_a30a_forloop__index0 = locksmith_a30a_forloop__index0 | plus: 1 %}{% endif %}{% if locksmith_a30a_forloop__index == 1 %}{% assign locksmith_a30a_forloop__first = true %}{% else %}{% assign locksmith_a30a_forloop__first = false %}{% endif %}{% if locksmith_a30a_forloop__index == locksmith_a30a_forloop__size %}{% assign locksmith_a30a_forloop__last = true %}{% else %}{% assign locksmith_a30a_forloop__last = false %}{% endif %}{% assign locksmith_a30a_forloop__rindex = locksmith_a30a_forloop__size | minus: locksmith_a30a_forloop__index | minus: 1 %}{% assign locksmith_a30a_forloop__rindex0 = locksmith_a30a_forloop__size | minus: locksmith_a30a_forloop__index0 | minus: 1 %}{% else %}{% continue %}{% endif -%}
331
- {%- comment %}</locksmith:7221>{% endcomment -%}
332
320
  <div class="tw-daisy-collapse collapse-arrow tw-daisy-join-item ">
333
- <input type="radio" name="my-accordion-{{ section.id }}" value="{{ locksmith_a30a_forloop__index }}" {% if locksmith_a30a_forloop__first %}checked data-last-checked="true"{% endif %} onclick="handleFAQClick(this)" style="cursor: pointer;" />
321
+ <input type="radio" name="my-accordion-{{ section.id }}" value="{{ forloop.index }}" {% if forloop.first %}checked data-last-checked="true"{% endif %} onclick="handleFAQClick(this)" style="cursor: pointer;" />
334
322
  <div class="tw-daisy-collapse-title tw-py-[20px] tw-px-0 tw-text-[20px] tw-font-[700] tw-leading-[1]" style="color: {{ section.settings.tab_title_active_color }};">
335
323
  {{ collapsible_row.settings.heading }}
336
324
  </div>
@@ -349,9 +337,6 @@
349
337
 
350
338
  <div class='tw-px-[20px] tw-py-[20px] lg:tw-hidden'>
351
339
  {% for collapsible_row in swiper_blocks %}
352
- {%- comment %}<locksmith:7587>{% endcomment -%}
353
- {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: collapsible_row, variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% else %}{% continue %}{% endif -%}
354
- {%- comment %}</locksmith:7587>{% endcomment -%}
355
340
  <div class='video-box tw-hidden tw-rounded-[10px] tw-overflow-hidden'>
356
341
  {% comment %} 移动端配置视频 {% endcomment %}
357
342
  {% if collapsible_row.settings.video_mb != blank %}
@@ -380,17 +365,11 @@
380
365
  <div
381
366
  class='tabs tw-flex tw-justify-between tw-w-full tw-min-w-[320px] tw-max-w-[480px] tw-px-[16px] tw-rounded-[16px] tw-items-center tw-h-[32px] tw-mb-[12px]'
382
367
  style="background-color: {{ section.settings.tab_bar_bg_color }};">
383
- {%- comment %}<locksmith:ec8e>{% endcomment -%}
384
- {%- assign locksmith_1f74_forloop__size = 0 %}{% for collapsible_row in swiper_blocks %}{% capture var %}{% render 'locksmith-variables', scope: 'subject', subject: collapsible_row, variable: 'transparent' %}{% endcapture %}{% if var == 'true' %}{% assign locksmith_1f74_forloop__size = locksmith_1f74_forloop__size | plus: 1 %}{% endif %}{% endfor %}{% assign locksmith_1f74_forloop__index = nil -%}
385
- {%- comment %}</locksmith:ec8e>{% endcomment -%}
386
368
  {% for collapsible_row in swiper_blocks %}
387
- {%- comment %}<locksmith:6093>{% endcomment -%}
388
- {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: collapsible_row, variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% if locksmith_1f74_forloop__index == nil %}{% assign locksmith_1f74_forloop__index = 1 %}{% assign locksmith_1f74_forloop__index0 = 0 %}{% else %}{% assign locksmith_1f74_forloop__index = locksmith_1f74_forloop__index | plus: 1 %}{% assign locksmith_1f74_forloop__index0 = locksmith_1f74_forloop__index0 | plus: 1 %}{% endif %}{% if locksmith_1f74_forloop__index == 1 %}{% assign locksmith_1f74_forloop__first = true %}{% else %}{% assign locksmith_1f74_forloop__first = false %}{% endif %}{% if locksmith_1f74_forloop__index == locksmith_1f74_forloop__size %}{% assign locksmith_1f74_forloop__last = true %}{% else %}{% assign locksmith_1f74_forloop__last = false %}{% endif %}{% assign locksmith_1f74_forloop__rindex = locksmith_1f74_forloop__size | minus: locksmith_1f74_forloop__index | minus: 1 %}{% assign locksmith_1f74_forloop__rindex0 = locksmith_1f74_forloop__size | minus: locksmith_1f74_forloop__index0 | minus: 1 %}{% else %}{% continue %}{% endif -%}
389
- {%- comment %}</locksmith:6093>{% endcomment -%}
390
369
  <div
391
370
  class='tab-item tw-cursor-pointer tw-text-[10px] tw-font-[500] hover:tw-border-white'
392
371
  style="color: {{ section.settings.tab_title_color }};"
393
- data-index='{{ locksmith_1f74_forloop__index0 }}'
372
+ data-index='{{ forloop.index0 }}'
394
373
  >
395
374
  {{ collapsible_row.settings.heading }}
396
375
  </div>
@@ -398,15 +377,9 @@
398
377
  </div>
399
378
 
400
379
  <div class='video-content '>
401
- {%- comment %}<locksmith:a1fa>{% endcomment -%}
402
- {%- assign locksmith_3ef6_forloop__size = 0 %}{% for collapsible_row in swiper_blocks %}{% capture var %}{% render 'locksmith-variables', scope: 'subject', subject: collapsible_row, variable: 'transparent' %}{% endcapture %}{% if var == 'true' %}{% assign locksmith_3ef6_forloop__size = locksmith_3ef6_forloop__size | plus: 1 %}{% endif %}{% endfor %}{% assign locksmith_3ef6_forloop__index = nil -%}
403
- {%- comment %}</locksmith:a1fa>{% endcomment -%}
404
380
  {% for collapsible_row in swiper_blocks %}
405
- {%- comment %}<locksmith:7a77>{% endcomment -%}
406
- {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: collapsible_row, variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% if locksmith_3ef6_forloop__index == nil %}{% assign locksmith_3ef6_forloop__index = 1 %}{% assign locksmith_3ef6_forloop__index0 = 0 %}{% else %}{% assign locksmith_3ef6_forloop__index = locksmith_3ef6_forloop__index | plus: 1 %}{% assign locksmith_3ef6_forloop__index0 = locksmith_3ef6_forloop__index0 | plus: 1 %}{% endif %}{% if locksmith_3ef6_forloop__index == 1 %}{% assign locksmith_3ef6_forloop__first = true %}{% else %}{% assign locksmith_3ef6_forloop__first = false %}{% endif %}{% if locksmith_3ef6_forloop__index == locksmith_3ef6_forloop__size %}{% assign locksmith_3ef6_forloop__last = true %}{% else %}{% assign locksmith_3ef6_forloop__last = false %}{% endif %}{% assign locksmith_3ef6_forloop__rindex = locksmith_3ef6_forloop__size | minus: locksmith_3ef6_forloop__index | minus: 1 %}{% assign locksmith_3ef6_forloop__rindex0 = locksmith_3ef6_forloop__size | minus: locksmith_3ef6_forloop__index0 | minus: 1 %}{% else %}{% continue %}{% endif -%}
407
- {%- comment %}</locksmith:7a77>{% endcomment -%}
408
381
  <div
409
- class='content-item row-content tw-hidden tw-font-[500] tw-leading-[1.5] tw-text-center' data-index='{{ locksmith_3ef6_forloop__index0 }}'
382
+ class='content-item row-content tw-hidden tw-font-[500] tw-leading-[1.5] tw-text-center' data-index='{{ forloop.index0 }}'
410
383
  style="color: {{ section.settings.collapsible_text_color }};"
411
384
  >
412
385
  {{ collapsible_row.settings.row_content }}
@@ -175,9 +175,6 @@
175
175
  >
176
176
  <div class='fade-in-box tw-flex tw-flex-col tw-justify-center'>
177
177
  {% for block in non_collapsible_blocks %}
178
- {%- comment %}<locksmith:ad09>{% endcomment -%}
179
- {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: block, variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% else %}{% continue %}{% endif -%}
180
- {%- comment %}</locksmith:ad09>{% endcomment -%}
181
178
  {% case block.type %}
182
179
  {% when 'title' %}
183
180
  {% render 'zz-h2',
@@ -223,9 +220,6 @@
223
220
  >
224
221
  <div class='swiper-wrapper'>
225
222
  {% for banner in swiper_blocks %}
226
- {%- comment %}<locksmith:6e8b>{% endcomment -%}
227
- {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: banner, variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% else %}{% continue %}{% endif -%}
228
- {%- comment %}</locksmith:6e8b>{% endcomment -%}
229
223
  <div class='swiper-slide'>
230
224
  <div class='media-box tw-rounded-[10px] lg:tw-rounded-[16px] tw-overflow-hidden'>
231
225
  {% if banner.settings.video_pc != blank or banner.settings.video_url_pc != blank %}
@@ -285,9 +279,6 @@
285
279
  // 添加标题数组
286
280
  const slideTitles = [
287
281
  {% for block in swiper_blocks %}
288
- {%- comment %}<locksmith:41d3>{% endcomment -%}
289
- {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: block, variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% else %}{% continue %}{% endif -%}
290
- {%- comment %}</locksmith:41d3>{% endcomment -%}
291
282
  "{{ block.settings.slide_title }}",
292
283
  {% endfor %}
293
284
  ];
@@ -38,7 +38,7 @@ size:按钮尺寸
38
38
 
39
39
  {% if href %}
40
40
  {% if btn_type == 'link' %}
41
- <a href="{{ href | default: '#' }}" class="zz-btn-link tw-text-[#378DDD] tw-inline-flex tw-items-center tw-text-[12px] lg:tw-text-[14px] tw-no-underline zz-btn-link-{{ btn_size }} {% if width == 'full' %} tw-w-full {% endif %} {{ class_name }}">
41
+ <a href="{{ href | default: '#' }}" {% if btn_id != blank %} id="{{ btn_id }}" {% endif %} class="zz-btn-link tw-text-[#378DDD] tw-inline-flex tw-items-center tw-text-[12px] lg:tw-text-[14px] tw-no-underline zz-btn-link-{{ btn_size }} {% if width == 'full' %} tw-w-full {% endif %} {{ class_name }}">
42
42
  <span class="zz-btn-text">{{ text }}</span>
43
43
  {% if postfix_icon %}<span class="zz-btn-icon" style="margin-left: {{ icon_left_margin }}px; margin-right: {{ icon_right_margin }}px;">
44
44
  {% render 'zz-icon', icon_name: postfix_icon, icon_size: icon_size %}
@@ -47,6 +47,7 @@ size:按钮尺寸
47
47
  {% else %}
48
48
  <a
49
49
  href="{{ href | default: '#' }}"
50
+ {% if btn_id != blank %} id="{{ btn_id }}" {% endif %}
50
51
  class="zz-btn zz-btn-{{ btn_type }} zz-btn-{{ btn_color }} zz-btn-{{ btn_size }} {% if width == 'full' %} tw-w-full {% endif %} {{ class_name }}"
51
52
  {% if backdrop_filter %}
52
53
  style="backdrop-filter: blur(12px);background: #FFFFFF0F;"
@@ -61,6 +62,8 @@ size:按钮尺寸
61
62
 
62
63
  <button
63
64
  class="zz-btn zz-btn-{{ btn_type }} zz-btn-{{ btn_color }} zz-btn-{{ btn_size }} {% if width == 'full' %} tw-w-full {% endif %} {{ width }} {{ class_name }}"
65
+ {% if modal_id != blank %} data-zz-modal-target="#{{ modal_id }}" {% endif %}
66
+ {% if btn_id != blank %} id="{{ btn_id }}" {% endif %}
64
67
  {% if backdrop_filter %}
65
68
  style="backdrop-filter: blur(12px);background: #FFFFFF0F;"
66
69
  {% endif %}