@magic-spells/cart-panel 0.1.0 → 0.1.2

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.
package/package.json CHANGED
@@ -1,82 +1,82 @@
1
1
  {
2
- "name": "@magic-spells/cart-panel",
3
- "version": "0.1.0",
4
- "description": "Accessible modal shopping cart dialog web component with Shopify integration, focus management, and smooth animations.",
5
- "author": "Cory Schulz",
6
- "license": "MIT",
7
- "type": "module",
8
- "main": "dist/cart-panel.cjs.js",
9
- "module": "dist/cart-panel.esm.js",
10
- "unpkg": "dist/cart-panel.min.js",
11
- "style": "dist/cart-panel.css",
12
- "sass": "dist/cart-panel.scss",
13
- "exports": {
14
- ".": {
15
- "import": "./dist/cart-panel.esm.js",
16
- "require": "./dist/cart-panel.cjs.js",
17
- "default": "./dist/cart-panel.esm.js"
18
- },
19
- "./css": "./dist/cart-panel.css",
20
- "./scss": "./dist/cart-panel.scss"
21
- },
22
- "sideEffects": true,
23
- "repository": {
24
- "type": "git",
25
- "url": "https://github.com/magic-spells/cart-panel"
26
- },
27
- "homepage": "https://github.com/magic-spells/cart-panel#readme",
28
- "bugs": {
29
- "url": "https://github.com/magic-spells/cart-panel/issues"
30
- },
31
- "keywords": [
32
- "cart-panel",
33
- "web-components",
34
- "e-commerce",
35
- "shopping-cart",
36
- "custom-elements",
37
- "shopify",
38
- "modal",
39
- "dialog",
40
- "accessibility",
41
- "a11y"
42
- ],
43
- "files": [
44
- "dist/",
45
- "src/"
46
- ],
47
- "scripts": {
48
- "build": "rollup -c",
49
- "lint": "eslint src/ rollup.config.mjs",
50
- "format": "prettier --write .",
51
- "prepublishOnly": "npm run build",
52
- "serve": "rollup -c --watch",
53
- "dev": "rollup -c --watch"
54
- },
55
- "publishConfig": {
56
- "access": "public",
57
- "registry": "https://registry.npmjs.org/"
58
- },
59
- "browserslist": [
60
- "last 2 versions",
61
- "not dead",
62
- "not ie <= 11"
63
- ],
64
- "devDependencies": {
65
- "@eslint/js": "^8.57.0",
66
- "@rollup/plugin-node-resolve": "^15.2.3",
67
- "@rollup/plugin-terser": "^0.4.4",
68
- "eslint": "^8.0.0",
69
- "globals": "^13.24.0",
70
- "prettier": "^3.3.3",
71
- "rollup": "^3.0.0",
72
- "rollup-plugin-copy": "^3.5.0",
73
- "rollup-plugin-postcss": "^4.0.2",
74
- "rollup-plugin-serve": "^1.1.1",
75
- "sass": "^1.89.2"
76
- },
77
- "dependencies": {
78
- "@magic-spells/cart-item": "^0.1.0",
79
- "@magic-spells/event-emitter": "^0.1.0",
80
- "@magic-spells/focus-trap": "^1.0.6"
81
- }
2
+ "name": "@magic-spells/cart-panel",
3
+ "version": "0.1.2",
4
+ "description": "Accessible modal shopping cart dialog web component with Shopify integration, focus management, and smooth animations.",
5
+ "author": "Cory Schulz",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/cart-panel.cjs.js",
9
+ "module": "dist/cart-panel.esm.js",
10
+ "unpkg": "dist/cart-panel.min.js",
11
+ "style": "dist/cart-panel.css",
12
+ "sass": "dist/cart-panel.scss",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./dist/cart-panel.esm.js",
16
+ "require": "./dist/cart-panel.cjs.js",
17
+ "default": "./dist/cart-panel.esm.js"
18
+ },
19
+ "./css": "./dist/cart-panel.css",
20
+ "./scss": "./dist/cart-panel.scss"
21
+ },
22
+ "sideEffects": true,
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/magic-spells/cart-panel"
26
+ },
27
+ "homepage": "https://github.com/magic-spells/cart-panel#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/magic-spells/cart-panel/issues"
30
+ },
31
+ "keywords": [
32
+ "cart-panel",
33
+ "web-components",
34
+ "e-commerce",
35
+ "shopping-cart",
36
+ "custom-elements",
37
+ "shopify",
38
+ "modal",
39
+ "dialog",
40
+ "accessibility",
41
+ "a11y"
42
+ ],
43
+ "files": [
44
+ "dist/",
45
+ "src/"
46
+ ],
47
+ "scripts": {
48
+ "build": "rollup -c",
49
+ "lint": "eslint src/ rollup.config.mjs",
50
+ "format": "prettier --write .",
51
+ "prepublishOnly": "npm run build",
52
+ "serve": "rollup -c --watch",
53
+ "dev": "rollup -c --watch"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "registry": "https://registry.npmjs.org/"
58
+ },
59
+ "browserslist": [
60
+ "last 2 versions",
61
+ "not dead",
62
+ "not ie <= 11"
63
+ ],
64
+ "devDependencies": {
65
+ "@eslint/js": "^8.57.0",
66
+ "@rollup/plugin-node-resolve": "^15.2.3",
67
+ "@rollup/plugin-terser": "^0.4.4",
68
+ "eslint": "^8.0.0",
69
+ "globals": "^13.24.0",
70
+ "prettier": "^3.3.3",
71
+ "rollup": "^3.0.0",
72
+ "rollup-plugin-copy": "^3.5.0",
73
+ "rollup-plugin-postcss": "^4.0.2",
74
+ "rollup-plugin-serve": "^1.1.1",
75
+ "sass": "^1.89.2"
76
+ },
77
+ "dependencies": {
78
+ "@magic-spells/cart-item": "^0.3.0",
79
+ "@magic-spells/event-emitter": "^0.1.0",
80
+ "@magic-spells/focus-trap": "^1.0.7"
81
+ }
82
82
  }
