@magic-spells/cart-panel 0.3.1 → 1.0.1

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/README.md CHANGED
@@ -1,20 +1,19 @@
1
1
  # Cart Panel Web Component
2
2
 
3
- A professional, highly-customizable modal shopping cart dialog built with Web Components. Features accessible modal interactions, smooth slide-in animations, real-time cart synchronization, and seamless integration with Shopify and other e-commerce platforms.
3
+ A professional, highly-customizable shopping cart component built with Web Components. Features smooth animations, real-time cart synchronization, and seamless integration with Shopify and other e-commerce platforms.
4
4
 
5
5
  [**Live Demo**](https://magic-spells.github.io/cart-panel/demo/)
6
6
 
7
7
  ## Features
8
8
 
9
- - 🛒 **Complete cart modal** - Slide-in panel with overlay and focus management
10
- - **Accessibility-first** - ARIA attributes, focus trapping, and keyboard navigation
11
- - 🔄 **Real-time sync** - Automatic cart updates via `/cart.json` and `/cart/change.json` APIs
12
- - 📡 **Event-driven architecture** - Rich event system with custom event emitter
13
- - 🎬 **Smooth animations** - CSS transitions with customizable timing and effects
14
- - 🔒 **Body scroll locking** - Prevents background scrolling when modal is open
15
- - 🎛️ **Highly customizable** - CSS custom properties and SCSS variables
16
- - 📱 **Framework agnostic** - Pure Web Components work with any framework
17
- - 🛒 **Shopify-ready** - Built specifically for Shopify cart integrations
9
+ - **Complete cart management** - Handles cart data, AJAX requests, and item rendering
10
+ - **Delegates modal to dialog-panel** - Works with `@magic-spells/dialog-panel` for accessible modal behavior
11
+ - **Real-time sync** - Automatic cart updates via `/cart.json` and `/cart/change.json` APIs
12
+ - **Event-driven architecture** - Rich event system with custom event emitter
13
+ - **Smooth animations** - CSS transitions for processing, appearing, and destroying states
14
+ - **Highly customizable** - CSS custom properties and template system
15
+ - **Framework agnostic** - Pure Web Components work with any framework
16
+ - **Shopify-ready** - Built specifically for Shopify cart integrations
18
17
 
19
18
  ## Installation
20
19
 
@@ -39,439 +38,314 @@ Or include directly in your HTML:
39
38
 
40
39
  ## Usage
41
40
 
42
- ```html
43
- <!-- Trigger button -->
44
- <button aria-haspopup="dialog" aria-controls="my-cart" aria-expanded="false">
45
- Open Cart (3 items)
46
- </button>
41
+ The cart-panel component delegates modal behavior to a `<dialog-panel>` ancestor. It finds its nearest `<dialog-panel>` and calls `show()`/`hide()` on it.
47
42
 
48
- <!-- Cart modal dialog -->
49
- <cart-dialog id="my-cart" aria-labelledby="cart-title">
50
- <cart-panel>
51
- <div class="cart-header">
52
- <h2 id="cart-title">Shopping Cart</h2>
53
- <button data-action="hide-cart" aria-label="Close cart">&times;</button>
54
- </div>
55
-
56
- <div class="cart-body">
57
- <!-- Cart items using @magic-spells/cart-item -->
58
- <cart-item data-key="shopify-line-item-123">
59
- <cart-item-content>
60
- <div class="product-info">
61
- <img src="product.jpg" alt="Product" />
62
- <div>
63
- <h4>Awesome T-Shirt</h4>
64
- <div class="price">$29.99</div>
65
- </div>
43
+ ```html
44
+ <!-- Cart with dialog-panel wrapper -->
45
+ <dialog-panel id="cart-dialog">
46
+ <dialog aria-labelledby="cart-title">
47
+ <cart-panel manual>
48
+ <div class="cart-header">
49
+ <h2 id="cart-title">Shopping Cart</h2>
50
+ <button aria-label="Close cart" class="close-button" data-action-hide-cart>
51
+ &times;
52
+ </button>
53
+ </div>
54
+ <div class="cart-body">
55
+ <!-- Cart has items section -->
56
+ <div data-cart-has-items>
57
+ <div class="cart-items" data-content-cart-items>
58
+ <!-- Cart items rendered dynamically -->
66
59
  </div>
67
- <div class="quantity-controls">
68
- <input type="number" data-cart-quantity value="1" min="1" />
69
- <button data-action="remove">Remove</button>
60
+ </div>
61
+
62
+ <!-- Cart is empty section -->
63
+ <div data-cart-is-empty>
64
+ <div class="empty-cart">
65
+ <p>Your cart is empty</p>
66
+ <p>Add some items to get started!</p>
70
67
  </div>
71
- </cart-item-content>
72
- <cart-item-processing>
73
- <div>Processing...</div>
74
- </cart-item-processing>
75
- </cart-item>
76
- </div>
68
+ </div>
69
+ </div>
70
+ <div class="cart-footer">
71
+ <div class="cart-total">
72
+ Subtotal: <span data-content-cart-subtotal>$0.00</span>
73
+ </div>
74
+ <button class="checkout-button">Proceed to Checkout</button>
75
+ </div>
76
+ </cart-panel>
77
+ </dialog>
78
+ </dialog-panel>
77
79
 
78
- <div class="cart-footer">
79
- <div class="cart-total">Total: $29.99</div>
80
- <button class="checkout-btn">Checkout</button>
81
- </div>
82
- </cart-panel>
83
- </cart-dialog>
80
+ <!-- Trigger button -->
81
+ <button onclick="document.querySelector('cart-panel').show()">
82
+ Open Cart
83
+ </button>
84
84
  ```
85
85
 
86
86
  ## How It Works
87
87
 
88
- The cart panel component creates a complete modal cart experience with three main elements:
88
+ The cart panel architecture consists of:
89
89
 
90
- - **cart-dialog**: Main container managing modal state, focus trapping, and scroll locking
91
- - **cart-overlay**: Clickable backdrop that closes the modal when clicked
92
- - **cart-panel**: Sliding content area that contains the actual cart items and controls
90
+ - **cart-panel**: Main component managing cart data, AJAX requests, and rendering
91
+ - **cart-item**: Individual cart item with state management and animations
92
+ - **cart-item-content**: Content wrapper inside cart-item
93
+ - **cart-item-processing**: Processing overlay with loader
93
94
 
94
95
  The component automatically handles:
95
96
 
96
- - Opening when trigger buttons with `aria-controls` are clicked
97
- - Closing via close buttons, escape key, or overlay clicks
98
97
  - Fetching cart data from `/cart.json` on show
99
98
  - Updating cart items via `/cart/change.json` API calls
100
- - Managing cart item states and animations through integrated `@magic-spells/cart-item`
101
- - Filtering out cart items with `_hidden` property from display and calculations
99
+ - Smart rendering with add/update/remove animations
100
+ - Filtering out cart items with `_hide_in_cart` property from display and calculations
102
101
  - Emitting events for cart updates and state changes
103
102
 
104
- ## Configuration
105
-
106
- ### Cart Dialog Attributes
103
+ ### Key Architecture Decisions
107
104
 
108
- | Attribute | Description | Required |
109
- | ----------------- | ----------------------------------------------- | ----------- |
110
- | `id` | Unique identifier referenced by trigger buttons | Yes |
111
- | `aria-labelledby` | References the cart title element | Recommended |
112
- | `aria-modal` | Set to "true" for proper modal semantics | Recommended |
105
+ 1. **Delegates modal to dialog-panel**: CartPanel finds its nearest `<dialog-panel>` ancestor and calls `show()`/`hide()` on it. No modal management code in cart-panel.
113
106
 
114
- ### Required HTML Structure
107
+ 2. **Native dialog features**: Focus trap, escape key, backdrop click are all handled by `<dialog-panel>` which wraps native `<dialog>`.
115
108
 
116
- | Element | Description | Required |
117
- | ---------------- | -------------------------------------------- | -------- |
118
- | `<cart-dialog>` | Main modal container | Yes |
119
- | `<cart-panel>` | Sliding content area | Yes |
120
- | `<cart-overlay>` | Background overlay (auto-created if missing) | No |
109
+ 3. **Event-driven items**: CartItem emits `cart-item:remove` and `cart-item:quantity-change` events that bubble up to CartPanel.
121
110
 
122
- ### Interactive Elements
123
-
124
- | Selector | Description | Event Triggered |
125
- | --------------------------- | ----------------------------------- | --------------------------- |
126
- | `[aria-controls="cart-id"]` | Trigger buttons to open cart | Opens modal |
127
- | `[data-action="hide-cart"]` | Close buttons inside modal | Closes modal |
128
- | `[data-action="remove"]` | Remove item buttons (via cart-item) | `cart-item:remove` |
129
- | `[data-cart-quantity]` | Quantity inputs (via cart-item) | `cart-item:quantity-change` |
111
+ ## Configuration
130
112
 
131
- Example:
113
+ ### CartPanel Attributes
132
114
 
133
- ```html
134
- <!-- Minimal cart modal -->
135
- <cart-dialog id="simple-cart">
136
- <cart-panel>
137
- <h2>Cart</h2>
138
- <button data-action="hide-cart">Close</button>
139
- <!-- Cart content here -->
140
- </cart-panel>
141
- </cart-dialog>
142
-
143
- <!-- Complete cart with all features -->
144
- <cart-dialog id="full-cart" aria-modal="true" aria-labelledby="cart-heading">
145
- <cart-overlay></cart-overlay>
146
- <cart-panel>
147
- <header class="cart-header">
148
- <h2 id="cart-heading">Shopping Cart</h2>
149
- <button data-action="hide-cart" aria-label="Close cart">×</button>
150
- </header>
151
- <div class="cart-content">
152
- <!-- Cart items will be rendered here -->
153
- </div>
154
- <footer class="cart-footer">
155
- <button class="checkout-btn">Checkout</button>
156
- </footer>
157
- </cart-panel>
158
- </cart-dialog>
159
- ```
115
+ | Attribute | Type | Description |
116
+ | --------- | ------- | ------------------------------------------------------ |
117
+ | `manual` | Boolean | Skip auto-refresh on connect, require explicit refreshCart() |
118
+ | `state` | String | Reflected attribute: 'has-items' or 'empty' |
160
119
 
161
- ## Customization
120
+ ### Required HTML Structure
162
121
 
163
- ### Styling
122
+ | Selector | Description | Required |
123
+ | --------------------------- | ---------------------------------------- | -------- |
124
+ | `[data-content-cart-items]` | Container where cart-item elements render | Yes |
125
+ | `[data-cart-has-items]` | Section shown when cart has visible items | No |
126
+ | `[data-cart-is-empty]` | Section shown when cart is empty | No |
127
+ | `[data-action-hide-cart]` | Close buttons (click triggers hide()) | No |
128
+ | `[data-content-cart-count]` | Elements updated with visible item count | No |
129
+ | `[data-content-cart-subtotal]` | Elements updated with formatted subtotal | No |
164
130
 
165
- The component provides complete styling control through CSS custom properties and SCSS variables. Customize the modal appearance to match your design:
131
+ ### CartItem Child Elements
166
132
 
167
- ```css
168
- /* Customize modal positioning and sizing */
169
- cart-dialog {
170
- --cart-panel-width: min(500px, 95vw);
171
- --cart-panel-z-index: 9999;
172
- --cart-overlay-z-index: 9998;
173
- }
133
+ | Selector | Description |
134
+ | --------------------------- | ---------------------------------------------- |
135
+ | `[data-action-remove-item]` | Remove button (triggers cart-item:remove) |
136
+ | `[data-cart-quantity]` | Quantity input field |
137
+ | `[data-content-line-price]` | Line price display (auto-formatted) |
174
138
 
175
- /* Customize overlay appearance */
176
- cart-overlay {
177
- --cart-overlay-background: rgba(0, 0, 0, 0.3);
178
- --cart-overlay-backdrop-filter: blur(8px);
179
- }
139
+ ## JavaScript API
180
140
 
181
- /* Customize panel styling */
182
- cart-panel {
183
- --cart-panel-background: #ffffff;
184
- --cart-panel-shadow: -10px 0 30px rgba(0, 0, 0, 0.2);
185
- --cart-panel-border-radius: 12px 0 0 12px;
186
- }
141
+ ### CartPanel Methods
187
142
 
188
- /* Customize animations */
189
- cart-dialog {
190
- --cart-transition-duration: 400ms;
191
- --cart-transition-timing: cubic-bezier(0.25, 0.8, 0.25, 1);
192
- }
143
+ ```javascript
144
+ const cartPanel = document.querySelector('cart-panel');
193
145
 
194
- /* Style your cart content layout */
195
- cart-panel {
196
- display: flex;
197
- flex-direction: column;
198
- }
146
+ // Dialog Control
147
+ cartPanel.show(triggerEl?, cartObj?) // Open modal and refresh cart
148
+ cartPanel.hide() // Close modal
199
149
 
200
- .cart-header {
201
- padding: 1.5rem;
202
- border-bottom: 1px solid #eee;
203
- background: #f8f9fa;
204
- }
150
+ // Cart Data
151
+ cartPanel.getCart() // Fetch from /cart.json
152
+ cartPanel.updateCartItem(key, quantity) // POST to /cart/change.json
153
+ cartPanel.refreshCart(cartObj?) // Update display with cart data
205
154
 
206
- .cart-content {
207
- flex: 1;
208
- overflow-y: auto;
209
- padding: 1rem;
210
- }
155
+ // Templates
156
+ cartPanel.setCartItemTemplate(name, fn) // Set template function
157
+ cartPanel.setCartItemProcessingTemplate(fn) // Set processing overlay template
211
158
 
212
- .cart-footer {
213
- padding: 1.5rem;
214
- border-top: 1px solid #eee;
215
- background: #f8f9fa;
216
- }
159
+ // Event Subscription (chainable)
160
+ cartPanel.on(eventName, callback) // Add event listener
161
+ cartPanel.off(eventName, callback) // Remove event listener
217
162
  ```
218
163
 
219
- ### CSS Variables & SCSS Variables
164
+ ### CartPanel Events
220
165
 
221
- The component supports both CSS custom properties and SCSS variables for maximum flexibility:
166
+ | Event | Detail | Description |
167
+ | ---------------------- | --------------------------------------------------- | ------------------------ |
168
+ | `cart-panel:show` | `{ triggerElement }` | When show() called |
169
+ | `cart-panel:hide` | `{}` | When hide() called |
170
+ | `cart-panel:refreshed` | `{ cart }` | After cart data refreshed |
171
+ | `cart-panel:updated` | `{ cart }` | After item quantity/remove |
172
+ | `cart-panel:data-changed` | `{ calculated_count, calculated_subtotal, ... }` | Any cart change |
222
173
 
223
- | CSS Variable | SCSS Variable | Description | Default |
224
- | -------------------------------- | ------------------------------- | ---------------------------- | ---------------------------- |
225
- | `--cart-dialog-z-index` | `$cart-dialog-z-index` | Base z-index for modal | 1000 |
226
- | `--cart-overlay-z-index` | `$cart-overlay-z-index` | Overlay layer z-index | 1000 |
227
- | `--cart-panel-z-index` | `$cart-panel-z-index` | Panel layer z-index | 1001 |
228
- | `--cart-panel-width` | `$cart-panel-width` | Width of the sliding panel | min(400px, 90vw) |
229
- | `--cart-overlay-background` | `$cart-overlay-background` | Overlay background color | rgba(0, 0, 0, 0.15) |
230
- | `--cart-overlay-backdrop-filter` | `$cart-overlay-backdrop-filter` | Overlay backdrop blur effect | blur(4px) |
231
- | `--cart-panel-background` | `$cart-panel-background` | Panel background color | #ffffff |
232
- | `--cart-panel-shadow` | `$cart-panel-shadow` | Panel box shadow | -5px 0 25px rgba(0,0,0,0.15) |
233
- | `--cart-panel-border-radius` | `$cart-panel-border-radius` | Panel border radius | 0 |
234
- | `--cart-transition-duration` | `$cart-transition-duration` | Animation duration | 350ms |
235
- | `--cart-transition-timing` | `$cart-transition-timing` | Animation timing function | cubic-bezier(0.4, 0, 0.2, 1) |
174
+ ### CartItem Events (bubbled)
236
175
 
237
- #### CSS Override Examples:
176
+ | Event | Detail | Description |
177
+ | ------------------------- | ---------------------------------- | --------------------- |
178
+ | `cart-item:remove` | `{ cartKey, element }` | Remove button clicked |
179
+ | `cart-item:quantity-change` | `{ cartKey, quantity, element }` | Quantity changed |
238
180
 
239
- ```css
240
- /* Dramatic slide-in effect */
241
- .dramatic-cart {
242
- --cart-transition-duration: 600ms;
243
- --cart-transition-timing: cubic-bezier(0.68, -0.55, 0.265, 1.55);
244
- --cart-overlay-background: rgba(0, 0, 0, 0.4);
245
- --cart-overlay-backdrop-filter: blur(10px);
246
- }
247
-
248
- /* Subtle minimal styling */
249
- .minimal-cart {
250
- --cart-panel-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
251
- --cart-panel-border-radius: 8px;
252
- --cart-transition-duration: 200ms;
253
- --cart-overlay-background: rgba(0, 0, 0, 0.05);
254
- }
181
+ ### CartItem States
255
182
 
256
- /* Mobile-optimized full-width */
257
- @media (max-width: 768px) {
258
- .mobile-cart {
259
- --cart-panel-width: 100vw;
260
- --cart-panel-border-radius: 0;
261
- }
262
- }
263
- ```
183
+ | State | Description |
184
+ | ------------ | ---------------------------------------------- |
185
+ | `ready` | Interactive state, content visible |
186
+ | `processing` | During AJAX calls, blur/scale effects, loader visible |
187
+ | `destroying` | Removal animation (height collapses) |
188
+ | `appearing` | Entry animation (height expands from 0) |
264
189
 
265
- #### SCSS Override Examples:
190
+ ### CartItem Static Methods
266
191
 
267
- ```scss
268
- // Override SCSS variables before importing
269
- $cart-panel-width: min(500px, 95vw);
270
- $cart-transition-duration: 400ms;
271
- $cart-overlay-background: rgba(0, 0, 0, 0.25);
192
+ ```javascript
193
+ import { CartItem } from '@magic-spells/cart-panel';
272
194
 
273
- // Import the component styles
274
- @import '@magic-spells/cart-panel/scss';
195
+ // Set template globally
196
+ CartItem.setTemplate(name, templateFn)
275
197
 
276
- // Or import the CSS and override with CSS custom properties
277
- @import '@magic-spells/cart-panel/css';
198
+ // Set processing overlay template
199
+ CartItem.setProcessingTemplate(templateFn)
278
200
 
279
- .my-store cart-dialog {
280
- --cart-transition-duration: 400ms;
281
- --cart-panel-background: #f8f9fa;
282
- }
201
+ // Create with animation
202
+ CartItem.createAnimated(itemData, cartData)
283
203
  ```
284
204
 
285
- ### JavaScript API
286
-
287
- #### Methods
288
-
289
- - `show(triggerElement)`: Open the cart modal and focus the first interactive element
290
- - `hide()`: Close the cart modal and restore focus to trigger element
291
- - `getCart()`: Fetch current cart data from `/cart.json`
292
- - `updateCartItem(key, quantity)`: Update cart item quantity via `/cart/change.json`
293
- - `refreshCart()`: Refresh cart data and update UI components
294
- - `on(eventName, callback)`: Add event listener using the event emitter
295
- - `off(eventName, callback)`: Remove event listener
296
-
297
- #### Events
298
-
299
- The component emits custom events for cart state changes and data updates:
300
-
301
- **Modal Events:**
302
-
303
- - `cart-dialog:show` - Modal has opened
304
- - `cart-dialog:hide` - Modal has started closing
305
- - `cart-dialog:afterHide` - Modal has finished closing animation
306
-
307
- **Cart Data Events:**
308
-
309
- - `cart-dialog:updated` - Cart data updated after item change
310
- - `cart-dialog:refreshed` - Cart data refreshed from server
311
- - `cart-dialog:data-changed` - Any cart data change (unified event)
312
-
313
- **Cart Item Events (bubbled from cart-item components):**
314
-
315
- - `cart-item:remove` - Remove button clicked: `{ cartKey, element }`
316
- - `cart-item:quantity-change` - Quantity changed: `{ cartKey, quantity, element }`
317
-
318
- #### Programmatic Control
205
+ ### Programmatic Control
319
206
 
320
207
  ```javascript
321
- const cartDialog = document.querySelector('cart-dialog');
208
+ const cartPanel = document.querySelector('cart-panel');
322
209
 
323
210
  // Open/close cart
324
- cartDialog.show(); // Open modal
325
- cartDialog.hide(); // Close modal
211
+ cartPanel.show();
212
+ cartPanel.hide();
326
213
 
327
214
  // Cart data operations
328
- const cartData = await cartDialog.getCart();
329
- const updatedCart = await cartDialog.updateCartItem('item-key', 2);
330
- await cartDialog.refreshCart();
215
+ const cartData = await cartPanel.getCart();
216
+ const updatedCart = await cartPanel.updateCartItem('item-key', 2);
217
+ await cartPanel.refreshCart();
331
218
 
332
- // Event emitter pattern (recommended)
333
- cartDialog
334
- .on('cart-dialog:show', (e) => {
219
+ // Event emitter pattern (chainable)
220
+ cartPanel
221
+ .on('cart-panel:show', (e) => {
335
222
  console.log('Cart opened by:', e.detail.triggerElement);
336
223
  })
337
- .on('cart-dialog:data-changed', (cartData) => {
338
- console.log('Cart updated:', cartData);
224
+ .on('cart-panel:data-changed', (e) => {
225
+ console.log('Cart updated:', e.detail);
339
226
  // Update header cart count, etc.
340
227
  });
341
228
 
342
- // Traditional event listeners (also supported)
343
- cartDialog.addEventListener('cart-item:remove', (e) => {
229
+ // Traditional event listeners also work
230
+ cartPanel.addEventListener('cart-item:remove', (e) => {
344
231
  console.log('Remove requested:', e.detail.cartKey);
232
+ });
233
+ ```
234
+
235
+ ## Template System
236
+
237
+ Set up custom templates to control how cart items render:
345
238
 
346
- // The component handles the API calls automatically
347
- // Just listen for the data changes
239
+ ```javascript
240
+ const cartPanel = document.querySelector('cart-panel');
241
+
242
+ // Default template
243
+ cartPanel.setCartItemTemplate('default', (itemData, cartData) => {
244
+ return `
245
+ <div class="cart-item">
246
+ <img src="${itemData.image}" alt="${itemData.product_title}" />
247
+ <div class="cart-item-info">
248
+ <h4>${itemData.product_title}</h4>
249
+ <div class="variant">${itemData.variant_title || ''}</div>
250
+ </div>
251
+ <quantity-input value="${itemData.quantity}" min="1"></quantity-input>
252
+ <button data-action-remove-item>Remove</button>
253
+ <span data-content-line-price></span>
254
+ </div>
255
+ `;
348
256
  });
349
257
 
350
- cartDialog.addEventListener('cart-item:quantity-change', (e) => {
351
- console.log('Quantity changed:', e.detail.quantity);
352
- // Component automatically syncs with Shopify
258
+ // Custom template for subscriptions
259
+ cartPanel.setCartItemTemplate('subscription', (itemData, cartData) => {
260
+ return `
261
+ <div class="subscription-item">
262
+ <div class="recurring-badge">Subscription</div>
263
+ <h4>${itemData.product_title}</h4>
264
+ <div class="price">$${(itemData.price / 100).toFixed(2)}/month</div>
265
+ </div>
266
+ `;
353
267
  });
354
268
 
355
- // Listen for all cart changes
356
- cartDialog.on('cart-dialog:data-changed', (cartData) => {
357
- // Update your UI when cart changes
358
- updateCartBadge(cartData.item_count);
359
- updateCartTotal(cartData.total_price);
269
+ // Custom processing overlay
270
+ cartPanel.setCartItemProcessingTemplate(() => {
271
+ return `<div class="custom-loader">Updating...</div>`;
360
272
  });
361
273
  ```
362
274
 
363
- #### Performance & Architecture
364
-
365
- The component is optimized for:
366
-
367
- - **Smooth animations**: CSS transforms and transitions for slide-in effects
368
- - **Focus management**: Automatic focus trapping with `@magic-spells/focus-trap`
369
- - **Memory management**: Proper event listener cleanup on disconnect
370
- - **Scroll lock**: Body scroll prevention with position restoration
371
- - **API efficiency**: Smart cart data fetching and caching
372
- - **Event system**: Centralized event handling with custom event emitter
373
- - **Accessibility**: Full ARIA support and keyboard navigation
275
+ ## Customization
374
276
 
375
- ## Integration Examples
277
+ ### CSS Custom Properties
376
278
 
377
- ### Line Item Properties
279
+ ```css
280
+ cart-item {
281
+ /* Animation durations */
282
+ --cart-item-processing-duration: 250ms;
283
+ --cart-item-destroying-duration: 600ms;
284
+ --cart-item-appearing-duration: 400ms;
285
+
286
+ /* Colors */
287
+ --cart-item-shadow-color: rgba(0, 0, 0, 0.15);
288
+ --cart-item-shadow-color-strong: rgba(0, 0, 0, 0.5);
289
+ --cart-item-destroying-bg: rgba(0, 0, 0, 0.1);
290
+
291
+ /* Scale transforms */
292
+ --cart-item-processing-scale: 0.98;
293
+ --cart-item-destroying-scale: 0.85;
294
+ --cart-item-appearing-scale: 0.9;
295
+
296
+ /* Blur effects */
297
+ --cart-item-processing-blur: 1px;
298
+ --cart-item-destroying-blur: 10px;
299
+ --cart-item-appearing-blur: 2px;
300
+
301
+ /* Opacity and filters */
302
+ --cart-item-destroying-opacity: 0.2;
303
+ --cart-item-appearing-opacity: 0.5;
304
+ --cart-item-destroying-brightness: 0.6;
305
+ --cart-item-destroying-saturate: 0.3;
306
+ }
307
+ ```
378
308
 
379
- The cart panel supports several Shopify line item properties for enhanced functionality:
309
+ ## Line Item Properties
380
310
 
381
- #### Cart Item Filtering (`_hide_in_cart`)
311
+ The cart-panel supports Shopify line item properties for enhanced functionality:
382
312
 
383
- Cart items can be hidden from display by setting the `_hide_in_cart` property. Hidden items are excluded from:
313
+ | Property | Purpose |
314
+ | --------------------------- | -------------------------------------- |
315
+ | `_hide_in_cart` | Hide item from display (still in cart) |
316
+ | `_ignore_price_in_subtotal` | Exclude from subtotal calculation |
317
+ | `_cart_template` | Use specific template name for rendering |
318
+ | `_group_id` | Group items together (bundles) |
319
+ | `_group_role` | Role within a group: "parent" or "child" |
384
320
 
385
- - Cart item display and rendering
386
- - Cart count calculations
387
- - Subtotal calculations
321
+ ### Hidden Items (`_hide_in_cart`)
388
322
 
389
323
  ```javascript
390
- // Example: Hide a cart item from display
324
+ // Item hidden from display but stays in actual cart
391
325
  {
392
- "items": [
393
- {
394
- "key": "item-123",
395
- "properties": {
396
- "_hide_in_cart": "true" // Hide from cart display
397
- }
398
- }
399
- ]
326
+ "key": "item-123",
327
+ "properties": {
328
+ "_hide_in_cart": "true"
329
+ }
400
330
  }
401
331
  ```
402
332
 
403
- #### Custom Templates (`_cart_template`)
404
-
405
- Different cart item templates can be specified using the `_cart_template` property:
333
+ ### Custom Templates (`_cart_template`)
406
334
 
407
335
  ```javascript
408
- // Example: Use different templates for different item types
336
+ // Use subscription template for this item
409
337
  {
410
- "items": [
411
- {
412
- "key": "subscription-item",
413
- "properties": {
414
- "_cart_template": "subscription" // Use subscription template
415
- }
416
- },
417
- {
418
- "key": "bundle-item",
419
- "properties": {
420
- "_cart_template": "bundle" // Use bundle template
421
- }
422
- }
423
- ]
338
+ "key": "subscription-item",
339
+ "properties": {
340
+ "_cart_template": "subscription"
341
+ }
424
342
  }
425
343
  ```
426
344
 
427
- Then set up custom templates in JavaScript:
428
-
429
- ```javascript
430
- import { CartItem } from '@magic-spells/cart-panel';
431
-
432
- // Set up different templates
433
- CartItem.setTemplate('subscription', (itemData, cartData) => {
434
- return `
435
- <div class="subscription-item">
436
- <div class="recurring-badge">🔄 Subscription</div>
437
- <h4>${itemData.product_title}</h4>
438
- <div class="price">$${(itemData.price / 100).toFixed(2)} every month</div>
439
- <quantity-modifier value="${itemData.quantity}"></quantity-modifier>
440
- </div>
441
- `;
442
- });
443
-
444
- CartItem.setTemplate('bundle', (itemData, cartData) => {
445
- return `
446
- <div class="bundle-item">
447
- <div class="bundle-badge">📦 Bundle Deal</div>
448
- <h4>${itemData.product_title}</h4>
449
- <div class="savings">Save 20%!</div>
450
- <div class="price">$${(itemData.price / 100).toFixed(2)}</div>
451
- </div>
452
- `;
453
- });
454
- ```
455
-
456
- #### Item Grouping (`_group_id` and `_group_role`)
457
-
458
- Items can be grouped together using `_group_id` and `_group_role` properties. This is commonly used for bundle products where multiple items should be displayed as a single unit.
459
-
460
- **Use Cases:**
461
- - Bundle products (main product + accessories)
462
- - Gift with purchase promotions
463
- - Subscription boxes with multiple items
464
- - Product kits and sets
345
+ ### Bundle Grouping (`_group_id` / `_group_role`)
465
346
 
466
- **How it works:**
467
- 1. All items in a group share the same `_group_id` (a unique identifier like a UUID)
468
- 2. One item has `_group_role: "parent"` (typically with `_cart_template: "bundle"`)
469
- 3. Other items have `_group_role: "child"` (typically with `_hide_in_cart: true`)
470
- 4. The bundle template renders all grouped items together in one display
471
-
472
- **Example usage:**
473
347
  ```javascript
474
- // Bundle: T-shirt + Hat + Sticker (shown as one item in cart)
348
+ // Bundle: Parent shows, children hidden
475
349
  {
476
350
  "items": [
477
351
  {
@@ -484,17 +358,9 @@ Items can be grouped together using `_group_id` and `_group_role` properties. Th
484
358
  },
485
359
  {
486
360
  "key": "bundle-child-1",
487
- "properties": {
488
- "_group_id": "Q6RT1B48",
489
- "_group_role": "child",
490
- "_hide_in_cart": "true"
491
- }
492
- },
493
- {
494
- "key": "bundle-child-2",
495
361
  "properties": {
496
362
  "_group_id": "Q6RT1B48",
497
- "_group_role": "child",
363
+ "_group_role": "child",
498
364
  "_hide_in_cart": "true"
499
365
  }
500
366
  }
@@ -502,45 +368,10 @@ Items can be grouped together using `_group_id` and `_group_role` properties. Th
502
368
  }
503
369
  ```
504
370
 
505
- **Bundle template example:**
506
- ```javascript
507
- CartItem.setTemplate('bundle', (itemData, cartData) => {
508
- // Find all items in this group
509
- const groupId = itemData.properties._group_id;
510
- const groupItems = cartData.items.filter(item =>
511
- item.properties?._group_id === groupId
512
- );
513
-
514
- return `
515
- <div class="bundle-item">
516
- <div class="bundle-badge">📦 Bundle Deal</div>
517
- <h4>${itemData.product_title}</h4>
518
- <div class="bundle-contents">
519
- ${groupItems.map(item => `
520
- <div class="bundle-item-detail">
521
- • ${item.product_title} (${item.quantity})
522
- </div>
523
- `).join('')}
524
- </div>
525
- <div class="bundle-price">$${(groupItems.reduce((sum, item) => sum + item.line_price, 0) / 100).toFixed(2)}</div>
526
- </div>
527
- `;
528
- });
529
- ```
530
-
531
- #### Subtotal Exclusion (`_ignore_price_in_subtotal`)
532
-
533
- Items can be excluded from subtotal calculations using the `_ignore_price_in_subtotal` property. This is useful for promotional items that receive automatic discounts at checkout.
371
+ ### Subtotal Exclusion (`_ignore_price_in_subtotal`)
534
372
 
535
- **Use Cases:**
536
- - Gift with purchase items (free items that show $0 at checkout)
537
- - Promotional items with automatic discounts applied later
538
- - Service fees handled by other systems
539
- - Items with complex pricing logic
540
-
541
- **Usage:**
542
373
  ```javascript
543
- // Gift with purchase item - shows in cart but excluded from subtotal
374
+ // Gift item excluded from subtotal calculation
544
375
  {
545
376
  "key": "gift-item",
546
377
  "properties": {
@@ -549,135 +380,52 @@ Items can be excluded from subtotal calculations using the `_ignore_price_in_sub
549
380
  }
550
381
  ```
551
382
 
552
- **Implementation:**
553
- The cart panel automatically excludes these items when calculating visible subtotals, but they remain in the cart for Shopify's checkout process where discounts are applied.
554
-
555
- #### Supported Properties
556
-
557
- | Property | Purpose | Example Values |
558
- | -------------------------- | --------------------------------------------- | -------------------------------------- |
559
- | `_hide_in_cart` | Hide items from cart display | `"true"`, `true` |
560
- | `_cart_template` | Specify custom template for rendering | `"subscription"`, `"bundle"`, `"gift"` |
561
- | `_group_id` | Group items together with shared UUID | `"Q6RT1B48"`, `"ABC123XYZ"` |
562
- | `_group_role` | Role within a group | `"parent"`, `"child"` |
563
- | `_ignore_price_in_subtotal` | Exclude from subtotal calculations | `"true"`, `true` |
564
-
565
- These properties follow Shopify's line item properties pattern and are commonly used for gift-with-purchase items, subscription products, bundles, and other special cart items.
566
-
567
- ### Shopify Integration
568
-
569
- The cart panel automatically integrates with Shopify's AJAX Cart API. Simply add the component to your theme and it handles all cart operations:
383
+ ## Shopify Integration
570
384
 
571
385
  ```html
572
- <!-- In your Shopify theme layout -->
573
- <button
574
- aria-haspopup="dialog"
575
- aria-controls="shopify-cart"
576
- aria-expanded="false"
577
- class="cart-trigger">
578
- Cart ({{ cart.item_count }})
579
- </button>
580
-
581
- <cart-dialog id="shopify-cart" aria-labelledby="cart-heading">
582
- <cart-panel>
583
- <header class="cart-header">
584
- <h2 id="cart-heading">Your Cart</h2>
585
- <button data-action="hide-cart" aria-label="hide cart">X</button>
586
- </header>
587
-
588
- <div class="cart-content">
589
- <!-- Cart items will be populated automatically in javascript -->
590
- </div>
591
-
592
- <footer class="cart-footer">
593
- <div class="cart-total"></div>
594
- <a href="/checkout" class="button"> Checkout </a>
595
- </footer>
596
- </cart-panel>
597
- </cart-dialog>
386
+ <!-- Cart with dialog-panel wrapper -->
387
+ <dialog-panel id="cart-dialog">
388
+ <dialog aria-labelledby="cart-title">
389
+ <cart-panel>
390
+ <div class="cart-header">
391
+ <h2 id="cart-title">Your Cart</h2>
392
+ <button aria-label="Close cart" data-action-hide-cart>&times;</button>
393
+ </div>
394
+ <div class="cart-body">
395
+ <div data-cart-has-items>
396
+ <div class="cart-items" data-content-cart-items></div>
397
+ </div>
398
+ <div data-cart-is-empty>
399
+ <p>Your cart is empty</p>
400
+ </div>
401
+ </div>
402
+ <footer class="cart-footer">
403
+ <div class="cart-summary">
404
+ <span data-content-cart-count></span> items |
405
+ <span data-content-cart-subtotal></span>
406
+ </div>
407
+ <a href="/checkout" class="checkout-button">Checkout</a>
408
+ </footer>
409
+ </cart-panel>
410
+ </dialog>
411
+ </dialog-panel>
598
412
 
599
413
  <script>
600
- // Optional: Listen for cart updates to sync with other UI elements
601
- document.querySelector('cart-dialog').on('cart-dialog:data-changed', (cartData) => {
602
- // Update cart count in header
603
- document.querySelector('.cart-trigger').textContent = `Cart (${cartData.item_count})`;
604
-
605
- // Update cart total
606
- document.querySelector('[data-cart-total]').textContent = new Intl.NumberFormat('en-US', {
607
- style: 'currency',
608
- currency: 'USD',
609
- }).format(cartData.total_price / 100);
414
+ const cartPanel = document.querySelector('cart-panel');
415
+
416
+ // Update header cart count on changes
417
+ cartPanel.on('cart-panel:data-changed', (e) => {
418
+ document.querySelector('.header-cart-count').textContent =
419
+ e.detail.calculated_count;
610
420
  });
611
421
  </script>
612
422
  ```
613
423
 
614
- ### Vanilla JavaScript Integration
615
-
616
- ```javascript
617
- // Example for non-Shopify platforms
618
- class CustomCartManager {
619
- constructor() {
620
- this.cartDialog = document.querySelector('cart-dialog');
621
- this.setupEventListeners();
622
- }
623
-
624
- setupEventListeners() {
625
- // Listen for cart data changes
626
- this.cartDialog.on('cart-dialog:data-changed', (cartData) => {
627
- this.updateCartUI(cartData);
628
- });
629
-
630
- // Override default cart operations for custom API
631
- this.cartDialog.getCart = this.customGetCart.bind(this);
632
- this.cartDialog.updateCartItem = this.customUpdateCartItem.bind(this);
633
- }
634
-
635
- async customGetCart() {
636
- try {
637
- const response = await fetch('/api/cart');
638
- return await response.json();
639
- } catch (error) {
640
- console.error('Failed to fetch cart:', error);
641
- return { error: true, message: error.message };
642
- }
643
- }
424
+ ## Dependencies
644
425
 
645
- async customUpdateCartItem(itemId, quantity) {
646
- try {
647
- const response = await fetch('/api/cart/update', {
648
- method: 'POST',
649
- headers: { 'Content-Type': 'application/json' },
650
- body: JSON.stringify({ itemId, quantity }),
651
- });
652
-
653
- if (!response.ok) throw new Error(response.statusText);
654
-
655
- // Return updated cart data
656
- return this.customGetCart();
657
- } catch (error) {
658
- console.error('Failed to update cart:', error);
659
- return { error: true, message: error.message };
660
- }
661
- }
662
-
663
- updateCartUI(cartData) {
664
- // Update cart count in navigation
665
- const cartCount = document.querySelector('.cart-count');
666
- if (cartCount) {
667
- cartCount.textContent = cartData.items?.length || 0;
668
- }
669
-
670
- // Update cart total display
671
- const cartTotal = document.querySelector('.cart-total-display');
672
- if (cartTotal && cartData.total) {
673
- cartTotal.textContent = cartData.total;
674
- }
675
- }
676
- }
677
-
678
- // Initialize
679
- new CustomCartManager();
680
- ```
426
+ - `@magic-spells/event-emitter` - Event system (bundled)
427
+ - `@magic-spells/dialog-panel` - Modal behavior (peer dependency, optional)
428
+ - `@magic-spells/quantity-input` - Quantity controls (optional, for templates)
681
429
 
682
430
  ## Browser Support
683
431