zz-shopify-components 0.3.1-beta.0 → 0.3.1-beta.10

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.
@@ -4,7 +4,7 @@
4
4
  align-items: center;
5
5
  height: 36px;
6
6
  border-radius: 32px;
7
- background: #FFFFFF;
7
+ background: #F5F5F6;
8
8
  }
9
9
 
10
10
  .zz-radio-tabs-black.zz-radio-tabs {
@@ -123,6 +123,16 @@ zz-radio-tabs-item {
123
123
  .content-text a:hover {
124
124
  opacity: 0.8;
125
125
  }
126
+ .content-text ol {
127
+ list-style: decimal;
128
+ padding-left: 20px;
129
+ }
130
+
131
+ .content-text ul {
132
+ list-style: disc;
133
+ padding-left: 20px;
134
+ }
135
+
126
136
  /* 标题 */
127
137
  .zz-title-hx p{
128
138
  margin: 0;
@@ -7,13 +7,13 @@ class ZZRadioTabsItem extends HTMLElement {
7
7
  // 创建radio input
8
8
  const groupName =
9
9
  this.closest('zz-radio-tabs')?.getAttribute('name') || 'option';
10
- const type =
11
- this.closest('zz-radio-tabs')?.getAttribute('type') || 'default';
10
+ // 获取checked属性 - 支持checked和checked="true"两种方式
11
+ const checked = this.hasAttribute('checked');
12
12
  const value = this.getAttribute('value') || '';
13
13
  const slot = this.innerHTML;
14
14
  this.innerHTML = `
15
15
  <label class="zz-radio-tabs-wrapper">
16
- <input type="radio" name=${groupName} value=${value}>
16
+ <input type="radio" name=${groupName} value=${value} ${checked ? 'checked' : ''}>
17
17
  <div class="zz-radio-tabs-label">
18
18
  ${slot}
19
19
  </div>
@@ -50,6 +50,30 @@
50
50
  }
51
51
  }
52
52
 
53
+ function requestFormData(method, url, formData, options = {}) {
54
+ const baseUrl = options.baseUrl || DEFAULT_BASE_URL;
55
+ const timeout = options.timeout || DEFAULT_TIMEOUT;
56
+ const fullUrl = baseUrl + url;
57
+
58
+ let fetchOptions = {
59
+ method: method.toUpperCase(),
60
+ headers: {
61
+ // 不设置Content-Type,让浏览器自动设置multipart/form-data
62
+ ...(options.headers || {}),
63
+ },
64
+ ...options,
65
+ };
66
+
67
+ // 移除可能存在的Content-Type,让浏览器自动处理FormData
68
+ delete fetchOptions.headers['Content-Type'];
69
+
70
+ fetchOptions.body = formData;
71
+
72
+ return timeoutFetch(fetch(fullUrl, fetchOptions), timeout).then(
73
+ handleResponse
74
+ );
75
+ }
76
+
53
77
  function handleResponse(response) {
54
78
  if (!response.ok) {
55
79
  if (response.status === 401) {
@@ -69,5 +93,8 @@
69
93
  post: function (url, params = {}, options = {}) {
70
94
  return request('post', url, params, options);
71
95
  },
96
+ formData: function (url, formData, options = {}) {
97
+ return requestFormData('post', url, formData, options);
98
+ },
72
99
  };
73
100
  })();
@@ -0,0 +1,418 @@
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
+ will-change: transform, opacity;
37
+ position: fixed;
38
+ inset: 0;
39
+ z-index: var(--zz-modal-z-index, 9999);
40
+ display: grid;
41
+ place-items: center;
42
+ pointer-events: none; /* enable backdrop clicks via dialog */
43
+ }
44
+
45
+ .panel-inner {
46
+ will-change: transform, opacity;
47
+ pointer-events: auto;
48
+ background: var(--zz-modal-background, #ffffff);
49
+ color: inherit;
50
+ border-radius: var(--zz-modal-radius, 12px);
51
+ box-shadow: var(--zz-modal-shadow, 0 20px 60px rgba(0,0,0,0.2));
52
+ width: var(--zz-modal-width, min(720px, 92vw));
53
+ max-height: var(--zz-modal-max-height, 90vh);
54
+ display: flex;
55
+ flex-direction: column;
56
+ overflow: hidden;
57
+ opacity: 0;
58
+ transition: none;
59
+ }
60
+ @media (min-width: 768px) {
61
+ .panel-inner {
62
+ transform-origin: center center;
63
+ transform: translateY(8px) scale(0.98);
64
+ }
65
+ }
66
+
67
+ :host([open]) .panel-inner {
68
+ opacity: 1;
69
+ transform: translateY(0) scale(1);
70
+ }
71
+
72
+ /* 移动端底部抽屉模式 */
73
+ @media (max-width: 768px) {
74
+ :host([sheet-on-mobile]) .panel { place-items: end center; }
75
+ :host([sheet-on-mobile]) .panel-inner {
76
+ width: var(--zz-sheet-width, 100%);
77
+ max-width: 100%;
78
+ border-top-left-radius: var(--zz-modal-radius, 12px);
79
+ border-top-right-radius: var(--zz-modal-radius, 12px);
80
+ border-bottom-left-radius: 0;
81
+ border-bottom-right-radius: 0;
82
+ margin: 0;
83
+ transform: translateY(100%) scale(1);
84
+ }
85
+ :host([sheet-on-mobile][open]) .panel-inner { transform: translateY(0) scale(1); }
86
+ :host([sheet-on-mobile]) .body { padding-bottom: calc(var(--zz-modal-padding, 16px) + env(safe-area-inset-bottom)); }
87
+ }
88
+
89
+ @media (prefers-reduced-motion: no-preference) {
90
+ .panel-inner {
91
+ transition: opacity 160ms ease, transform 160ms ease;
92
+ }
93
+ }
94
+
95
+ .header, .footer {
96
+ padding: var(--zz-modal-padding, 16px);
97
+ flex: 0 0 auto;
98
+ }
99
+ :host([no-header]) .header, :host([no-header-auto]) .header { display: none; padding: 0; }
100
+ :host([no-footer]) .footer, :host([no-footer-auto]) .footer { display: none; padding: 0; }
101
+ .body {
102
+ padding: var(--zz-modal-padding, 16px);
103
+ overflow: auto;
104
+ flex: 1 1 auto;
105
+ }
106
+ .close-btn {
107
+ all: unset;
108
+ cursor: pointer;
109
+ position: absolute;
110
+ top: 8px;
111
+ right: 8px;
112
+ width: 32px;
113
+ height: 32px;
114
+ border-radius: 9999px;
115
+ display: grid;
116
+ place-items: center;
117
+ }
118
+ .close-btn:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
119
+ `;
120
+
121
+ const TEMPLATE = document.createElement('template');
122
+ TEMPLATE.innerHTML = `
123
+ <dialog part="dialog" aria-modal="true">
124
+ <div class="panel" part="backdrop">
125
+ <div class="panel-inner" role="document" part="panel">
126
+ <button style="z-index: 1000;" class="close-btn" part="close-button" aria-label="Close" data-zz-modal-close>
127
+ <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>
128
+ </button>
129
+ <header class="header" part="header"><slot name="header"></slot></header>
130
+ <section class="body" part="body"><slot></slot></section>
131
+ <footer class="footer" part="footer"><slot name="footer"></slot></footer>
132
+ </div>
133
+ </div>
134
+ </dialog>
135
+ `;
136
+
137
+ class ZZModal extends HTMLElement {
138
+ static get observedAttributes() { return ['open']; }
139
+
140
+ constructor() {
141
+ super();
142
+ this._onKeydown = this._onKeydown.bind(this);
143
+ this._onNativeClose = this._onNativeClose.bind(this);
144
+ this._onMouseDown = this._onMouseDown.bind(this);
145
+ this._onClick = this._onClick.bind(this);
146
+ this._previousActive = null;
147
+ this._mouseDownInsidePanel = false;
148
+
149
+ const shadow = this.attachShadow({ mode: 'open' });
150
+ const style = document.createElement('style');
151
+ style.textContent = STYLE_TEXT;
152
+ shadow.appendChild(style);
153
+ shadow.appendChild(TEMPLATE.content.cloneNode(true));
154
+
155
+ this._dialog = shadow.querySelector('dialog');
156
+ this._panel = shadow.querySelector('.panel-inner');
157
+ this._closeBtn = shadow.querySelector('[data-zz-modal-close]');
158
+ this._slotHeader = shadow.querySelector('slot[name="header"]');
159
+ this._slotFooter = shadow.querySelector('slot[name="footer"]');
160
+ }
161
+
162
+ connectedCallback() {
163
+ // Delegated internal events
164
+ this._closeBtn.addEventListener('click', () => this.hide());
165
+ this._dialog.addEventListener('close', this._onNativeClose);
166
+ this._dialog.addEventListener('mousedown', this._onMouseDown);
167
+ this._dialog.addEventListener('click', this._onClick);
168
+
169
+ // Reflect open state if attribute present at mount
170
+ if (this.hasAttribute('open')) {
171
+ // Defer to ensure upgraded before open
172
+ queueMicrotask(() => this.show());
173
+ }
174
+
175
+ // Auto hide empty header/footer
176
+ this._slotHeader.addEventListener('slotchange', () => this._updateSlotVisibility());
177
+ this._slotFooter.addEventListener('slotchange', () => this._updateSlotVisibility());
178
+ // Initial
179
+ this._updateSlotVisibility();
180
+ }
181
+
182
+ disconnectedCallback() {
183
+ this._dialog.removeEventListener('close', this._onNativeClose);
184
+ this._dialog.removeEventListener('mousedown', this._onMouseDown);
185
+ this._dialog.removeEventListener('click', this._onClick);
186
+ document.removeEventListener('keydown', this._onKeydown);
187
+ }
188
+
189
+ attributeChangedCallback(name, oldValue, newValue) {
190
+ if (name === 'open' && oldValue !== newValue) {
191
+ if (this.hasAttribute('open')) {
192
+ this.show();
193
+ } else {
194
+ this.hide();
195
+ }
196
+ }
197
+ }
198
+
199
+ get open() { return this._dialog?.open || this.hasAttribute('open'); }
200
+ set open(val) { if (val) this.setAttribute('open', ''); else this.removeAttribute('open'); }
201
+
202
+ show() { this._openInternal(true); }
203
+ hide() { this._closeInternal(true); }
204
+ toggle() { this.open ? this.hide() : this.show(); }
205
+
206
+ // Aliases
207
+ showModal() { this.show(); }
208
+ close() { this.hide(); }
209
+
210
+ _emit(name, detail = {}) {
211
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
212
+ }
213
+
214
+ _openInternal(emit = false) {
215
+ if (this._dialog.open) return;
216
+ this._previousActive = document.activeElement;
217
+
218
+ // Scroll lock
219
+ if (!this.hasAttribute('no-scroll-lock')) {
220
+ this._lockScroll();
221
+ }
222
+
223
+ // Inert others
224
+ if (this.hasAttribute('inert-others')) {
225
+ this._toggleInert(true);
226
+ }
227
+
228
+ // Native dialog path
229
+ try {
230
+ if ('showModal' in HTMLDialogElement.prototype) {
231
+ this._dialog.showModal();
232
+ } else {
233
+ // Fallback
234
+ this._dialog.setAttribute('open', '');
235
+ }
236
+ } catch (_) {
237
+ this._dialog.setAttribute('open', '');
238
+ }
239
+
240
+ // Sync attribute for CSS animations
241
+ this.setAttribute('open', '');
242
+
243
+ // Listeners
244
+ document.addEventListener('keydown', this._onKeydown);
245
+
246
+ // Focus first focusable within panel
247
+ this._focusInitial();
248
+
249
+ if (emit) this._emit('zz-modal:open');
250
+ }
251
+
252
+ _closeInternal(emit = false) {
253
+ if (!this._dialog.open && !this.hasAttribute('open')) return;
254
+
255
+ // Remove listeners
256
+ document.removeEventListener('keydown', this._onKeydown);
257
+
258
+ // Native dialog close
259
+ try {
260
+ if (this._dialog.open) this._dialog.close();
261
+ } catch (_) {
262
+ // ignore
263
+ }
264
+ this.removeAttribute('open');
265
+
266
+ // Restore scroll and inert
267
+ if (!this.hasAttribute('no-scroll-lock')) {
268
+ this._unlockScroll();
269
+ }
270
+ if (this.hasAttribute('inert-others')) {
271
+ this._toggleInert(false);
272
+ }
273
+
274
+ // Restore focus
275
+ if (this._previousActive && typeof this._previousActive.focus === 'function') {
276
+ this._previousActive.focus({ preventScroll: true });
277
+ }
278
+ this._previousActive = null;
279
+
280
+ if (emit) this._emit('zz-modal:close');
281
+ }
282
+
283
+ _onKeydown(e) {
284
+ const escAllowed = !this.hasAttribute('no-esc-close');
285
+ if (e.key === 'Escape' && escAllowed) {
286
+ e.stopPropagation();
287
+ this.hide();
288
+ }
289
+
290
+ // Basic focus trap for fallback scenario
291
+ if (e.key === 'Tab' && !('showModal' in HTMLDialogElement.prototype)) {
292
+ const focusables = this._getFocusable();
293
+ if (focusables.length === 0) return;
294
+ const first = focusables[0];
295
+ const last = focusables[focusables.length - 1];
296
+ const active = this.shadowRoot.activeElement || document.activeElement;
297
+ if (e.shiftKey) {
298
+ if (active === first || !this.contains(active)) {
299
+ e.preventDefault();
300
+ last.focus();
301
+ }
302
+ } else {
303
+ if (active === last || !this.contains(active)) {
304
+ e.preventDefault();
305
+ first.focus();
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ _onNativeClose() {
312
+ // Ensure attribute sync and cleanup
313
+ this._closeInternal(true);
314
+ }
315
+
316
+ _onMouseDown(e) {
317
+ // Track whether mousedown originated inside the panel to differentiate drags
318
+ const path = e.composedPath();
319
+ this._mouseDownInsidePanel = path.includes(this._panel);
320
+ }
321
+
322
+ _onClick(e) {
323
+ const backdropClose = !this.hasAttribute('no-backdrop-close');
324
+ if (!backdropClose) return;
325
+
326
+ // If click ended outside panel and started outside panel -> treat as backdrop click
327
+ const path = e.composedPath();
328
+ const clickInsidePanel = path.includes(this._panel);
329
+ if (!clickInsidePanel && !this._mouseDownInsidePanel) {
330
+ this.hide();
331
+ }
332
+ this._mouseDownInsidePanel = false;
333
+ }
334
+
335
+ _updateSlotVisibility() {
336
+ try {
337
+ const hasHeader = (this._slotHeader.assignedNodes({ flatten: true }).filter(n => n.nodeType === 1 || (n.nodeType === 3 && n.textContent.trim())).length) > 0;
338
+ const hasFooter = (this._slotFooter.assignedNodes({ flatten: true }).filter(n => n.nodeType === 1 || (n.nodeType === 3 && n.textContent.trim())).length) > 0;
339
+ if (!hasHeader) this.setAttribute('no-header-auto', ''); else this.removeAttribute('no-header-auto');
340
+ if (!hasFooter) this.setAttribute('no-footer-auto', ''); else this.removeAttribute('no-footer-auto');
341
+ } catch (_) {
342
+ // ignore
343
+ }
344
+ }
345
+
346
+ _getFocusable() {
347
+ const root = this.shadowRoot;
348
+ const within = root.querySelector('.panel-inner');
349
+ return Array.from(within.querySelectorAll([
350
+ 'a[href]','area[href]','input:not([disabled])','select:not([disabled])','textarea:not([disabled])',
351
+ 'button:not([disabled])','iframe','object','embed','[contenteditable]','[tabindex]:not([tabindex="-1"])'
352
+ ].join(',')));
353
+ }
354
+
355
+ _focusInitial() {
356
+ const focusables = this._getFocusable();
357
+ if (focusables.length > 0) {
358
+ focusables[0].focus({ preventScroll: true });
359
+ } else {
360
+ // Focus the dialog so that ESC works consistently
361
+ this._dialog.focus({ preventScroll: true });
362
+ }
363
+ }
364
+
365
+ _lockScroll() {
366
+ const body = document.body;
367
+ this._prevBodyOverflow = body.style.overflow;
368
+ body.style.overflow = 'hidden';
369
+ body.setAttribute('data-zz-modal-open', '');
370
+ }
371
+
372
+ _unlockScroll() {
373
+ const body = document.body;
374
+ body.style.overflow = this._prevBodyOverflow || '';
375
+ body.removeAttribute('data-zz-modal-open');
376
+ }
377
+
378
+ _toggleInert(isInert) {
379
+ const toInert = [];
380
+ const root = document.body;
381
+ for (const child of Array.from(root.children)) {
382
+ if (child === this.parentElement) continue;
383
+ toInert.push(child);
384
+ }
385
+ toInert.forEach((el) => {
386
+ if (isInert) el.setAttribute('inert', ''); else el.removeAttribute('inert');
387
+ });
388
+ }
389
+ }
390
+
391
+ customElements.define('zz-modal', ZZModal);
392
+
393
+ // Global open/close triggers
394
+ document.addEventListener('click', (e) => {
395
+ const openTrigger = e.target.closest('[data-zz-modal-target]');
396
+ if (openTrigger) {
397
+ const selector = openTrigger.getAttribute('data-zz-modal-target');
398
+ if (selector) {
399
+ const modal = document.querySelector(selector);
400
+ if (modal && modal.show) {
401
+ e.preventDefault();
402
+ modal.show();
403
+ }
404
+ }
405
+ return;
406
+ }
407
+ const closeTrigger = e.target.closest('[data-zz-modal-close]');
408
+ if (closeTrigger) {
409
+ const hostModal = closeTrigger.closest('zz-modal');
410
+ if (hostModal && hostModal.hide) {
411
+ e.preventDefault();
412
+ hostModal.hide();
413
+ }
414
+ }
415
+ }, { capture: true });
416
+ })();
417
+
418
+
@@ -168,7 +168,7 @@
168
168
 
169
169
  "blocks": [
170
170
  {
171
- "type": "zz-price-tag-mini"
171
+ "type": "zz-price-tag"
172
172
  },
173
173
  ],
174
174
  "presets": [
@@ -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