package/src/cart-panel.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import './cart-panel.scss';
2
2
  import '@magic-spells/cart-item';
3
+ import { CartItem } from '@magic-spells/cart-item';
3
4
  import '@magic-spells/focus-trap';
4
5
  import EventEmitter from '@magic-spells/event-emitter';
5
6
 
@@ -12,6 +13,7 @@ class CartDialog extends HTMLElement {
12
13
  #scrollPosition = 0;
13
14
  #currentCart = null;
14
15
  #eventEmitter;
16
+ #isInitialRender = true;
15
17
 
16
18
  /**
17
19
  * Clean up event listeners when component is removed from DOM
@@ -109,13 +111,18 @@ class CartDialog extends HTMLElement {
109
111
  }
110
112
  }
111
113
 
114
+ // Insert focus trap before the cart-panel
112
115
  _.contentPanel.parentNode.insertBefore(_.focusTrap, _.contentPanel);
116
+ // Move cart-panel inside the focus trap
113
117
  _.focusTrap.appendChild(_.contentPanel);
114
118
 
119
+ // Setup the trap - this will add focus-trap-start/end elements around the content
115
120
  _.focusTrap.setupTrap();
116
121
 
117
- // Add modal overlay
118
- _.prepend(document.createElement('cart-overlay'));
122
+ // Add modal overlay if it doesn't already exist
123
+ if (!_.querySelector('cart-overlay')) {
124
+ _.prepend(document.createElement('cart-overlay'));
125
+ }
119
126
  _.#attachListeners();
120
127
  _.#bindKeyboard();
121
128
  }
@@ -173,7 +180,7 @@ class CartDialog extends HTMLElement {
173
180
 
174
181
  // Handle close buttons
175
182
  _.addEventListener('click', (e) => {
176
- if (!e.target.closest('[data-action="hide-cart"]')) return;
183
+ if (!e.target.closest('[data-action-hide-cart]')) return;
177
184
  _.hide();
178
185
  });
179
186
 
@@ -228,9 +235,9 @@ class CartDialog extends HTMLElement {
228
235
  this.updateCartItem(cartKey, 0)
229
236
  .then((updatedCart) => {
230
237
  if (updatedCart && !updatedCart.error) {
231
- // Success - remove with animation
232
- element.destroyYourself();
238
+ // Success - let smart comparison handle the removal animation
233
239
  this.#currentCart = updatedCart;
240
+ this.#renderCartItems(updatedCart);
234
241
  this.#updateCartItems(updatedCart);
235
242
 
236
243
  // Emit cart updated and data changed events
@@ -263,8 +270,9 @@ class CartDialog extends HTMLElement {
263
270
  this.updateCartItem(cartKey, quantity)
264
271
  .then((updatedCart) => {
265
272
  if (updatedCart && !updatedCart.error) {
266
- // Success - update cart data
273
+ // Success - update cart data and refresh items
267
274
  this.#currentCart = updatedCart;
275
+ this.#renderCartItems(updatedCart);
268
276
  this.#updateCartItems(updatedCart);
269
277
  element.setState('ready');
270
278
 
@@ -285,15 +293,32 @@ class CartDialog extends HTMLElement {
285
293
  }
286
294
 
287
295
  /**
288
- * Update cart items
296
+ * Update cart items display based on cart data
289
297
  * @private
290
298
  */
291
299
  #updateCartItems(cart = null) {
292
- // Placeholder for cart item updates
293
- // Could be used to sync cart items with server data
294
300
  const cartData = cart || this.#currentCart;
295
- if (cartData) {
296
- // Future implementation: update cart item components
301
+ if (!cartData) return;
302
+
303
+ // Get cart sections
304
+ const hasItemsSection = this.querySelector('[data-cart-has-items]');
305
+ const emptySection = this.querySelector('[data-cart-is-empty]');
306
+ const itemsContainer = this.querySelector('[data-content-cart-items]');
307
+
308
+ if (!hasItemsSection || !emptySection || !itemsContainer) {
309
+ console.warn(
310
+ 'Cart sections not found. Expected [data-cart-has-items], [data-cart-is-empty], and [data-content-cart-items]'
311
+ );
312
+ return;
313
+ }
314
+
315
+ // Show/hide sections based on item count
316
+ if (cartData.item_count > 0) {
317
+ hasItemsSection.style.display = 'block';
318
+ emptySection.style.display = 'none';
319
+ } else {
320
+ hasItemsSection.style.display = 'none';
321
+ emptySection.style.display = 'block';
297
322
  }
298
323
  }
299
324
 
@@ -349,19 +374,157 @@ class CartDialog extends HTMLElement {
349
374
  * @returns {Promise<Object>} Cart data object
350
375
  */
351
376
  refreshCart() {
377
+ console.log('Refreshing cart...');
352
378
  return this.getCart().then((cartData) => {
379
+ console.log('Cart data received:', cartData);
353
380
  if (cartData && !cartData.error) {
354
381
  this.#currentCart = cartData;
382
+ this.#renderCartItems(cartData);
355
383
  this.#updateCartItems(cartData);
356
384
 
357
385
  // Emit cart refreshed and data changed events
358
386
  this.#emit('cart-dialog:refreshed', { cart: cartData });
359
387
  this.#emit('cart-dialog:data-changed', cartData);
388
+ } else {
389
+ console.warn('Cart data has error or is null:', cartData);
360
390
  }
361
391
  return cartData;
362
392
  });
363
393
  }
364
394
 
395
+ /**
396
+ * Remove items from DOM that are no longer in cart data
397
+ * @private
398
+ */
399
+ #removeItemsFromDOM(itemsContainer, newKeysSet) {
400
+ const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
401
+ const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
402
+
403
+ console.log(
404
+ `Removing ${itemsToRemove.length} items:`,
405
+ itemsToRemove.map((item) => item.getAttribute('key'))
406
+ );
407
+
408
+ itemsToRemove.forEach((item) => {
409
+ item.destroyYourself();
410
+ });
411
+ }
412
+
413
+ /**
414
+ * Add new items to DOM with animation delay
415
+ * @private
416
+ */
417
+ #addItemsToDOM(itemsContainer, itemsToAdd, newKeys) {
418
+ console.log(
419
+ `Adding ${itemsToAdd.length} items:`,
420
+ itemsToAdd.map((item) => item.key || item.id)
421
+ );
422
+
423
+ // Delay adding new items by 300ms to let cart slide open first
424
+ setTimeout(() => {
425
+ itemsToAdd.forEach((itemData) => {
426
+ const cartItem = CartItem.createAnimated(itemData);
427
+ const targetIndex = newKeys.indexOf(itemData.key || itemData.id);
428
+
429
+ // Find the correct position to insert the new item
430
+ if (targetIndex === 0) {
431
+ // Insert at the beginning
432
+ itemsContainer.insertBefore(cartItem, itemsContainer.firstChild);
433
+ } else {
434
+ // Find the item that should come before this one
435
+ let insertAfter = null;
436
+ for (let i = targetIndex - 1; i >= 0; i--) {
437
+ const prevKey = newKeys[i];
438
+ const prevItem = itemsContainer.querySelector(`cart-item[key="${prevKey}"]`);
439
+ if (prevItem) {
440
+ insertAfter = prevItem;
441
+ break;
442
+ }
443
+ }
444
+
445
+ if (insertAfter) {
446
+ insertAfter.insertAdjacentElement('afterend', cartItem);
447
+ } else {
448
+ itemsContainer.appendChild(cartItem);
449
+ }
450
+ }
451
+ });
452
+ }, 100);
453
+ }
454
+
455
+ /**
456
+ * Render cart items from Shopify cart data with smart comparison
457
+ * @private
458
+ */
459
+ #renderCartItems(cartData) {
460
+ const itemsContainer = this.querySelector('[data-content-cart-items]');
461
+
462
+ if (!itemsContainer || !cartData || !cartData.items) {
463
+ console.warn('Cannot render cart items:', {
464
+ itemsContainer: !!itemsContainer,
465
+ cartData: !!cartData,
466
+ items: cartData?.items?.length,
467
+ });
468
+ return;
469
+ }
470
+
471
+ // Handle initial render - load all items without animation
472
+ if (this.#isInitialRender) {
473
+ console.log('Initial cart render:', cartData.items.length, 'items');
474
+
475
+ // Clear existing items
476
+ itemsContainer.innerHTML = '';
477
+
478
+ // Create cart-item elements without animation
479
+ cartData.items.forEach((itemData) => {
480
+ const cartItem = new CartItem(itemData); // No animation
481
+ itemsContainer.appendChild(cartItem);
482
+ });
483
+
484
+ this.#isInitialRender = false;
485
+ console.log('Initial render complete, container children:', itemsContainer.children.length);
486
+ return;
487
+ }
488
+
489
+ console.log('Smart rendering cart items:', cartData.items.length, 'items');
490
+
491
+ // Get current DOM items and their keys
492
+ const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
493
+ const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
494
+
495
+ // Get new cart data keys in order
496
+ const newKeys = cartData.items.map((item) => item.key || item.id);
497
+ const newKeysSet = new Set(newKeys);
498
+
499
+ // Step 1: Remove items that are no longer in cart data
500
+ this.#removeItemsFromDOM(itemsContainer, newKeysSet);
501
+
502
+ // Step 2: Add new items that weren't in DOM (with animation delay)
503
+ const itemsToAdd = cartData.items.filter(
504
+ (itemData) => !currentKeys.has(itemData.key || itemData.id)
505
+ );
506
+
507
+ this.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys);
508
+
509
+ console.log('Smart rendering complete, container children:', itemsContainer.children.length);
510
+ }
511
+
512
+ /**
513
+ * Set the template function for cart items
514
+ * @param {Function} templateFn - Function that takes item data and returns HTML string
515
+ */
516
+ setCartItemTemplate(templateFn) {
517
+ CartItem.setTemplate(templateFn);
518
+ }
519
+
520
+ /**
521
+ * Set the processing template function for cart items
522
+ * @param {Function} templateFn - Function that returns HTML string for processing state
523
+ */
524
+ setCartItemProcessingTemplate(templateFn) {
525
+ CartItem.setProcessingTemplate(templateFn);
526
+ }
527
+
365
528
  /**
366
529
  * Shows the cart dialog and traps focus within it
367
530
  * @param {HTMLElement} [triggerEl=null] - The element that triggered the cart dialog
@@ -462,9 +625,20 @@ class CartPanel extends HTMLElement {
462
625
  }
463
626
  }
464
627
 
465
- customElements.define('cart-dialog', CartDialog);
466
- customElements.define('cart-overlay', CartOverlay);
467
- customElements.define('cart-panel', CartPanel);
628
+ if (!customElements.get('cart-dialog')) {
629
+ customElements.define('cart-dialog', CartDialog);
630
+ }
631
+ if (!customElements.get('cart-overlay')) {
632
+ customElements.define('cart-overlay', CartOverlay);
633
+ }
634
+ if (!customElements.get('cart-panel')) {
635
+ customElements.define('cart-panel', CartPanel);
636
+ }
468
637
 
469
- export { CartDialog, CartOverlay, CartPanel };
638
+ export { CartDialog, CartOverlay, CartPanel, CartItem };
470
639
  export default CartDialog;
640
+
641
+ // Make CartItem available globally for Shopify themes
642
+ if (typeof window !== 'undefined') {
643
+ window.CartItem = CartItem;
644
+ }
@@ -25,93 +25,93 @@ $cart-transition-timing: cubic-bezier(0.4, 0, 0.2, 1) !default;
25
25
 
26
26
  // Define CSS Custom Properties using SCSS values
27
27
  :root {
28
- // Layout
29
- --cart-dialog-z-index: #{$cart-dialog-z-index};
30
- --cart-overlay-z-index: #{$cart-overlay-z-index};
31
- --cart-panel-z-index: #{$cart-panel-z-index};
32
- --cart-panel-width: #{$cart-panel-width};
33
-
34
- // Overlay
35
- --cart-overlay-background: #{$cart-overlay-background};
36
- --cart-overlay-backdrop-filter: #{$cart-overlay-backdrop-filter};
37
-
38
- // Panel
39
- --cart-panel-background: #{$cart-panel-background};
40
- --cart-panel-shadow: #{$cart-panel-shadow};
41
- --cart-panel-border-radius: #{$cart-panel-border-radius};
42
-
43
- // Animation
44
- --cart-transition-duration: #{$cart-transition-duration};
45
- --cart-transition-timing: #{$cart-transition-timing};
28
+ // Layout
29
+ --cart-dialog-z-index: #{$cart-dialog-z-index};
30
+ --cart-overlay-z-index: #{$cart-overlay-z-index};
31
+ --cart-panel-z-index: #{$cart-panel-z-index};
32
+ --cart-panel-width: #{$cart-panel-width};
33
+
34
+ // Overlay
35
+ --cart-overlay-background: #{$cart-overlay-background};
36
+ --cart-overlay-backdrop-filter: #{$cart-overlay-backdrop-filter};
37
+
38
+ // Panel
39
+ --cart-panel-background: #{$cart-panel-background};
40
+ --cart-panel-shadow: #{$cart-panel-shadow};
41
+ --cart-panel-border-radius: #{$cart-panel-border-radius};
42
+
43
+ // Animation
44
+ --cart-transition-duration: #{$cart-transition-duration};
45
+ --cart-transition-timing: #{$cart-transition-timing};
46
46
  }
47
47
 
48
48
  // Cart Dialog - Main container
49
49
  cart-dialog {
50
- display: contents;
51
-
52
- &[aria-hidden='false'] {
53
- cart-overlay,
54
- cart-panel {
55
- pointer-events: auto;
56
- opacity: 1;
57
- }
58
-
59
- cart-panel {
60
- transform: translateX(0);
61
- }
62
- }
50
+ display: contents;
51
+
52
+ &[aria-hidden='false'] {
53
+ cart-overlay,
54
+ cart-panel {
55
+ pointer-events: auto;
56
+ opacity: 1;
57
+ }
58
+
59
+ cart-panel {
60
+ transform: translateX(0);
61
+ }
62
+ }
63
63
  }
64
64
 
65
65
  // Cart Overlay - Backdrop
66
66
  cart-overlay {
67
- position: fixed;
68
- top: 0;
69
- left: 0;
70
- width: 100vw;
71
- height: 100vh;
72
- opacity: 0;
73
- pointer-events: none;
74
- z-index: var(--cart-overlay-z-index);
75
- background-color: var(--cart-overlay-background);
76
- backdrop-filter: var(--cart-overlay-backdrop-filter);
77
- transition:
78
- opacity var(--cart-transition-duration) var(--cart-transition-timing),
79
- backdrop-filter var(--cart-transition-duration) var(--cart-transition-timing);
67
+ position: fixed;
68
+ top: 0;
69
+ left: 0;
70
+ width: 100vw;
71
+ height: 100vh;
72
+ opacity: 0;
73
+ pointer-events: none;
74
+ z-index: var(--cart-overlay-z-index);
75
+ background-color: var(--cart-overlay-background);
76
+ backdrop-filter: var(--cart-overlay-backdrop-filter);
77
+ transition:
78
+ opacity var(--cart-transition-duration) var(--cart-transition-timing),
79
+ backdrop-filter var(--cart-transition-duration) var(--cart-transition-timing);
80
80
  }
81
81
 
82
82
  // Cart Panel - Sliding content area
83
83
  cart-panel {
84
- position: fixed;
85
- top: 0;
86
- right: 0;
87
- width: var(--cart-panel-width);
88
- height: 100vh;
89
- opacity: 0;
90
- transform: translateX(100%);
91
- pointer-events: none;
92
- z-index: var(--cart-panel-z-index);
93
- background: var(--cart-panel-background);
94
- box-shadow: var(--cart-panel-shadow);
95
- border-radius: var(--cart-panel-border-radius);
96
- overflow: hidden;
97
- transition:
98
- opacity var(--cart-transition-duration) var(--cart-transition-timing),
99
- transform var(--cart-transition-duration) var(--cart-transition-timing);
100
-
101
- // When explicitly hidden, remove from layout
102
- &.hidden {
103
- display: none;
104
- }
84
+ position: fixed;
85
+ top: 0;
86
+ right: 0;
87
+ width: var(--cart-panel-width);
88
+ height: 100vh;
89
+ opacity: 0;
90
+ transform: translateX(100%);
91
+ pointer-events: none;
92
+ z-index: var(--cart-panel-z-index);
93
+ background: var(--cart-panel-background);
94
+ box-shadow: var(--cart-panel-shadow);
95
+ border-radius: var(--cart-panel-border-radius);
96
+ overflow: hidden;
97
+ transition:
98
+ opacity var(--cart-transition-duration) var(--cart-transition-timing),
99
+ transform var(--cart-transition-duration) var(--cart-transition-timing);
100
+
101
+ // When explicitly hidden, remove from layout
102
+ &.hidden {
103
+ display: none;
104
+ }
105
105
  }
106
106
 
107
107
  // Body scroll lock when cart is open
108
108
  body.overflow-hidden {
109
- overflow: hidden;
110
- position: fixed;
111
- width: 100%;
112
- height: 100%;
113
- left: 0;
114
- right: 0;
115
- margin: 0;
116
- // The top property will be set dynamically by the component
109
+ overflow: hidden;
110
+ position: fixed;
111
+ width: 100%;
112
+ height: 100%;
113
+ left: 0;
114
+ right: 0;
115
+ margin: 0;
116
+ // The top property will be set dynamically by the component
117
117
  }