@magic-spells/cart-progress-bar 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/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@magic-spells/cart-progress-bar",
3
+ "version": "0.1.0",
4
+ "description": "Cart progress bar web component for free shipping thresholds.",
5
+ "author": "Cory Schulz",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/cart-progress-bar.cjs.js",
9
+ "module": "dist/cart-progress-bar.esm.js",
10
+ "unpkg": "dist/cart-progress-bar.min.js",
11
+ "style": "dist/cart-progress-bar.min.css",
12
+ "sass": "dist/cart-progress-bar.scss",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./dist/cart-progress-bar.esm.js",
16
+ "require": "./dist/cart-progress-bar.cjs.js",
17
+ "default": "./dist/cart-progress-bar.esm.js"
18
+ },
19
+ "./css": "./dist/cart-progress-bar.css",
20
+ "./css/min": "./dist/cart-progress-bar.min.css",
21
+ "./scss": "./dist/cart-progress-bar.scss"
22
+ },
23
+ "sideEffects": true,
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/magic-spells/cart-progress-bar"
27
+ },
28
+ "homepage": "https://github.com/magic-spells/cart-progress-bar#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/magic-spells/cart-progress-bar/issues"
31
+ },
32
+ "keywords": [
33
+ "cart-progress-bar",
34
+ "progress-bar",
35
+ "web-components",
36
+ "free-shipping",
37
+ "e-commerce",
38
+ "custom-elements",
39
+ "cart-threshold"
40
+ ],
41
+ "files": [
42
+ "dist/",
43
+ "src/"
44
+ ],
45
+ "scripts": {
46
+ "build": "rollup -c",
47
+ "lint": "eslint src/ rollup.config.mjs",
48
+ "format": "prettier --write .",
49
+ "prepublishOnly": "npm run build",
50
+ "serve": "rollup -c --watch",
51
+ "dev": "rollup -c --watch"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public",
55
+ "registry": "https://registry.npmjs.org/"
56
+ },
57
+ "browserslist": [
58
+ "last 2 versions",
59
+ "not dead",
60
+ "not ie <= 11"
61
+ ],
62
+ "devDependencies": {
63
+ "@eslint/js": "^8.57.0",
64
+ "@rollup/plugin-node-resolve": "^15.2.3",
65
+ "@rollup/plugin-terser": "^0.4.4",
66
+ "eslint": "^8.0.0",
67
+ "globals": "^13.24.0",
68
+ "prettier": "^3.3.3",
69
+ "rollup": "^3.0.0",
70
+ "rollup-plugin-copy": "^3.5.0",
71
+ "rollup-plugin-postcss": "^4.0.2",
72
+ "rollup-plugin-serve": "^1.1.1",
73
+ "sass": "^1.86.3"
74
+ }
75
+ }
@@ -0,0 +1,348 @@
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 cartPanel = this.closest('cart-panel');
160
+
161
+ if (cartPanel) {
162
+ // Listen for cart data changes
163
+ cartPanel.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 && typeof updatedCart.total_price !== 'undefined') {
173
+ // Convert from cents to dollars if needed (Shopify typically returns cents)
174
+ const currentAmount = updatedCart.total_price / 100;
175
+ this.setCurrentAmount(currentAmount);
176
+ }
177
+ }
178
+
179
+ #updateMessages() {
180
+ const isComplete = this.#currentAmount >= this.#minAmount;
181
+ const remainingAmount = Math.max(0, this.#minAmount - this.#currentAmount);
182
+
183
+ // Format remaining amount as currency (assuming USD for now)
184
+ const formattedAmount = this.#formatCurrency(remainingAmount);
185
+
186
+ // Update the single message element
187
+ if (this.#messageElement) {
188
+ let messageTemplate;
189
+
190
+ if (isComplete && this.#originalAboveMessage) {
191
+ // Show success message when complete
192
+ messageTemplate = this.#originalAboveMessage;
193
+ } else if (!isComplete) {
194
+ // Show progress message when incomplete
195
+ messageTemplate = this.#originalBelowMessage || this.#originalAboveMessage;
196
+ }
197
+
198
+ if (messageTemplate) {
199
+ const message = messageTemplate.replace(/\$\{left\}/g, formattedAmount);
200
+ this.#messageElement.textContent = message;
201
+ this.#messageElement.style.display = 'block';
202
+ } else {
203
+ // Hide message if no template available for current state
204
+ this.#messageElement.style.display = 'none';
205
+ }
206
+ }
207
+ }
208
+
209
+ #updateComponentState() {
210
+ const isComplete = this.#currentAmount >= this.#minAmount;
211
+
212
+ if (isComplete) {
213
+ this.setAttribute('data-complete', 'true');
214
+ this.removeAttribute('data-incomplete');
215
+ } else {
216
+ this.setAttribute('data-incomplete', 'true');
217
+ this.removeAttribute('data-complete');
218
+ }
219
+
220
+ // Set progress level classes for styling
221
+ this.classList.remove('progress-low', 'progress-medium', 'progress-high', 'progress-complete');
222
+
223
+ if (isComplete) {
224
+ this.classList.add('progress-complete');
225
+ } else if (this.#progressPercent >= 75) {
226
+ this.classList.add('progress-high');
227
+ } else if (this.#progressPercent >= 50) {
228
+ this.classList.add('progress-medium');
229
+ } else {
230
+ this.classList.add('progress-low');
231
+ }
232
+ }
233
+
234
+ #formatCurrency(amount) {
235
+ // Basic currency formatting - could be enhanced with locale/currency options
236
+ return new Intl.NumberFormat('en-US', {
237
+ style: 'currency',
238
+ currency: 'USD',
239
+ minimumFractionDigits: 2,
240
+ maximumFractionDigits: 2,
241
+ }).format(amount);
242
+ }
243
+
244
+ /**
245
+ * Public API: Set the progress percentage directly
246
+ * @param {number} percent - Progress percentage (0-100)
247
+ */
248
+ setPercent(percent) {
249
+ const clampedPercent = Math.max(0, Math.min(100, percent));
250
+ this.#progressPercent = clampedPercent;
251
+
252
+ if (this.#progressBar) {
253
+ this.#progressBar.setPercent(clampedPercent);
254
+ }
255
+
256
+ // Calculate current amount based on percentage
257
+ this.#currentAmount = (clampedPercent / 100) * this.#minAmount;
258
+ this.setAttribute('current', this.#currentAmount.toString());
259
+
260
+ this.#updateMessages();
261
+ this.#updateComponentState();
262
+ }
263
+
264
+ /**
265
+ * Public API: Set the current cart amount
266
+ * @param {number} amount - Current cart amount
267
+ */
268
+ setCurrentAmount(amount) {
269
+ this.#currentAmount = parseFloat(amount) || 0;
270
+ this.setAttribute('current', this.#currentAmount.toString());
271
+ this.#updateProgress();
272
+ }
273
+
274
+ /**
275
+ * Public API: Set the threshold amount for free shipping
276
+ * @param {number} amount - Threshold amount for free shipping
277
+ */
278
+ setThresholdAmount(amount) {
279
+ this.#minAmount = parseFloat(amount) || 0;
280
+ this.setAttribute('threshold', this.#minAmount.toString());
281
+ this.#updateProgress();
282
+ }
283
+
284
+ /**
285
+ * Public API: Set the minimum amount for free shipping (deprecated - use setThresholdAmount)
286
+ * @param {number} amount - Minimum amount threshold
287
+ * @deprecated Use setThresholdAmount instead
288
+ */
289
+ setMinAmount(amount) {
290
+ this.setThresholdAmount(amount);
291
+ }
292
+
293
+ /**
294
+ * Public API: Get current progress information
295
+ */
296
+ getProgress() {
297
+ return {
298
+ currentAmount: this.#currentAmount,
299
+ thresholdAmount: this.#minAmount,
300
+ minAmount: this.#minAmount, // backwards compatibility
301
+ remainingAmount: Math.max(0, this.#minAmount - this.#currentAmount),
302
+ percent: this.#progressPercent,
303
+ isComplete: this.#currentAmount >= this.#minAmount,
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Public API: Update message templates
309
+ * @param {string} aboveMessage - Message template for above the bar
310
+ * @param {string} belowMessage - Message template for below the bar
311
+ */
312
+ setMessages(aboveMessage = null, belowMessage = null) {
313
+ if (aboveMessage !== null) {
314
+ this.#originalAboveMessage = aboveMessage;
315
+ this.setAttribute('message-above', aboveMessage);
316
+ }
317
+
318
+ if (belowMessage !== null) {
319
+ this.#originalBelowMessage = belowMessage;
320
+ this.setAttribute('message-below', belowMessage);
321
+ }
322
+
323
+ this.#updateMessages();
324
+ }
325
+
326
+ // Getters
327
+ get currentAmount() {
328
+ return this.#currentAmount;
329
+ }
330
+ get thresholdAmount() {
331
+ return this.#minAmount;
332
+ }
333
+ get minAmount() {
334
+ return this.#minAmount;
335
+ } // backwards compatibility
336
+ get percent() {
337
+ return this.#progressPercent;
338
+ }
339
+ get isComplete() {
340
+ return this.#currentAmount >= this.#minAmount;
341
+ }
342
+ }
343
+
344
+ // Define CartProgressBar custom element
345
+ customElements.define('cart-progress-bar', CartProgressBar);
346
+
347
+ // Export components for external use
348
+ export { CartProgressBar, ProgressBar };
@@ -0,0 +1,202 @@
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-bg: #e9ecef !default;
5
+ $cart-progress-bar-fill-bg: #28a745 !default;
6
+ $cart-progress-bar-complete-bg: #007bff !default;
7
+ $cart-progress-bar-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1) !default;
8
+ $cart-progress-bar-transition-duration: 0.3s !default;
9
+
10
+ // Message styling
11
+ $cart-progress-message-font-size: 0.875rem !default;
12
+ $cart-progress-message-line-height: 1.4 !default;
13
+ $cart-progress-message-color: #495057 !default;
14
+ $cart-progress-message-complete-color: #155724 !default;
15
+ $cart-progress-message-margin: 0.5rem !default;
16
+
17
+ // Responsive breakpoints
18
+ $cart-progress-mobile-breakpoint: 768px !default;
19
+
20
+ // Cart progress bar component styles
21
+ cart-progress-bar {
22
+ // CSS Custom Properties for customization (mapped from SCSS variables)
23
+ --cart-progress-bar-height: #{$cart-progress-bar-height};
24
+ --cart-progress-bar-border-radius: #{$cart-progress-bar-border-radius};
25
+ --cart-progress-bar-bg: #{$cart-progress-bar-bg};
26
+ --cart-progress-bar-fill-bg: #{$cart-progress-bar-fill-bg};
27
+ --cart-progress-bar-complete-bg: #{$cart-progress-bar-complete-bg};
28
+ --cart-progress-bar-shadow: #{$cart-progress-bar-shadow};
29
+ --cart-progress-bar-transition-duration: #{$cart-progress-bar-transition-duration};
30
+ --cart-progress-message-font-size: #{$cart-progress-message-font-size};
31
+ --cart-progress-message-line-height: #{$cart-progress-message-line-height};
32
+ --cart-progress-message-color: #{$cart-progress-message-color};
33
+ --cart-progress-message-complete-color: #{$cart-progress-message-complete-color};
34
+ --cart-progress-message-margin: #{$cart-progress-message-margin};
35
+
36
+ // Dynamic progress percentage (set by JavaScript)
37
+ --cart-progress-percent: 0%;
38
+
39
+ display: block;
40
+ width: 100%;
41
+ max-width: 100%;
42
+
43
+ // Message styling
44
+ p[data-content-cart-progress-message] {
45
+ font-size: var(--cart-progress-message-font-size);
46
+ line-height: var(--cart-progress-message-line-height);
47
+ color: var(--cart-progress-message-color);
48
+ margin: var(--cart-progress-message-margin) 0;
49
+ text-align: center;
50
+ transition: color var(--cart-progress-bar-transition-duration) ease;
51
+
52
+ @media (max-width: $cart-progress-mobile-breakpoint) {
53
+ font-size: calc(var(--cart-progress-message-font-size) * 0.9);
54
+ margin: calc(var(--cart-progress-message-margin) * 0.75) 0;
55
+ }
56
+ }
57
+
58
+ // State-based styling
59
+ &[data-complete='true'] {
60
+ p[data-content-cart-progress-message] {
61
+ color: var(--cart-progress-message-complete-color);
62
+ font-weight: 600;
63
+ }
64
+ }
65
+
66
+ // Progress level classes for additional styling hooks
67
+ &.progress-low {
68
+ --cart-progress-bar-fill-bg: #dc3545; // red for low progress
69
+ }
70
+
71
+ &.progress-medium {
72
+ --cart-progress-bar-fill-bg: #ffc107; // yellow for medium progress
73
+ }
74
+
75
+ &.progress-high {
76
+ --cart-progress-bar-fill-bg: #fd7e14; // orange for high progress
77
+ }
78
+
79
+ &.progress-complete {
80
+ --cart-progress-bar-fill-bg: var(--cart-progress-bar-complete-bg);
81
+
82
+ p[data-content-cart-progress-message] {
83
+ color: var(--cart-progress-message-complete-color);
84
+ }
85
+ }
86
+ }
87
+
88
+ // Progress bar component styles
89
+ progress-bar {
90
+ display: block;
91
+ width: 100%;
92
+ height: var(--cart-progress-bar-height);
93
+ background-color: var(--cart-progress-bar-bg);
94
+ border-radius: var(--cart-progress-bar-border-radius);
95
+ box-shadow: var(--cart-progress-bar-shadow);
96
+ overflow: hidden;
97
+ position: relative;
98
+
99
+ .progress-bar-fill {
100
+ height: 100%;
101
+ width: var(--cart-progress-percent);
102
+ background-color: var(--cart-progress-bar-fill-bg);
103
+ border-radius: var(--cart-progress-bar-border-radius);
104
+ transition:
105
+ width var(--cart-progress-bar-transition-duration) ease,
106
+ background-color var(--cart-progress-bar-transition-duration) ease;
107
+ position: relative;
108
+
109
+ // Subtle gradient effect
110
+ background-image: linear-gradient(
111
+ 45deg,
112
+ rgba(255, 255, 255, 0.1) 25%,
113
+ transparent 25%,
114
+ transparent 50%,
115
+ rgba(255, 255, 255, 0.1) 50%,
116
+ rgba(255, 255, 255, 0.1) 75%,
117
+ transparent 75%,
118
+ transparent
119
+ );
120
+ background-size: 12px 12px;
121
+
122
+ // Shine effect for completed state
123
+ &::after {
124
+ content: '';
125
+ position: absolute;
126
+ top: 0;
127
+ left: -100%;
128
+ width: 100%;
129
+ height: 100%;
130
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
131
+ transition: left 0.5s ease;
132
+ }
133
+ }
134
+
135
+ // Animate shine effect when complete
136
+ cart-progress-bar[data-complete='true'] &,
137
+ cart-progress-bar.progress-complete & {
138
+ .progress-bar-fill::after {
139
+ left: 100%;
140
+ }
141
+ }
142
+
143
+ // Accessibility improvements
144
+ &:focus-visible {
145
+ outline: 2px solid #007bff;
146
+ outline-offset: 2px;
147
+ }
148
+
149
+ // High contrast mode support
150
+ @media (prefers-contrast: high) {
151
+ --cart-progress-bar-bg: #000000;
152
+ --cart-progress-bar-fill-bg: #ffffff;
153
+ border: 1px solid #ffffff;
154
+ }
155
+
156
+ // Reduced motion support
157
+ @media (prefers-reduced-motion: reduce) {
158
+ .progress-bar-fill {
159
+ transition: none;
160
+ }
161
+
162
+ .progress-bar-fill::after {
163
+ transition: none;
164
+ }
165
+ }
166
+ }
167
+
168
+ // Responsive adjustments
169
+ @media (max-width: $cart-progress-mobile-breakpoint) {
170
+ cart-progress-bar {
171
+ --cart-progress-bar-height: 10px;
172
+ --cart-progress-message-margin: 0.375rem;
173
+ }
174
+ }
175
+
176
+ // Dark mode support
177
+ @media (prefers-color-scheme: dark) {
178
+ cart-progress-bar {
179
+ --cart-progress-bar-bg: #343a40;
180
+ --cart-progress-message-color: #e9ecef;
181
+ --cart-progress-message-complete-color: #28a745;
182
+ }
183
+ }
184
+
185
+ // Print styles
186
+ @media print {
187
+ cart-progress-bar {
188
+ .above-message,
189
+ .below-message {
190
+ color: #000000 !important;
191
+ }
192
+
193
+ progress-bar {
194
+ border: 1px solid #000000;
195
+
196
+ .progress-bar-fill {
197
+ background-color: #000000 !important;
198
+ background-image: none !important;
199
+ }
200
+ }
201
+ }
202
+ }