@magic-spells/cart-progress-bar 0.2.1 → 2.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.
@@ -1,332 +0,0 @@
1
- import './cart-progress-bar.scss';
2
-
3
- /**
4
- * ProgressBar helper class for the visual progress bar element
5
- */
6
- class ProgressBar extends HTMLElement {
7
- constructor() {
8
- super();
9
- this.#setupProgressBar();
10
- }
11
-
12
- #setupProgressBar() {
13
- this.setAttribute('role', 'progressbar');
14
- this.setAttribute('aria-valuemin', '0');
15
- this.setAttribute('aria-valuemax', '100');
16
- this.setAttribute('aria-valuenow', '0');
17
-
18
- // Create the visual progress bar
19
- this.innerHTML = '<div class="progress-bar-fill"></div>';
20
- }
21
-
22
- setPercent(percent) {
23
- const clampedPercent = Math.max(0, Math.min(100, percent));
24
- this.style.setProperty('--cart-progress-percent', `${clampedPercent}%`);
25
- this.setAttribute('aria-valuenow', clampedPercent);
26
- }
27
- }
28
-
29
- // Define ProgressBar custom element immediately so it's available for CartProgressBar
30
- customElements.define('progress-bar', ProgressBar);
31
-
32
- /**
33
- * CartProgressBar main component
34
- */
35
- class CartProgressBar extends HTMLElement {
36
- // Private fields
37
- #minAmount = 0;
38
- #currentAmount = 0;
39
- #progressPercent = 0;
40
- #originalAboveMessage = '';
41
- #originalBelowMessage = '';
42
- #progressBar = null;
43
- #messageElement = null;
44
-
45
- /**
46
- * Define which attributes should be observed for changes
47
- */
48
- static get observedAttributes() {
49
- return ['threshold', 'current', 'message-above', 'message-below'];
50
- }
51
-
52
- constructor() {
53
- super();
54
- this.#init();
55
- }
56
-
57
- #init() {
58
- // Read initial attributes
59
- this.#minAmount = parseFloat(this.getAttribute('threshold')) || 0;
60
- this.#currentAmount = parseFloat(this.getAttribute('current')) || 0;
61
-
62
- // Store original message templates
63
- this.#originalAboveMessage = this.getAttribute('message-above') || '';
64
- this.#originalBelowMessage = this.getAttribute('message-below') || '';
65
- }
66
-
67
- async connectedCallback() {
68
- // ensure the child custom element has been registered
69
- await customElements.whenDefined('progress-bar');
70
-
71
- if (!customElements.get('progress-bar')) {
72
- throw new Error('<progress-bar> must be registered before <cart-progress-bar> runs');
73
- }
74
-
75
- this.#render();
76
- this.#updateProgress();
77
- this.#attachListeners();
78
- }
79
-
80
- attributeChangedCallback(name, oldValue, newValue) {
81
- if (oldValue === newValue) return;
82
-
83
- switch (name) {
84
- case 'threshold':
85
- this.#minAmount = parseFloat(newValue) || 0;
86
- this.#updateProgress();
87
- break;
88
- case 'current':
89
- this.#currentAmount = parseFloat(newValue) || 0;
90
- this.#updateProgress();
91
- break;
92
- case 'message-above':
93
- this.#originalAboveMessage = newValue || '';
94
- this.#updateMessages();
95
- break;
96
- case 'message-below':
97
- this.#originalBelowMessage = newValue || '';
98
- this.#updateMessages();
99
- break;
100
- }
101
- }
102
-
103
- #render() {
104
- // Find or create the message element
105
- this.#messageElement =
106
- this.querySelector('[data-content-cart-progress-message]') ||
107
- this.querySelector('p[data-content-cart-progress-message]');
108
-
109
- // Find existing progress bar (user can add their own)
110
- this.#progressBar = this.querySelector('progress-bar');
111
-
112
- // Create message element if it doesn't exist but we have message templates
113
- if (!this.#messageElement && (this.#originalAboveMessage || this.#originalBelowMessage)) {
114
- this.#messageElement = document.createElement('p');
115
- this.#messageElement.setAttribute('data-content-cart-progress-message', '');
116
- this.appendChild(this.#messageElement);
117
- }
118
-
119
- // Create progress bar if it doesn't exist - add it at the end (below text message)
120
- if (!this.#progressBar) {
121
- // Ensure progress-bar is defined before creating
122
- if (customElements.get('progress-bar')) {
123
- this.#progressBar = document.createElement('progress-bar');
124
- this.appendChild(this.#progressBar);
125
- } else {
126
- // Fallback: wait for definition
127
- customElements.whenDefined('progress-bar').then(() => {
128
- if (!this.#progressBar) {
129
- this.#progressBar = document.createElement('progress-bar');
130
- this.appendChild(this.#progressBar);
131
- this.#updateProgress(); // Update progress after creating
132
- }
133
- });
134
- }
135
- }
136
- }
137
-
138
- #updateProgress() {
139
- if (this.#minAmount === 0) {
140
- this.#progressPercent = 100;
141
- } else {
142
- this.#progressPercent = Math.min(100, (this.#currentAmount / this.#minAmount) * 100);
143
- }
144
-
145
- // Update progress bar
146
- if (this.#progressBar) {
147
- this.#progressBar.setPercent(this.#progressPercent);
148
- }
149
-
150
- // Update messages
151
- this.#updateMessages();
152
-
153
- // Update component state
154
- this.#updateComponentState();
155
- }
156
-
157
- #attachListeners() {
158
- // Find the nearest cart-panel component
159
- const cartDialog = this.closest('cart-dialog');
160
-
161
- if (cartDialog) {
162
- // Listen for cart data changes
163
- cartDialog.addEventListener('cart-dialog:data-changed', (event) => {
164
- this.#handleCartDataChange(event);
165
- });
166
- }
167
- }
168
-
169
- #handleCartDataChange(event) {
170
- const updatedCart = event.detail;
171
-
172
- if (updatedCart) {
173
- // Use calculated_subtotal if available (handles _ignore_price_in_subtotal logic)
174
- // Otherwise fall back to total_price for backwards compatibility
175
- let currentAmount = 0;
176
-
177
- if (typeof updatedCart.calculated_subtotal !== 'undefined') {
178
- // calculated_subtotal is already in dollars, no conversion needed
179
- currentAmount = updatedCart.calculated_subtotal / 100;
180
- } else if (typeof updatedCart.total_price !== 'undefined') {
181
- // Convert from cents to dollars if needed (Shopify typically returns cents)
182
- currentAmount = updatedCart.total_price / 100;
183
- }
184
-
185
- this.setCurrentAmount(currentAmount);
186
- }
187
- }
188
-
189
- #updateMessages() {
190
- const isComplete = this.#currentAmount >= this.#minAmount;
191
- const remainingAmount = Math.max(0, this.#minAmount - this.#currentAmount);
192
-
193
- // Format amount with minimal formatting
194
- const formattedAmount = remainingAmount.toFixed(2).replace('.00', '');
195
-
196
- // Update the single message element
197
- if (this.#messageElement) {
198
- let messageTemplate;
199
-
200
- if (isComplete && this.#originalAboveMessage) {
201
- // Show success message when complete
202
- messageTemplate = this.#originalAboveMessage;
203
- } else if (!isComplete) {
204
- // Show progress message when incomplete
205
- messageTemplate = this.#originalBelowMessage || this.#originalAboveMessage;
206
- }
207
-
208
- if (messageTemplate) {
209
- // Support multiple placeholder formats: {amount}, { amount }, [amount]
210
- const message = messageTemplate
211
- .replace(/\{\s*amount\s*\}/g, formattedAmount)
212
- .replace(/\[\s*amount\s*\]/g, formattedAmount);
213
- this.#messageElement.textContent = message;
214
- this.#messageElement.style.display = 'block';
215
- } else {
216
- // Hide message if no template available for current state
217
- this.#messageElement.style.display = 'none';
218
- }
219
- }
220
- }
221
-
222
- #updateComponentState() {
223
- const isComplete = this.#currentAmount >= this.#minAmount;
224
- this.setAttribute('complete', isComplete.toString());
225
- }
226
-
227
-
228
- /**
229
- * Public API: Set the progress percentage directly
230
- * @param {number} percent - Progress percentage (0-100)
231
- */
232
- setPercent(percent) {
233
- const clampedPercent = Math.max(0, Math.min(100, percent));
234
- this.#progressPercent = clampedPercent;
235
-
236
- if (this.#progressBar) {
237
- this.#progressBar.setPercent(clampedPercent);
238
- }
239
-
240
- // Calculate current amount based on percentage
241
- this.#currentAmount = (clampedPercent / 100) * this.#minAmount;
242
- this.setAttribute('current', this.#currentAmount.toString());
243
-
244
- this.#updateMessages();
245
- this.#updateComponentState();
246
- }
247
-
248
- /**
249
- * Public API: Set the current cart amount
250
- * @param {number} amount - Current cart amount
251
- */
252
- setCurrentAmount(amount) {
253
- this.#currentAmount = parseFloat(amount) || 0;
254
- this.setAttribute('current', this.#currentAmount.toString());
255
- this.#updateProgress();
256
- }
257
-
258
- /**
259
- * Public API: Set the threshold amount for free shipping
260
- * @param {number} amount - Threshold amount for free shipping
261
- */
262
- setThresholdAmount(amount) {
263
- this.#minAmount = parseFloat(amount) || 0;
264
- this.setAttribute('threshold', this.#minAmount.toString());
265
- this.#updateProgress();
266
- }
267
-
268
- /**
269
- * Public API: Set the minimum amount for free shipping (deprecated - use setThresholdAmount)
270
- * @param {number} amount - Minimum amount threshold
271
- * @deprecated Use setThresholdAmount instead
272
- */
273
- setMinAmount(amount) {
274
- this.setThresholdAmount(amount);
275
- }
276
-
277
- /**
278
- * Public API: Get current progress information
279
- */
280
- getProgress() {
281
- return {
282
- currentAmount: this.#currentAmount,
283
- thresholdAmount: this.#minAmount,
284
- minAmount: this.#minAmount, // backwards compatibility
285
- remainingAmount: Math.max(0, this.#minAmount - this.#currentAmount),
286
- percent: this.#progressPercent,
287
- isComplete: this.#currentAmount >= this.#minAmount,
288
- };
289
- }
290
-
291
- /**
292
- * Public API: Update message templates
293
- * @param {string} aboveMessage - Message template for above the bar
294
- * @param {string} belowMessage - Message template for below the bar
295
- */
296
- setMessages(aboveMessage = null, belowMessage = null) {
297
- if (aboveMessage !== null) {
298
- this.#originalAboveMessage = aboveMessage;
299
- this.setAttribute('message-above', aboveMessage);
300
- }
301
-
302
- if (belowMessage !== null) {
303
- this.#originalBelowMessage = belowMessage;
304
- this.setAttribute('message-below', belowMessage);
305
- }
306
-
307
- this.#updateMessages();
308
- }
309
-
310
- // Getters
311
- get currentAmount() {
312
- return this.#currentAmount;
313
- }
314
- get thresholdAmount() {
315
- return this.#minAmount;
316
- }
317
- get minAmount() {
318
- return this.#minAmount;
319
- } // backwards compatibility
320
- get percent() {
321
- return this.#progressPercent;
322
- }
323
- get isComplete() {
324
- return this.#currentAmount >= this.#minAmount;
325
- }
326
- }
327
-
328
- // Define CartProgressBar custom element
329
- customElements.define('cart-progress-bar', CartProgressBar);
330
-
331
- // Export components for external use
332
- export { CartProgressBar, ProgressBar };
@@ -1,124 +0,0 @@
1
- // SCSS Variables (can be overridden before import)
2
- $cart-progress-bar-height: 12px !default;
3
- $cart-progress-bar-border-radius: 6px !default;
4
- $cart-progress-bar-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1) !default;
5
- $cart-progress-bar-transition-duration: 0.3s !default;
6
-
7
- // Core color variables
8
- $cart-progress-section-bg: transparent !default;
9
- $cart-progress-section-color: #495057 !default;
10
- $cart-progress-bar-bg: #e9ecef !default;
11
- $cart-progress-bar-fill-before: #28a745 !default;
12
- $cart-progress-bar-fill-after: #007bff !default;
13
-
14
- // Responsive breakpoints
15
- $cart-progress-mobile-breakpoint: 768px !default;
16
-
17
- // Cart progress bar component styles
18
- cart-progress-bar {
19
- // CSS Custom Properties for customization (mapped from SCSS variables)
20
- --cart-progress-bar-height: #{$cart-progress-bar-height};
21
- --cart-progress-bar-border-radius: #{$cart-progress-bar-border-radius};
22
- --cart-progress-bar-shadow: #{$cart-progress-bar-shadow};
23
- --cart-progress-bar-transition-duration: #{$cart-progress-bar-transition-duration};
24
-
25
- // Core color variables
26
- --cart-progress-section-bg: #{$cart-progress-section-bg};
27
- --cart-progress-section-color: #{$cart-progress-section-color};
28
- --cart-progress-bar-bg: #{$cart-progress-bar-bg};
29
- --cart-progress-bar-fill-before: #{$cart-progress-bar-fill-before};
30
- --cart-progress-bar-fill-after: #{$cart-progress-bar-fill-after};
31
-
32
- // Dynamic variables (set by JavaScript)
33
- --cart-progress-percent: 0%;
34
- --cart-progress-bar-fill-current: var(--cart-progress-bar-fill-before);
35
-
36
- display: block;
37
- width: 100%;
38
- max-width: 100%;
39
- background-color: var(--cart-progress-section-bg);
40
- color: var(--cart-progress-section-color);
41
-
42
- // Message styling
43
- p[data-content-cart-progress-message] {
44
- color: inherit;
45
- transition: opacity var(--cart-progress-bar-transition-duration) ease;
46
- }
47
-
48
- // State-based styling
49
- &[complete='true'] {
50
- p[data-content-cart-progress-message] {
51
- font-weight: 600;
52
- }
53
- }
54
-
55
- // State-based progress bar coloring
56
- &[complete='false'] {
57
- --cart-progress-bar-fill-current: var(--cart-progress-bar-fill-before);
58
- }
59
-
60
- &[complete='true'] {
61
- --cart-progress-bar-fill-current: var(--cart-progress-bar-fill-after);
62
- }
63
- }
64
-
65
- // Progress bar component styles
66
- progress-bar {
67
- display: block;
68
- width: 100%;
69
- height: var(--cart-progress-bar-height);
70
- background-color: var(--cart-progress-bar-bg);
71
- border-radius: var(--cart-progress-bar-border-radius);
72
- box-shadow: var(--cart-progress-bar-shadow);
73
- overflow: hidden;
74
- position: relative;
75
-
76
- .progress-bar-fill {
77
- height: 100%;
78
- width: var(--cart-progress-percent);
79
- background-color: var(--cart-progress-bar-fill-current);
80
- border-radius: var(--cart-progress-bar-border-radius);
81
- transition:
82
- width var(--cart-progress-bar-transition-duration) ease,
83
- background-color var(--cart-progress-bar-transition-duration) ease;
84
- position: relative;
85
-
86
- // Subtle gradient effect
87
- background-image: linear-gradient(
88
- 45deg,
89
- rgba(255, 255, 255, 0.1) 25%,
90
- transparent 25%,
91
- transparent 50%,
92
- rgba(255, 255, 255, 0.1) 50%,
93
- rgba(255, 255, 255, 0.1) 75%,
94
- transparent 75%,
95
- transparent
96
- );
97
- background-size: 12px 12px;
98
-
99
- // Shine effect for completed state
100
- &::after {
101
- content: '';
102
- position: absolute;
103
- top: 0;
104
- left: -100%;
105
- width: 100%;
106
- height: 100%;
107
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
108
- transition: left 0.5s ease;
109
- }
110
- }
111
-
112
- // Animate shine effect when complete
113
- cart-progress-bar[complete='true'] & {
114
- .progress-bar-fill::after {
115
- left: 100%;
116
- }
117
- }
118
-
119
- // Accessibility improvements
120
- &:focus-visible {
121
- outline: 2px solid #007bff;
122
- outline-offset: 2px;
123
- }
124
- }