@magic-spells/cart-panel 0.1.0
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 +537 -0
- package/dist/cart-panel.cjs.css +171 -0
- package/dist/cart-panel.cjs.js +473 -0
- package/dist/cart-panel.cjs.js.map +1 -0
- package/dist/cart-panel.css +171 -0
- package/dist/cart-panel.esm.css +171 -0
- package/dist/cart-panel.esm.js +466 -0
- package/dist/cart-panel.esm.js.map +1 -0
- package/dist/cart-panel.js +969 -0
- package/dist/cart-panel.js.map +1 -0
- package/dist/cart-panel.min.css +1 -0
- package/dist/cart-panel.min.js +1 -0
- package/dist/cart-panel.scss +117 -0
- package/package.json +82 -0
- package/src/cart-panel.js +470 -0
- package/src/cart-panel.scss +117 -0
package/README.md
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# Cart Panel Web Component
|
|
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.
|
|
4
|
+
|
|
5
|
+
[**Live Demo**](https://magic-spells.github.io/cart-panel/demo/)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
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
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @magic-spells/cart-panel
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// Import the component (includes cart-item automatically)
|
|
27
|
+
import '@magic-spells/cart-panel';
|
|
28
|
+
|
|
29
|
+
// Import styles (includes cart-item styles automatically)
|
|
30
|
+
import '@magic-spells/cart-panel/css';
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or include directly in your HTML:
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<script src="https://unpkg.com/@magic-spells/cart-panel"></script>
|
|
37
|
+
<link rel="stylesheet" href="https://unpkg.com/@magic-spells/cart-panel/dist/cart-panel.css" />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<!-- Trigger button -->
|
|
44
|
+
<button aria-haspopup="dialog" aria-controls="my-cart" aria-expanded="false">
|
|
45
|
+
Open Cart (3 items)
|
|
46
|
+
</button>
|
|
47
|
+
|
|
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">×</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>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="quantity-controls">
|
|
68
|
+
<input type="number" data-cart-quantity value="1" min="1" />
|
|
69
|
+
<button data-action="remove">Remove</button>
|
|
70
|
+
</div>
|
|
71
|
+
</cart-item-content>
|
|
72
|
+
<cart-item-processing>
|
|
73
|
+
<div>Processing...</div>
|
|
74
|
+
</cart-item-processing>
|
|
75
|
+
</cart-item>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
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>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## How It Works
|
|
87
|
+
|
|
88
|
+
The cart panel component creates a complete modal cart experience with three main elements:
|
|
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
|
|
93
|
+
|
|
94
|
+
The component automatically handles:
|
|
95
|
+
|
|
96
|
+
- Opening when trigger buttons with `aria-controls` are clicked
|
|
97
|
+
- Closing via close buttons, escape key, or overlay clicks
|
|
98
|
+
- Fetching cart data from `/cart.json` on show
|
|
99
|
+
- Updating cart items via `/cart/change.json` API calls
|
|
100
|
+
- Managing cart item states and animations through integrated `@magic-spells/cart-item`
|
|
101
|
+
- Emitting events for cart updates and state changes
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
### Cart Dialog Attributes
|
|
106
|
+
|
|
107
|
+
| Attribute | Description | Required |
|
|
108
|
+
| ----------------- | ----------------------------------------------- | ----------- |
|
|
109
|
+
| `id` | Unique identifier referenced by trigger buttons | Yes |
|
|
110
|
+
| `aria-labelledby` | References the cart title element | Recommended |
|
|
111
|
+
| `aria-modal` | Set to "true" for proper modal semantics | Recommended |
|
|
112
|
+
|
|
113
|
+
### Required HTML Structure
|
|
114
|
+
|
|
115
|
+
| Element | Description | Required |
|
|
116
|
+
| ---------------- | -------------------------------------------- | -------- |
|
|
117
|
+
| `<cart-dialog>` | Main modal container | Yes |
|
|
118
|
+
| `<cart-panel>` | Sliding content area | Yes |
|
|
119
|
+
| `<cart-overlay>` | Background overlay (auto-created if missing) | No |
|
|
120
|
+
|
|
121
|
+
### Interactive Elements
|
|
122
|
+
|
|
123
|
+
| Selector | Description | Event Triggered |
|
|
124
|
+
| --------------------------- | ----------------------------------- | --------------------------- |
|
|
125
|
+
| `[aria-controls="cart-id"]` | Trigger buttons to open cart | Opens modal |
|
|
126
|
+
| `[data-action="hide-cart"]` | Close buttons inside modal | Closes modal |
|
|
127
|
+
| `[data-action="remove"]` | Remove item buttons (via cart-item) | `cart-item:remove` |
|
|
128
|
+
| `[data-cart-quantity]` | Quantity inputs (via cart-item) | `cart-item:quantity-change` |
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<!-- Minimal cart modal -->
|
|
134
|
+
<cart-dialog id="simple-cart">
|
|
135
|
+
<cart-panel>
|
|
136
|
+
<h2>Cart</h2>
|
|
137
|
+
<button data-action="hide-cart">Close</button>
|
|
138
|
+
<!-- Cart content here -->
|
|
139
|
+
</cart-panel>
|
|
140
|
+
</cart-dialog>
|
|
141
|
+
|
|
142
|
+
<!-- Complete cart with all features -->
|
|
143
|
+
<cart-dialog id="full-cart" aria-modal="true" aria-labelledby="cart-heading">
|
|
144
|
+
<cart-overlay></cart-overlay>
|
|
145
|
+
<cart-panel>
|
|
146
|
+
<header class="cart-header">
|
|
147
|
+
<h2 id="cart-heading">Shopping Cart</h2>
|
|
148
|
+
<button data-action="hide-cart" aria-label="Close cart">×</button>
|
|
149
|
+
</header>
|
|
150
|
+
<div class="cart-content">
|
|
151
|
+
<!-- Cart items will be rendered here -->
|
|
152
|
+
</div>
|
|
153
|
+
<footer class="cart-footer">
|
|
154
|
+
<button class="checkout-btn">Checkout</button>
|
|
155
|
+
</footer>
|
|
156
|
+
</cart-panel>
|
|
157
|
+
</cart-dialog>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Customization
|
|
161
|
+
|
|
162
|
+
### Styling
|
|
163
|
+
|
|
164
|
+
The component provides complete styling control through CSS custom properties and SCSS variables. Customize the modal appearance to match your design:
|
|
165
|
+
|
|
166
|
+
```css
|
|
167
|
+
/* Customize modal positioning and sizing */
|
|
168
|
+
cart-dialog {
|
|
169
|
+
--cart-panel-width: min(500px, 95vw);
|
|
170
|
+
--cart-panel-z-index: 9999;
|
|
171
|
+
--cart-overlay-z-index: 9998;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Customize overlay appearance */
|
|
175
|
+
cart-overlay {
|
|
176
|
+
--cart-overlay-background: rgba(0, 0, 0, 0.3);
|
|
177
|
+
--cart-overlay-backdrop-filter: blur(8px);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Customize panel styling */
|
|
181
|
+
cart-panel {
|
|
182
|
+
--cart-panel-background: #ffffff;
|
|
183
|
+
--cart-panel-shadow: -10px 0 30px rgba(0, 0, 0, 0.2);
|
|
184
|
+
--cart-panel-border-radius: 12px 0 0 12px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Customize animations */
|
|
188
|
+
cart-dialog {
|
|
189
|
+
--cart-transition-duration: 400ms;
|
|
190
|
+
--cart-transition-timing: cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Style your cart content layout */
|
|
194
|
+
cart-panel {
|
|
195
|
+
display: flex;
|
|
196
|
+
flex-direction: column;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.cart-header {
|
|
200
|
+
padding: 1.5rem;
|
|
201
|
+
border-bottom: 1px solid #eee;
|
|
202
|
+
background: #f8f9fa;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.cart-content {
|
|
206
|
+
flex: 1;
|
|
207
|
+
overflow-y: auto;
|
|
208
|
+
padding: 1rem;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.cart-footer {
|
|
212
|
+
padding: 1.5rem;
|
|
213
|
+
border-top: 1px solid #eee;
|
|
214
|
+
background: #f8f9fa;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### CSS Variables & SCSS Variables
|
|
219
|
+
|
|
220
|
+
The component supports both CSS custom properties and SCSS variables for maximum flexibility:
|
|
221
|
+
|
|
222
|
+
| CSS Variable | SCSS Variable | Description | Default |
|
|
223
|
+
| -------------------------------- | ------------------------------- | ---------------------------- | ---------------------------- |
|
|
224
|
+
| `--cart-dialog-z-index` | `$cart-dialog-z-index` | Base z-index for modal | 1000 |
|
|
225
|
+
| `--cart-overlay-z-index` | `$cart-overlay-z-index` | Overlay layer z-index | 1000 |
|
|
226
|
+
| `--cart-panel-z-index` | `$cart-panel-z-index` | Panel layer z-index | 1001 |
|
|
227
|
+
| `--cart-panel-width` | `$cart-panel-width` | Width of the sliding panel | min(400px, 90vw) |
|
|
228
|
+
| `--cart-overlay-background` | `$cart-overlay-background` | Overlay background color | rgba(0, 0, 0, 0.15) |
|
|
229
|
+
| `--cart-overlay-backdrop-filter` | `$cart-overlay-backdrop-filter` | Overlay backdrop blur effect | blur(4px) |
|
|
230
|
+
| `--cart-panel-background` | `$cart-panel-background` | Panel background color | #ffffff |
|
|
231
|
+
| `--cart-panel-shadow` | `$cart-panel-shadow` | Panel box shadow | -5px 0 25px rgba(0,0,0,0.15) |
|
|
232
|
+
| `--cart-panel-border-radius` | `$cart-panel-border-radius` | Panel border radius | 0 |
|
|
233
|
+
| `--cart-transition-duration` | `$cart-transition-duration` | Animation duration | 350ms |
|
|
234
|
+
| `--cart-transition-timing` | `$cart-transition-timing` | Animation timing function | cubic-bezier(0.4, 0, 0.2, 1) |
|
|
235
|
+
|
|
236
|
+
#### CSS Override Examples:
|
|
237
|
+
|
|
238
|
+
```css
|
|
239
|
+
/* Dramatic slide-in effect */
|
|
240
|
+
.dramatic-cart {
|
|
241
|
+
--cart-transition-duration: 600ms;
|
|
242
|
+
--cart-transition-timing: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
243
|
+
--cart-overlay-background: rgba(0, 0, 0, 0.4);
|
|
244
|
+
--cart-overlay-backdrop-filter: blur(10px);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Subtle minimal styling */
|
|
248
|
+
.minimal-cart {
|
|
249
|
+
--cart-panel-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
250
|
+
--cart-panel-border-radius: 8px;
|
|
251
|
+
--cart-transition-duration: 200ms;
|
|
252
|
+
--cart-overlay-background: rgba(0, 0, 0, 0.05);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Mobile-optimized full-width */
|
|
256
|
+
@media (max-width: 768px) {
|
|
257
|
+
.mobile-cart {
|
|
258
|
+
--cart-panel-width: 100vw;
|
|
259
|
+
--cart-panel-border-radius: 0;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### SCSS Override Examples:
|
|
265
|
+
|
|
266
|
+
```scss
|
|
267
|
+
// Override SCSS variables before importing
|
|
268
|
+
$cart-panel-width: min(500px, 95vw);
|
|
269
|
+
$cart-transition-duration: 400ms;
|
|
270
|
+
$cart-overlay-background: rgba(0, 0, 0, 0.25);
|
|
271
|
+
|
|
272
|
+
// Import the component styles
|
|
273
|
+
@import '@magic-spells/cart-panel/scss';
|
|
274
|
+
|
|
275
|
+
// Or import the CSS and override with CSS custom properties
|
|
276
|
+
@import '@magic-spells/cart-panel/css';
|
|
277
|
+
|
|
278
|
+
.my-store cart-dialog {
|
|
279
|
+
--cart-transition-duration: 400ms;
|
|
280
|
+
--cart-panel-background: #f8f9fa;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### JavaScript API
|
|
285
|
+
|
|
286
|
+
#### Methods
|
|
287
|
+
|
|
288
|
+
- `show(triggerElement)`: Open the cart modal and focus the first interactive element
|
|
289
|
+
- `hide()`: Close the cart modal and restore focus to trigger element
|
|
290
|
+
- `getCart()`: Fetch current cart data from `/cart.json`
|
|
291
|
+
- `updateCartItem(key, quantity)`: Update cart item quantity via `/cart/change.json`
|
|
292
|
+
- `refreshCart()`: Refresh cart data and update UI components
|
|
293
|
+
- `on(eventName, callback)`: Add event listener using the event emitter
|
|
294
|
+
- `off(eventName, callback)`: Remove event listener
|
|
295
|
+
|
|
296
|
+
#### Events
|
|
297
|
+
|
|
298
|
+
The component emits custom events for cart state changes and data updates:
|
|
299
|
+
|
|
300
|
+
**Modal Events:**
|
|
301
|
+
|
|
302
|
+
- `cart-dialog:show` - Modal has opened
|
|
303
|
+
- `cart-dialog:hide` - Modal has started closing
|
|
304
|
+
- `cart-dialog:afterHide` - Modal has finished closing animation
|
|
305
|
+
|
|
306
|
+
**Cart Data Events:**
|
|
307
|
+
|
|
308
|
+
- `cart-dialog:updated` - Cart data updated after item change
|
|
309
|
+
- `cart-dialog:refreshed` - Cart data refreshed from server
|
|
310
|
+
- `cart-dialog:data-changed` - Any cart data change (unified event)
|
|
311
|
+
|
|
312
|
+
**Cart Item Events (bubbled from cart-item components):**
|
|
313
|
+
|
|
314
|
+
- `cart-item:remove` - Remove button clicked: `{ cartKey, element }`
|
|
315
|
+
- `cart-item:quantity-change` - Quantity changed: `{ cartKey, quantity, element }`
|
|
316
|
+
|
|
317
|
+
#### Programmatic Control
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
const cartDialog = document.querySelector('cart-dialog');
|
|
321
|
+
|
|
322
|
+
// Open/close cart
|
|
323
|
+
cartDialog.show(); // Open modal
|
|
324
|
+
cartDialog.hide(); // Close modal
|
|
325
|
+
|
|
326
|
+
// Cart data operations
|
|
327
|
+
const cartData = await cartDialog.getCart();
|
|
328
|
+
const updatedCart = await cartDialog.updateCartItem('item-key', 2);
|
|
329
|
+
await cartDialog.refreshCart();
|
|
330
|
+
|
|
331
|
+
// Event emitter pattern (recommended)
|
|
332
|
+
cartDialog
|
|
333
|
+
.on('cart-dialog:show', (e) => {
|
|
334
|
+
console.log('Cart opened by:', e.detail.triggerElement);
|
|
335
|
+
})
|
|
336
|
+
.on('cart-dialog:data-changed', (cartData) => {
|
|
337
|
+
console.log('Cart updated:', cartData);
|
|
338
|
+
// Update header cart count, etc.
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Traditional event listeners (also supported)
|
|
342
|
+
cartDialog.addEventListener('cart-item:remove', (e) => {
|
|
343
|
+
console.log('Remove requested:', e.detail.cartKey);
|
|
344
|
+
|
|
345
|
+
// The component handles the API calls automatically
|
|
346
|
+
// Just listen for the data changes
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
cartDialog.addEventListener('cart-item:quantity-change', (e) => {
|
|
350
|
+
console.log('Quantity changed:', e.detail.quantity);
|
|
351
|
+
// Component automatically syncs with Shopify
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Listen for all cart changes
|
|
355
|
+
cartDialog.on('cart-dialog:data-changed', (cartData) => {
|
|
356
|
+
// Update your UI when cart changes
|
|
357
|
+
updateCartBadge(cartData.item_count);
|
|
358
|
+
updateCartTotal(cartData.total_price);
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### Performance & Architecture
|
|
363
|
+
|
|
364
|
+
The component is optimized for:
|
|
365
|
+
|
|
366
|
+
- **Smooth animations**: CSS transforms and transitions for slide-in effects
|
|
367
|
+
- **Focus management**: Automatic focus trapping with `@magic-spells/focus-trap`
|
|
368
|
+
- **Memory management**: Proper event listener cleanup on disconnect
|
|
369
|
+
- **Scroll lock**: Body scroll prevention with position restoration
|
|
370
|
+
- **API efficiency**: Smart cart data fetching and caching
|
|
371
|
+
- **Event system**: Centralized event handling with custom event emitter
|
|
372
|
+
- **Accessibility**: Full ARIA support and keyboard navigation
|
|
373
|
+
|
|
374
|
+
## Integration Examples
|
|
375
|
+
|
|
376
|
+
### Shopify Integration
|
|
377
|
+
|
|
378
|
+
The cart panel automatically integrates with Shopify's AJAX Cart API. Simply add the component to your theme and it handles all cart operations:
|
|
379
|
+
|
|
380
|
+
```liquid
|
|
381
|
+
<!-- In your Shopify theme layout -->
|
|
382
|
+
<button
|
|
383
|
+
aria-haspopup="dialog"
|
|
384
|
+
aria-controls="shopify-cart"
|
|
385
|
+
aria-expanded="false"
|
|
386
|
+
class="cart-trigger">
|
|
387
|
+
Cart ({{ cart.item_count }})
|
|
388
|
+
</button>
|
|
389
|
+
|
|
390
|
+
<cart-dialog id="shopify-cart" aria-labelledby="cart-heading">
|
|
391
|
+
<cart-panel>
|
|
392
|
+
<header class="cart-header">
|
|
393
|
+
<h2 id="cart-heading">{{ 'cart.general.title' | t }}</h2>
|
|
394
|
+
<button data-action="hide-cart" aria-label="{{ 'cart.general.close' | t }}">
|
|
395
|
+
{% render 'icon-close' %}
|
|
396
|
+
</button>
|
|
397
|
+
</header>
|
|
398
|
+
|
|
399
|
+
<div class="cart-content">
|
|
400
|
+
<!-- Cart items will be populated automatically -->
|
|
401
|
+
{% for item in cart.items %}
|
|
402
|
+
<cart-item data-key="{{ item.key }}">
|
|
403
|
+
<cart-item-content>
|
|
404
|
+
<div class="cart-item-layout">
|
|
405
|
+
<img src="{{ item.image | img_url: '100x100' }}" alt="{{ item.title | escape }}">
|
|
406
|
+
<div class="item-details">
|
|
407
|
+
<h4>{{ item.product.title }}</h4>
|
|
408
|
+
{% unless item.variant.title == 'Default Title' %}
|
|
409
|
+
<p class="variant">{{ item.variant.title }}</p>
|
|
410
|
+
{% endunless %}
|
|
411
|
+
<p class="price">{{ item.final_price | money }}</p>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="item-controls">
|
|
414
|
+
<input
|
|
415
|
+
type="number"
|
|
416
|
+
data-cart-quantity
|
|
417
|
+
value="{{ item.quantity }}"
|
|
418
|
+
min="0">
|
|
419
|
+
<button data-action="remove">{{ 'cart.general.remove' | t }}</button>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</cart-item-content>
|
|
423
|
+
<cart-item-processing>
|
|
424
|
+
<div class="spinner"></div>
|
|
425
|
+
<span>{{ 'cart.general.updating' | t }}</span>
|
|
426
|
+
</cart-item-processing>
|
|
427
|
+
</cart-item>
|
|
428
|
+
{% endfor %}
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<footer class="cart-footer">
|
|
432
|
+
<div class="cart-total">
|
|
433
|
+
{{ 'cart.general.total' | t }}: <span data-cart-total>{{ cart.total_price | money }}</span>
|
|
434
|
+
</div>
|
|
435
|
+
<a href="/checkout" class="checkout-btn">
|
|
436
|
+
{{ 'cart.general.checkout' | t }}
|
|
437
|
+
</a>
|
|
438
|
+
</footer>
|
|
439
|
+
</cart-panel>
|
|
440
|
+
</cart-dialog>
|
|
441
|
+
|
|
442
|
+
<script>
|
|
443
|
+
// Optional: Listen for cart updates to sync with other UI elements
|
|
444
|
+
document.querySelector('cart-dialog').on('cart-dialog:data-changed', (cartData) => {
|
|
445
|
+
// Update cart count in header
|
|
446
|
+
document.querySelector('.cart-trigger').textContent = `Cart (${cartData.item_count})`;
|
|
447
|
+
|
|
448
|
+
// Update cart total
|
|
449
|
+
document.querySelector('[data-cart-total]').textContent =
|
|
450
|
+
new Intl.NumberFormat('en-US', {
|
|
451
|
+
style: 'currency',
|
|
452
|
+
currency: 'USD'
|
|
453
|
+
}).format(cartData.total_price / 100);
|
|
454
|
+
});
|
|
455
|
+
</script>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Vanilla JavaScript Integration
|
|
459
|
+
|
|
460
|
+
```javascript
|
|
461
|
+
// Example for non-Shopify platforms
|
|
462
|
+
class CustomCartManager {
|
|
463
|
+
constructor() {
|
|
464
|
+
this.cartDialog = document.querySelector('cart-dialog');
|
|
465
|
+
this.setupEventListeners();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
setupEventListeners() {
|
|
469
|
+
// Listen for cart data changes
|
|
470
|
+
this.cartDialog.on('cart-dialog:data-changed', (cartData) => {
|
|
471
|
+
this.updateCartUI(cartData);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Override default cart operations for custom API
|
|
475
|
+
this.cartDialog.getCart = this.customGetCart.bind(this);
|
|
476
|
+
this.cartDialog.updateCartItem = this.customUpdateCartItem.bind(this);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async customGetCart() {
|
|
480
|
+
try {
|
|
481
|
+
const response = await fetch('/api/cart');
|
|
482
|
+
return await response.json();
|
|
483
|
+
} catch (error) {
|
|
484
|
+
console.error('Failed to fetch cart:', error);
|
|
485
|
+
return { error: true, message: error.message };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async customUpdateCartItem(itemId, quantity) {
|
|
490
|
+
try {
|
|
491
|
+
const response = await fetch('/api/cart/update', {
|
|
492
|
+
method: 'POST',
|
|
493
|
+
headers: { 'Content-Type': 'application/json' },
|
|
494
|
+
body: JSON.stringify({ itemId, quantity }),
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
if (!response.ok) throw new Error(response.statusText);
|
|
498
|
+
|
|
499
|
+
// Return updated cart data
|
|
500
|
+
return this.customGetCart();
|
|
501
|
+
} catch (error) {
|
|
502
|
+
console.error('Failed to update cart:', error);
|
|
503
|
+
return { error: true, message: error.message };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
updateCartUI(cartData) {
|
|
508
|
+
// Update cart count in navigation
|
|
509
|
+
const cartCount = document.querySelector('.cart-count');
|
|
510
|
+
if (cartCount) {
|
|
511
|
+
cartCount.textContent = cartData.items?.length || 0;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Update cart total display
|
|
515
|
+
const cartTotal = document.querySelector('.cart-total-display');
|
|
516
|
+
if (cartTotal && cartData.total) {
|
|
517
|
+
cartTotal.textContent = cartData.total;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Initialize
|
|
523
|
+
new CustomCartManager();
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Browser Support
|
|
527
|
+
|
|
528
|
+
- Chrome 54+
|
|
529
|
+
- Firefox 63+
|
|
530
|
+
- Safari 10.1+
|
|
531
|
+
- Edge 79+
|
|
532
|
+
|
|
533
|
+
All modern browsers with Web Components support.
|
|
534
|
+
|
|
535
|
+
## License
|
|
536
|
+
|
|
537
|
+
MIT
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
cart-item {
|
|
2
|
+
--cart-item-processing-duration: 250ms;
|
|
3
|
+
--cart-item-destroying-duration: 600ms;
|
|
4
|
+
--cart-item-shadow-color: rgba(0, 0, 0, 0.15);
|
|
5
|
+
--cart-item-shadow-color-strong: rgba(0, 0, 0, 0.5);
|
|
6
|
+
--cart-item-processing-bg: rgba(100, 100, 100, 0.2);
|
|
7
|
+
--cart-item-destroying-bg: rgba(0, 0, 0, 0.1);
|
|
8
|
+
--cart-item-processing-scale: 0.98;
|
|
9
|
+
--cart-item-destroying-scale: 0.85;
|
|
10
|
+
--cart-item-processing-blur: 1px;
|
|
11
|
+
--cart-item-destroying-blur: 10px;
|
|
12
|
+
--cart-item-destroying-opacity: 0.2;
|
|
13
|
+
--cart-item-destroying-brightness: 0.6;
|
|
14
|
+
--cart-item-destroying-saturate: 0.3;
|
|
15
|
+
display: block;
|
|
16
|
+
position: relative;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
padding: 0px;
|
|
19
|
+
box-shadow: inset 0px 0px 0px rgba(0, 0, 0, 0);
|
|
20
|
+
transition: filter var(--cart-item-processing-duration) ease-out, background-color var(--cart-item-processing-duration) ease-out, box-shadow var(--cart-item-processing-duration) ease-out;
|
|
21
|
+
}
|
|
22
|
+
cart-item::after {
|
|
23
|
+
content: "";
|
|
24
|
+
display: block;
|
|
25
|
+
position: absolute;
|
|
26
|
+
background: rgba(0, 0, 0, 0);
|
|
27
|
+
width: 100%;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
height: 100%;
|
|
30
|
+
top: 0px;
|
|
31
|
+
left: 0px;
|
|
32
|
+
transition: background-color var(--cart-item-processing-duration) ease;
|
|
33
|
+
}
|
|
34
|
+
cart-item[data-state=ready] cart-item-content {
|
|
35
|
+
transform: scale(1);
|
|
36
|
+
filter: blur(0px);
|
|
37
|
+
opacity: 1;
|
|
38
|
+
}
|
|
39
|
+
cart-item[data-state=ready] cart-item-processing {
|
|
40
|
+
opacity: 0;
|
|
41
|
+
visibility: hidden;
|
|
42
|
+
}
|
|
43
|
+
cart-item[data-state=processing] {
|
|
44
|
+
box-shadow: inset 0px 2px 10px var(--cart-item-shadow-color);
|
|
45
|
+
}
|
|
46
|
+
cart-item[data-state=processing]::after {
|
|
47
|
+
background: rgba(0, 0, 0, 0.15);
|
|
48
|
+
}
|
|
49
|
+
cart-item[data-state=processing] cart-item-content {
|
|
50
|
+
transform: scale(var(--cart-item-processing-scale));
|
|
51
|
+
filter: blur(var(--cart-item-processing-blur));
|
|
52
|
+
opacity: 0.9;
|
|
53
|
+
pointer-events: none;
|
|
54
|
+
}
|
|
55
|
+
cart-item[data-state=processing] cart-item-processing {
|
|
56
|
+
opacity: 1;
|
|
57
|
+
visibility: visible;
|
|
58
|
+
}
|
|
59
|
+
cart-item[data-state=destroying] {
|
|
60
|
+
background-color: var(--cart-item-destroying-bg);
|
|
61
|
+
box-shadow: inset 0px 4px 20px var(--cart-item-shadow-color-strong);
|
|
62
|
+
margin-top: 0px;
|
|
63
|
+
margin-bottom: 0px;
|
|
64
|
+
transition: filter var(--cart-item-destroying-duration) ease, background-color var(--cart-item-destroying-duration) ease, box-shadow var(--cart-item-destroying-duration) ease, margin var(--cart-item-destroying-duration) ease;
|
|
65
|
+
}
|
|
66
|
+
cart-item[data-state=destroying]::after {
|
|
67
|
+
background: rgba(0, 0, 0, 0.9);
|
|
68
|
+
transition: background-color var(--cart-item-destroying-duration) ease;
|
|
69
|
+
}
|
|
70
|
+
cart-item[data-state=destroying] cart-item-content {
|
|
71
|
+
transition: transform var(--cart-item-destroying-duration) ease, filter var(--cart-item-destroying-duration) ease, opacity var(--cart-item-destroying-duration) ease;
|
|
72
|
+
transform: scale(var(--cart-item-destroying-scale));
|
|
73
|
+
filter: blur(var(--cart-item-destroying-blur)) saturate(var(--cart-item-destroying-saturate));
|
|
74
|
+
opacity: var(--cart-item-destroying-opacity);
|
|
75
|
+
pointer-events: none;
|
|
76
|
+
}
|
|
77
|
+
cart-item[data-state=destroying] cart-item-processing {
|
|
78
|
+
opacity: 0;
|
|
79
|
+
transition: opacity var(--cart-item-processing-duration) ease;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
cart-item-content {
|
|
83
|
+
display: block;
|
|
84
|
+
transition: transform var(--cart-item-processing-duration) ease-out, filter var(--cart-item-processing-duration) ease-out, opacity var(--cart-item-processing-duration) ease-out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
cart-item-processing {
|
|
88
|
+
position: absolute;
|
|
89
|
+
top: 0;
|
|
90
|
+
left: 0;
|
|
91
|
+
right: 0;
|
|
92
|
+
bottom: 0;
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
background: transparent;
|
|
97
|
+
opacity: 0;
|
|
98
|
+
visibility: hidden;
|
|
99
|
+
transition: opacity var(--cart-item-processing-duration) ease-out, visibility var(--cart-item-processing-duration) ease-out;
|
|
100
|
+
z-index: 10;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
:root {
|
|
104
|
+
--cart-dialog-z-index: 1000;
|
|
105
|
+
--cart-overlay-z-index: 1000;
|
|
106
|
+
--cart-panel-z-index: 1001;
|
|
107
|
+
--cart-panel-width: min(400px, 90vw);
|
|
108
|
+
--cart-overlay-background: rgba(0, 0, 0, 0.15);
|
|
109
|
+
--cart-overlay-backdrop-filter: blur(4px);
|
|
110
|
+
--cart-panel-background: #ffffff;
|
|
111
|
+
--cart-panel-shadow: -5px 0 25px rgba(0, 0, 0, 0.15);
|
|
112
|
+
--cart-panel-border-radius: 0;
|
|
113
|
+
--cart-transition-duration: 350ms;
|
|
114
|
+
--cart-transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
cart-dialog {
|
|
118
|
+
display: contents;
|
|
119
|
+
}
|
|
120
|
+
cart-dialog[aria-hidden=false] cart-overlay,
|
|
121
|
+
cart-dialog[aria-hidden=false] cart-panel {
|
|
122
|
+
pointer-events: auto;
|
|
123
|
+
opacity: 1;
|
|
124
|
+
}
|
|
125
|
+
cart-dialog[aria-hidden=false] cart-panel {
|
|
126
|
+
transform: translateX(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
cart-overlay {
|
|
130
|
+
position: fixed;
|
|
131
|
+
top: 0;
|
|
132
|
+
left: 0;
|
|
133
|
+
width: 100vw;
|
|
134
|
+
height: 100vh;
|
|
135
|
+
opacity: 0;
|
|
136
|
+
pointer-events: none;
|
|
137
|
+
z-index: var(--cart-overlay-z-index);
|
|
138
|
+
background-color: var(--cart-overlay-background);
|
|
139
|
+
backdrop-filter: var(--cart-overlay-backdrop-filter);
|
|
140
|
+
transition: opacity var(--cart-transition-duration) var(--cart-transition-timing), backdrop-filter var(--cart-transition-duration) var(--cart-transition-timing);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
cart-panel {
|
|
144
|
+
position: fixed;
|
|
145
|
+
top: 0;
|
|
146
|
+
right: 0;
|
|
147
|
+
width: var(--cart-panel-width);
|
|
148
|
+
height: 100vh;
|
|
149
|
+
opacity: 0;
|
|
150
|
+
transform: translateX(100%);
|
|
151
|
+
pointer-events: none;
|
|
152
|
+
z-index: var(--cart-panel-z-index);
|
|
153
|
+
background: var(--cart-panel-background);
|
|
154
|
+
box-shadow: var(--cart-panel-shadow);
|
|
155
|
+
border-radius: var(--cart-panel-border-radius);
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
transition: opacity var(--cart-transition-duration) var(--cart-transition-timing), transform var(--cart-transition-duration) var(--cart-transition-timing);
|
|
158
|
+
}
|
|
159
|
+
cart-panel.hidden {
|
|
160
|
+
display: none;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
body.overflow-hidden {
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
position: fixed;
|
|
166
|
+
width: 100%;
|
|
167
|
+
height: 100%;
|
|
168
|
+
left: 0;
|
|
169
|
+
right: 0;
|
|
170
|
+
margin: 0;
|
|
171
|
+
}
|