@magic-spells/cart-panel 0.3.1 → 1.0.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 +7 -7
- package/dist/cart-panel.cjs.css +216 -193
- package/dist/cart-panel.cjs.js +692 -508
- package/dist/cart-panel.cjs.js.map +1 -1
- package/dist/cart-panel.css +216 -193
- package/dist/cart-panel.esm.css +216 -193
- package/dist/cart-panel.esm.js +686 -500
- package/dist/cart-panel.esm.js.map +1 -1
- package/dist/cart-panel.js +1054 -1716
- package/dist/cart-panel.js.map +1 -1
- package/dist/cart-panel.min.css +1 -1
- package/dist/cart-panel.min.js +1 -1
- package/package.json +12 -8
- package/src/cart-item.js +448 -0
- package/src/cart-panel.css +231 -0
- package/src/cart-panel.js +258 -517
- package/dist/cart-panel.scss +0 -107
- package/src/cart-panel.scss +0 -107
package/src/cart-item.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// CartItem Component
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CartItem class that handles the functionality of a cart item component
|
|
7
|
+
*/
|
|
8
|
+
class CartItem extends HTMLElement {
|
|
9
|
+
// Static template functions shared across all instances
|
|
10
|
+
static #templates = new Map();
|
|
11
|
+
static #processingTemplate = null;
|
|
12
|
+
|
|
13
|
+
// Private fields
|
|
14
|
+
#currentState = 'ready';
|
|
15
|
+
#isDestroying = false;
|
|
16
|
+
#isAppearing = false;
|
|
17
|
+
#handlers = {};
|
|
18
|
+
#itemData = null;
|
|
19
|
+
#cartData = null;
|
|
20
|
+
#lastRenderedHTML = '';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set the template function for rendering cart items
|
|
24
|
+
* @param {string} name - Template name ('default' for default template)
|
|
25
|
+
* @param {Function} templateFn - Function that takes (itemData, cartData) and returns HTML string
|
|
26
|
+
*/
|
|
27
|
+
static setTemplate(name, templateFn) {
|
|
28
|
+
if (typeof name !== 'string') {
|
|
29
|
+
throw new Error('Template name must be a string');
|
|
30
|
+
}
|
|
31
|
+
if (typeof templateFn !== 'function') {
|
|
32
|
+
throw new Error('Template must be a function');
|
|
33
|
+
}
|
|
34
|
+
CartItem.#templates.set(name, templateFn);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set the processing template function for rendering processing overlay
|
|
39
|
+
* @param {Function} templateFn - Function that returns HTML string for processing state
|
|
40
|
+
*/
|
|
41
|
+
static setProcessingTemplate(templateFn) {
|
|
42
|
+
if (typeof templateFn !== 'function') {
|
|
43
|
+
throw new Error('Processing template must be a function');
|
|
44
|
+
}
|
|
45
|
+
CartItem.#processingTemplate = templateFn;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a cart item with appearing animation
|
|
50
|
+
* @param {Object} itemData - Shopify cart item data
|
|
51
|
+
* @param {Object} cartData - Full Shopify cart object
|
|
52
|
+
* @returns {CartItem} Cart item instance that will animate in
|
|
53
|
+
*/
|
|
54
|
+
static createAnimated(itemData, cartData) {
|
|
55
|
+
return new CartItem(itemData, cartData, { animate: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Define which attributes should be observed for changes
|
|
60
|
+
*/
|
|
61
|
+
static get observedAttributes() {
|
|
62
|
+
return ['state', 'key'];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Called when observed attributes change
|
|
67
|
+
*/
|
|
68
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
69
|
+
if (oldValue === newValue) return;
|
|
70
|
+
|
|
71
|
+
if (name === 'state') {
|
|
72
|
+
this.#currentState = newValue || 'ready';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
constructor(itemData = null, cartData = null, options = {}) {
|
|
77
|
+
super();
|
|
78
|
+
|
|
79
|
+
// Store item and cart data if provided
|
|
80
|
+
this.#itemData = itemData;
|
|
81
|
+
this.#cartData = cartData;
|
|
82
|
+
|
|
83
|
+
// Set initial state - start with 'appearing' only if explicitly requested
|
|
84
|
+
const shouldAnimate = options.animate || this.hasAttribute('animate-in');
|
|
85
|
+
this.#currentState =
|
|
86
|
+
itemData && shouldAnimate ? 'appearing' : this.getAttribute('state') || 'ready';
|
|
87
|
+
|
|
88
|
+
// Bind event handlers
|
|
89
|
+
this.#handlers = {
|
|
90
|
+
click: this.#handleClick.bind(this),
|
|
91
|
+
change: this.#handleChange.bind(this),
|
|
92
|
+
transitionEnd: this.#handleTransitionEnd.bind(this),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
connectedCallback() {
|
|
97
|
+
const _ = this;
|
|
98
|
+
|
|
99
|
+
// If we have item data, render it first
|
|
100
|
+
if (_.#itemData) _.#render();
|
|
101
|
+
|
|
102
|
+
// Find child elements and attach listeners
|
|
103
|
+
_.#queryDOM();
|
|
104
|
+
_.#updateLinePriceElements();
|
|
105
|
+
_.#attachListeners();
|
|
106
|
+
|
|
107
|
+
// If we started with 'appearing' state, handle the entry animation
|
|
108
|
+
if (_.#currentState === 'appearing') {
|
|
109
|
+
_.setAttribute('state', 'appearing');
|
|
110
|
+
_.#isAppearing = true;
|
|
111
|
+
|
|
112
|
+
requestAnimationFrame(() => {
|
|
113
|
+
_.style.height = `${_.scrollHeight}px`;
|
|
114
|
+
requestAnimationFrame(() => _.setState('ready'));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
disconnectedCallback() {
|
|
120
|
+
// Cleanup event listeners
|
|
121
|
+
this.#detachListeners();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Query and cache DOM elements
|
|
126
|
+
*/
|
|
127
|
+
#queryDOM() {
|
|
128
|
+
this.content = this.querySelector('cart-item-content');
|
|
129
|
+
this.processing = this.querySelector('cart-item-processing');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Attach event listeners
|
|
134
|
+
*/
|
|
135
|
+
#attachListeners() {
|
|
136
|
+
const _ = this;
|
|
137
|
+
_.addEventListener('click', _.#handlers.click);
|
|
138
|
+
_.addEventListener('change', _.#handlers.change);
|
|
139
|
+
_.addEventListener('quantity-input:change', _.#handlers.change);
|
|
140
|
+
_.addEventListener('transitionend', _.#handlers.transitionEnd);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Detach event listeners
|
|
145
|
+
*/
|
|
146
|
+
#detachListeners() {
|
|
147
|
+
const _ = this;
|
|
148
|
+
_.removeEventListener('click', _.#handlers.click);
|
|
149
|
+
_.removeEventListener('change', _.#handlers.change);
|
|
150
|
+
_.removeEventListener('quantity-input:change', _.#handlers.change);
|
|
151
|
+
_.removeEventListener('transitionend', _.#handlers.transitionEnd);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the current state
|
|
156
|
+
*/
|
|
157
|
+
get state() {
|
|
158
|
+
return this.#currentState;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get the cart key for this item
|
|
163
|
+
*/
|
|
164
|
+
get cartKey() {
|
|
165
|
+
return this.getAttribute('key');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Handle click events (for Remove buttons, etc.)
|
|
170
|
+
*/
|
|
171
|
+
#handleClick(e) {
|
|
172
|
+
// Check if clicked element is a remove button
|
|
173
|
+
const removeButton = e.target.closest('[data-action-remove-item]');
|
|
174
|
+
if (removeButton) {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
this.#emitRemoveEvent();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Handle change events (for quantity inputs and quantity-input component)
|
|
182
|
+
*/
|
|
183
|
+
#handleChange(e) {
|
|
184
|
+
// Check if event is from quantity-input component
|
|
185
|
+
if (e.type === 'quantity-input:change') {
|
|
186
|
+
this.#emitQuantityChangeEvent(e.detail.value);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check if changed element is a quantity input
|
|
191
|
+
const quantityInput = e.target.closest('[data-cart-quantity]');
|
|
192
|
+
if (quantityInput) {
|
|
193
|
+
this.#emitQuantityChangeEvent(quantityInput.value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Handle transition end events for destroy animation and appearing animation
|
|
199
|
+
*/
|
|
200
|
+
#handleTransitionEnd(e) {
|
|
201
|
+
if (e.propertyName === 'height' && this.#isDestroying) {
|
|
202
|
+
// Remove from DOM after height animation completes
|
|
203
|
+
this.remove();
|
|
204
|
+
} else if (e.propertyName === 'height' && this.#isAppearing) {
|
|
205
|
+
// Remove explicit height after appearing animation completes
|
|
206
|
+
this.style.height = '';
|
|
207
|
+
this.#isAppearing = false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Emit remove event
|
|
213
|
+
*/
|
|
214
|
+
#emitRemoveEvent() {
|
|
215
|
+
this.dispatchEvent(
|
|
216
|
+
new CustomEvent('cart-item:remove', {
|
|
217
|
+
bubbles: true,
|
|
218
|
+
detail: {
|
|
219
|
+
cartKey: this.cartKey,
|
|
220
|
+
element: this,
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Emit quantity change event
|
|
228
|
+
*/
|
|
229
|
+
#emitQuantityChangeEvent(quantity) {
|
|
230
|
+
this.dispatchEvent(
|
|
231
|
+
new CustomEvent('cart-item:quantity-change', {
|
|
232
|
+
bubbles: true,
|
|
233
|
+
detail: {
|
|
234
|
+
cartKey: this.cartKey,
|
|
235
|
+
quantity: parseInt(quantity),
|
|
236
|
+
element: this,
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Render cart item from data using the appropriate template
|
|
244
|
+
*/
|
|
245
|
+
#render() {
|
|
246
|
+
const _ = this;
|
|
247
|
+
if (!_.#itemData || CartItem.#templates.size === 0) return;
|
|
248
|
+
|
|
249
|
+
// Set the key attribute from item data
|
|
250
|
+
const key = _.#itemData.key || _.#itemData.id;
|
|
251
|
+
if (key) _.setAttribute('key', key);
|
|
252
|
+
|
|
253
|
+
// Generate HTML from template and store for future comparisons
|
|
254
|
+
const templateHTML = _.#generateTemplateHTML();
|
|
255
|
+
_.#lastRenderedHTML = templateHTML;
|
|
256
|
+
|
|
257
|
+
// Generate processing HTML from template or use default
|
|
258
|
+
const processingHTML = CartItem.#processingTemplate
|
|
259
|
+
? CartItem.#processingTemplate()
|
|
260
|
+
: '<div class="cart-item-loader"></div>';
|
|
261
|
+
|
|
262
|
+
// Create the cart-item structure with template content inside cart-item-content
|
|
263
|
+
_.innerHTML = `
|
|
264
|
+
<cart-item-content>
|
|
265
|
+
${templateHTML}
|
|
266
|
+
</cart-item-content>
|
|
267
|
+
<cart-item-processing>
|
|
268
|
+
${processingHTML}
|
|
269
|
+
</cart-item-processing>
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Update the cart item with new data
|
|
275
|
+
* @param {Object} itemData - Shopify cart item data
|
|
276
|
+
* @param {Object} cartData - Full Shopify cart object
|
|
277
|
+
*/
|
|
278
|
+
setData(itemData, cartData = null) {
|
|
279
|
+
const _ = this;
|
|
280
|
+
|
|
281
|
+
// Update internal data
|
|
282
|
+
_.#itemData = itemData;
|
|
283
|
+
if (cartData) _.#cartData = cartData;
|
|
284
|
+
|
|
285
|
+
// Generate new HTML with updated data
|
|
286
|
+
const newHTML = _.#generateTemplateHTML();
|
|
287
|
+
|
|
288
|
+
// Compare with previously rendered HTML
|
|
289
|
+
if (newHTML === _.#lastRenderedHTML) {
|
|
290
|
+
// HTML hasn't changed, just reset processing state
|
|
291
|
+
_.setState('ready');
|
|
292
|
+
_.#updateQuantityInput();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// HTML is different, proceed with full update
|
|
297
|
+
_.setState('ready');
|
|
298
|
+
_.#render();
|
|
299
|
+
_.#queryDOM();
|
|
300
|
+
_.#updateLinePriceElements();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Generate HTML from the current template with current data
|
|
305
|
+
* @returns {string} Generated HTML string or empty string if no template
|
|
306
|
+
* @private
|
|
307
|
+
*/
|
|
308
|
+
#generateTemplateHTML() {
|
|
309
|
+
// If no templates are available, return empty string
|
|
310
|
+
if (!this.#itemData || CartItem.#templates.size === 0) {
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Determine which template to use
|
|
315
|
+
const templateName = this.#itemData.properties?._cart_template || 'default';
|
|
316
|
+
const templateFn = CartItem.#templates.get(templateName) || CartItem.#templates.get('default');
|
|
317
|
+
|
|
318
|
+
if (!templateFn) {
|
|
319
|
+
return '';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Generate and return HTML from template
|
|
323
|
+
return templateFn(this.#itemData, this.#cartData);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Update quantity input component to match server data
|
|
328
|
+
* @private
|
|
329
|
+
*/
|
|
330
|
+
#updateQuantityInput() {
|
|
331
|
+
if (!this.#itemData) return;
|
|
332
|
+
|
|
333
|
+
const quantityInput = this.querySelector('quantity-input');
|
|
334
|
+
if (quantityInput) {
|
|
335
|
+
quantityInput.value = this.#itemData.quantity;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Update elements with data-content-line-price attribute
|
|
341
|
+
* @private
|
|
342
|
+
*/
|
|
343
|
+
#updateLinePriceElements() {
|
|
344
|
+
if (!this.#itemData) return;
|
|
345
|
+
|
|
346
|
+
const linePriceElements = this.querySelectorAll('[data-content-line-price]');
|
|
347
|
+
const formattedLinePrice = this.#formatCurrency(this.#itemData.line_price || 0);
|
|
348
|
+
|
|
349
|
+
linePriceElements.forEach((element) => {
|
|
350
|
+
element.textContent = formattedLinePrice;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Format currency value from cents to dollar string
|
|
356
|
+
* @param {number} cents - Price in cents
|
|
357
|
+
* @returns {string} Formatted currency string (e.g., "$29.99")
|
|
358
|
+
* @private
|
|
359
|
+
*/
|
|
360
|
+
#formatCurrency(cents) {
|
|
361
|
+
if (typeof cents !== 'number') return '$0.00';
|
|
362
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get the current item data
|
|
367
|
+
*/
|
|
368
|
+
get itemData() {
|
|
369
|
+
return this.#itemData;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Set the state of the cart item
|
|
374
|
+
* @param {string} state - 'ready', 'processing', 'destroying', or 'appearing'
|
|
375
|
+
*/
|
|
376
|
+
setState(state) {
|
|
377
|
+
if (['ready', 'processing', 'destroying', 'appearing'].includes(state)) {
|
|
378
|
+
this.setAttribute('state', state);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Gracefully animate this cart item closed, then remove it
|
|
384
|
+
*/
|
|
385
|
+
destroyYourself() {
|
|
386
|
+
const _ = this;
|
|
387
|
+
|
|
388
|
+
// bail if already in the middle of a destroy cycle
|
|
389
|
+
if (_.#isDestroying) return;
|
|
390
|
+
_.#isDestroying = true;
|
|
391
|
+
|
|
392
|
+
// snapshot the current rendered height before applying any "destroying" styles
|
|
393
|
+
const initialHeight = _.offsetHeight;
|
|
394
|
+
_.setState('destroying');
|
|
395
|
+
|
|
396
|
+
// lock the measured height on the next animation frame to ensure layout is fully flushed
|
|
397
|
+
requestAnimationFrame(() => {
|
|
398
|
+
_.style.height = `${initialHeight}px`;
|
|
399
|
+
|
|
400
|
+
// read the css custom property for timing, defaulting to 400ms
|
|
401
|
+
const destroyDuration =
|
|
402
|
+
getComputedStyle(_).getPropertyValue('--cart-item-destroying-duration')?.trim() || '400ms';
|
|
403
|
+
|
|
404
|
+
// animate only the height to zero; other properties stay under stylesheet control
|
|
405
|
+
_.style.transition = `height ${destroyDuration} ease`;
|
|
406
|
+
_.style.height = '0px';
|
|
407
|
+
|
|
408
|
+
setTimeout(() => _.remove(), 600);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Supporting component classes for cart item
|
|
415
|
+
*/
|
|
416
|
+
class CartItemContent extends HTMLElement {
|
|
417
|
+
constructor() {
|
|
418
|
+
super();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
class CartItemProcessing extends HTMLElement {
|
|
423
|
+
constructor() {
|
|
424
|
+
super();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// =============================================================================
|
|
429
|
+
// Register Custom Elements
|
|
430
|
+
// =============================================================================
|
|
431
|
+
|
|
432
|
+
if (!customElements.get('cart-item')) {
|
|
433
|
+
customElements.define('cart-item', CartItem);
|
|
434
|
+
}
|
|
435
|
+
if (!customElements.get('cart-item-content')) {
|
|
436
|
+
customElements.define('cart-item-content', CartItemContent);
|
|
437
|
+
}
|
|
438
|
+
if (!customElements.get('cart-item-processing')) {
|
|
439
|
+
customElements.define('cart-item-processing', CartItemProcessing);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export { CartItem, CartItemContent, CartItemProcessing };
|
|
443
|
+
export default CartItem;
|
|
444
|
+
|
|
445
|
+
// Make CartItem available globally for Shopify themes
|
|
446
|
+
if (typeof window !== 'undefined') {
|
|
447
|
+
window.CartItem = CartItem;
|
|
448
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
Cart Panel Component
|
|
3
|
+
============================================================================= */
|
|
4
|
+
|
|
5
|
+
cart-panel {
|
|
6
|
+
display: block;
|
|
7
|
+
height: 100%;
|
|
8
|
+
overflow-y: auto;
|
|
9
|
+
overflow-x: hidden;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* =============================================================================
|
|
13
|
+
Cart Item Component
|
|
14
|
+
============================================================================= */
|
|
15
|
+
|
|
16
|
+
cart-item {
|
|
17
|
+
/* CSS Custom Properties for customization */
|
|
18
|
+
--cart-item-processing-duration: 250ms;
|
|
19
|
+
--cart-item-destroying-duration: 600ms;
|
|
20
|
+
--cart-item-appearing-duration: 400ms;
|
|
21
|
+
--cart-item-shadow-color: rgba(0, 0, 0, 0.15);
|
|
22
|
+
--cart-item-shadow-color-strong: rgba(0, 0, 0, 0.5);
|
|
23
|
+
--cart-item-destroying-bg: rgba(0, 0, 0, 0.1);
|
|
24
|
+
--cart-item-processing-scale: 0.98;
|
|
25
|
+
--cart-item-destroying-scale: 0.85;
|
|
26
|
+
--cart-item-appearing-scale: 0.9;
|
|
27
|
+
--cart-item-processing-blur: 1px;
|
|
28
|
+
--cart-item-destroying-blur: 10px;
|
|
29
|
+
--cart-item-appearing-blur: 2px;
|
|
30
|
+
--cart-item-destroying-opacity: 0.2;
|
|
31
|
+
--cart-item-appearing-opacity: 0.5;
|
|
32
|
+
--cart-item-destroying-brightness: 0.6;
|
|
33
|
+
--cart-item-destroying-saturate: 0.3;
|
|
34
|
+
|
|
35
|
+
display: block;
|
|
36
|
+
position: relative;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
padding: 0px;
|
|
39
|
+
box-shadow: inset 0px 0px 0px rgba(0, 0, 0, 0);
|
|
40
|
+
transition:
|
|
41
|
+
filter var(--cart-item-processing-duration) ease-out,
|
|
42
|
+
background-color var(--cart-item-processing-duration) ease-out,
|
|
43
|
+
box-shadow var(--cart-item-processing-duration) ease-out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
cart-item::after {
|
|
47
|
+
content: '';
|
|
48
|
+
display: block;
|
|
49
|
+
position: absolute;
|
|
50
|
+
background: rgba(0, 0, 0, 0);
|
|
51
|
+
width: 100%;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
height: 100%;
|
|
54
|
+
top: 0px;
|
|
55
|
+
left: 0px;
|
|
56
|
+
transition: background-color var(--cart-item-processing-duration) ease;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Ready state (default) */
|
|
60
|
+
cart-item[state='ready'] {
|
|
61
|
+
transition:
|
|
62
|
+
filter var(--cart-item-processing-duration) ease-out,
|
|
63
|
+
background-color var(--cart-item-processing-duration) ease-out,
|
|
64
|
+
box-shadow var(--cart-item-processing-duration) ease-out,
|
|
65
|
+
height var(--cart-item-appearing-duration) ease-out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
cart-item[state='ready'] cart-item-content {
|
|
69
|
+
transform: scale(1);
|
|
70
|
+
filter: blur(0px);
|
|
71
|
+
opacity: 1;
|
|
72
|
+
transition:
|
|
73
|
+
transform var(--cart-item-appearing-duration) ease-out,
|
|
74
|
+
filter var(--cart-item-appearing-duration) ease-out,
|
|
75
|
+
opacity var(--cart-item-appearing-duration) ease-out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
cart-item[state='ready'] cart-item-processing {
|
|
79
|
+
opacity: 0;
|
|
80
|
+
visibility: hidden;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Processing state - content slightly scaled and blurred, processing overlay visible */
|
|
84
|
+
cart-item[state='processing'] {
|
|
85
|
+
box-shadow: inset 0px 2px 10px var(--cart-item-shadow-color);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
cart-item[state='processing']::after {
|
|
89
|
+
background: rgba(0, 0, 0, 0.15);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
cart-item[state='processing'] cart-item-content {
|
|
93
|
+
transform: scale(var(--cart-item-processing-scale));
|
|
94
|
+
filter: blur(var(--cart-item-processing-blur));
|
|
95
|
+
opacity: 0.9;
|
|
96
|
+
pointer-events: none;
|
|
97
|
+
transition:
|
|
98
|
+
transform var(--cart-item-processing-duration) ease-out,
|
|
99
|
+
filter var(--cart-item-processing-duration) ease-out,
|
|
100
|
+
opacity var(--cart-item-processing-duration) ease-out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
cart-item[state='processing'] cart-item-processing {
|
|
104
|
+
opacity: 1;
|
|
105
|
+
visibility: visible;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Destroying state - heavy effects on content, darker background */
|
|
109
|
+
cart-item[state='destroying'] {
|
|
110
|
+
background-color: var(--cart-item-destroying-bg);
|
|
111
|
+
box-shadow: inset 0px 2px 20px var(--cart-item-shadow-color-strong);
|
|
112
|
+
margin-top: 0px;
|
|
113
|
+
margin-bottom: 0px;
|
|
114
|
+
transition:
|
|
115
|
+
filter var(--cart-item-destroying-duration) ease,
|
|
116
|
+
background-color var(--cart-item-destroying-duration) ease,
|
|
117
|
+
box-shadow var(--cart-item-destroying-duration) ease,
|
|
118
|
+
margin var(--cart-item-destroying-duration) ease;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
cart-item[state='destroying']::after {
|
|
122
|
+
background: rgba(0, 0, 0, 0.9);
|
|
123
|
+
transition: background-color var(--cart-item-destroying-duration) ease;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
cart-item[state='destroying'] cart-item-content {
|
|
127
|
+
transition:
|
|
128
|
+
transform var(--cart-item-destroying-duration) ease,
|
|
129
|
+
filter var(--cart-item-destroying-duration) ease,
|
|
130
|
+
opacity var(--cart-item-destroying-duration) ease;
|
|
131
|
+
transform: scale(var(--cart-item-destroying-scale));
|
|
132
|
+
filter: blur(var(--cart-item-destroying-blur)) saturate(var(--cart-item-destroying-saturate));
|
|
133
|
+
opacity: var(--cart-item-destroying-opacity);
|
|
134
|
+
pointer-events: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
cart-item[state='destroying'] cart-item-processing {
|
|
138
|
+
opacity: 0;
|
|
139
|
+
transition: opacity var(--cart-item-processing-duration) ease;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Appearing state - reverse of destroying for smooth entry animation */
|
|
143
|
+
cart-item[state='appearing'] {
|
|
144
|
+
height: 0px;
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
transition:
|
|
147
|
+
height var(--cart-item-appearing-duration) ease-out,
|
|
148
|
+
filter var(--cart-item-appearing-duration) ease-out,
|
|
149
|
+
opacity var(--cart-item-appearing-duration) ease-out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
cart-item[state='appearing'] cart-item-content {
|
|
153
|
+
transform: scale(var(--cart-item-appearing-scale));
|
|
154
|
+
filter: blur(var(--cart-item-appearing-blur));
|
|
155
|
+
opacity: var(--cart-item-appearing-opacity);
|
|
156
|
+
transition:
|
|
157
|
+
transform var(--cart-item-appearing-duration) ease-out,
|
|
158
|
+
filter var(--cart-item-appearing-duration) ease-out,
|
|
159
|
+
opacity var(--cart-item-appearing-duration) ease-out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
cart-item[state='appearing'] cart-item-processing {
|
|
163
|
+
opacity: 0;
|
|
164
|
+
visibility: hidden;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* =============================================================================
|
|
168
|
+
Cart Item Child Components
|
|
169
|
+
============================================================================= */
|
|
170
|
+
|
|
171
|
+
cart-item-content {
|
|
172
|
+
display: block;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
cart-item-processing {
|
|
176
|
+
position: absolute;
|
|
177
|
+
top: 0;
|
|
178
|
+
left: 0;
|
|
179
|
+
right: 0;
|
|
180
|
+
bottom: 0;
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
background: transparent;
|
|
185
|
+
opacity: 0;
|
|
186
|
+
visibility: hidden;
|
|
187
|
+
transition:
|
|
188
|
+
opacity var(--cart-item-processing-duration) ease-out,
|
|
189
|
+
visibility var(--cart-item-processing-duration) ease-out;
|
|
190
|
+
z-index: 10;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Default loader animation */
|
|
194
|
+
cart-item-processing .cart-item-loader {
|
|
195
|
+
width: 60px;
|
|
196
|
+
aspect-ratio: 2;
|
|
197
|
+
--_g: no-repeat radial-gradient(circle closest-side, #000 90%, #0000);
|
|
198
|
+
background:
|
|
199
|
+
var(--_g) 0% 50%,
|
|
200
|
+
var(--_g) 50% 50%,
|
|
201
|
+
var(--_g) 100% 50%;
|
|
202
|
+
background-size: calc(100% / 3) 50%;
|
|
203
|
+
animation: cart-item-loader 1s infinite linear;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@keyframes cart-item-loader {
|
|
207
|
+
20% {
|
|
208
|
+
background-position:
|
|
209
|
+
0% 0%,
|
|
210
|
+
50% 50%,
|
|
211
|
+
100% 50%;
|
|
212
|
+
}
|
|
213
|
+
40% {
|
|
214
|
+
background-position:
|
|
215
|
+
0% 100%,
|
|
216
|
+
50% 0%,
|
|
217
|
+
100% 50%;
|
|
218
|
+
}
|
|
219
|
+
60% {
|
|
220
|
+
background-position:
|
|
221
|
+
0% 50%,
|
|
222
|
+
50% 100%,
|
|
223
|
+
100% 0%;
|
|
224
|
+
}
|
|
225
|
+
80% {
|
|
226
|
+
background-position:
|
|
227
|
+
0% 50%,
|
|
228
|
+
50% 50%,
|
|
229
|
+
100% 100%;
|
|
230
|
+
}
|
|
231
|
+
}
|