@magic-spells/cart-panel 1.0.0 → 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 +269 -521
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
# Cart Panel Web Component
|
|
2
2
|
|
|
3
|
-
A professional, highly-customizable
|
|
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
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
×
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
</
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
88
|
+
The cart panel architecture consists of:
|
|
89
89
|
|
|
90
|
-
- **cart-
|
|
91
|
-
- **cart-
|
|
92
|
-
- **cart-
|
|
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
|
-
-
|
|
101
|
-
- Filtering out cart items with `
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
### Cart Dialog Attributes
|
|
103
|
+
### Key Architecture Decisions
|
|
107
104
|
|
|
108
|
-
|
|
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
|
-
|
|
107
|
+
2. **Native dialog features**: Focus trap, escape key, backdrop click are all handled by `<dialog-panel>` which wraps native `<dialog>`.
|
|
115
108
|
|
|
116
|
-
|
|
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
|
-
|
|
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-item]` | 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
|
-
|
|
113
|
+
### CartPanel Attributes
|
|
132
114
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
120
|
+
### Required HTML Structure
|
|
162
121
|
|
|
163
|
-
|
|
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
|
-
|
|
131
|
+
### CartItem Child Elements
|
|
166
132
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
cart-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
cart-
|
|
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
|
-
|
|
195
|
-
cart
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
###
|
|
164
|
+
### CartPanel Events
|
|
220
165
|
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
190
|
+
### CartItem Static Methods
|
|
266
191
|
|
|
267
|
-
```
|
|
268
|
-
|
|
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
|
-
//
|
|
274
|
-
|
|
195
|
+
// Set template globally
|
|
196
|
+
CartItem.setTemplate(name, templateFn)
|
|
275
197
|
|
|
276
|
-
//
|
|
277
|
-
|
|
198
|
+
// Set processing overlay template
|
|
199
|
+
CartItem.setProcessingTemplate(templateFn)
|
|
278
200
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
--cart-panel-background: #f8f9fa;
|
|
282
|
-
}
|
|
201
|
+
// Create with animation
|
|
202
|
+
CartItem.createAnimated(itemData, cartData)
|
|
283
203
|
```
|
|
284
204
|
|
|
285
|
-
###
|
|
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
|
|
208
|
+
const cartPanel = document.querySelector('cart-panel');
|
|
322
209
|
|
|
323
210
|
// Open/close cart
|
|
324
|
-
|
|
325
|
-
|
|
211
|
+
cartPanel.show();
|
|
212
|
+
cartPanel.hide();
|
|
326
213
|
|
|
327
214
|
// Cart data operations
|
|
328
|
-
const cartData = await
|
|
329
|
-
const updatedCart = await
|
|
330
|
-
await
|
|
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 (
|
|
333
|
-
|
|
334
|
-
.on('cart-
|
|
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-
|
|
338
|
-
console.log('Cart updated:',
|
|
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
|
|
343
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
+
### CSS Custom Properties
|
|
376
278
|
|
|
377
|
-
|
|
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
|
-
|
|
309
|
+
## Line Item Properties
|
|
380
310
|
|
|
381
|
-
|
|
311
|
+
The cart-panel supports Shopify line item properties for enhanced functionality:
|
|
382
312
|
|
|
383
|
-
|
|
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
|
-
|
|
386
|
-
- Cart count calculations
|
|
387
|
-
- Subtotal calculations
|
|
321
|
+
### Hidden Items (`_hide_in_cart`)
|
|
388
322
|
|
|
389
323
|
```javascript
|
|
390
|
-
//
|
|
324
|
+
// Item hidden from display but stays in actual cart
|
|
391
325
|
{
|
|
392
|
-
"
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
336
|
+
// Use subscription template for this item
|
|
409
337
|
{
|
|
410
|
-
"
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
<!--
|
|
573
|
-
<
|
|
574
|
-
aria-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
</
|
|
580
|
-
|
|
581
|
-
<
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
</
|
|
596
|
-
</
|
|
597
|
-
</
|
|
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>×</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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
|
package/package.json
CHANGED