zz-shopify-components 0.3.1-beta.2 → 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.2",
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
+
@@ -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 %}