@o2vend/theme-cli 1.0.36 → 1.0.38

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.
Files changed (90) hide show
  1. package/README.md +4 -0
  2. package/lib/lib/dev-server.js +344 -48
  3. package/lib/lib/liquid-engine.js +3 -1
  4. package/lib/lib/mock-data.js +473 -119
  5. package/lib/lib/widget-service.js +12 -4
  6. package/package.json +2 -2
  7. package/test-theme/assets/async-sections.js +32 -24
  8. package/test-theme/assets/cart-drawer.js +20 -22
  9. package/test-theme/assets/cart-manager.js +1 -15
  10. package/test-theme/assets/checkout-price-handler.js +12 -11
  11. package/test-theme/assets/checkout.css +1415 -0
  12. package/test-theme/assets/checkout.js +3174 -0
  13. package/test-theme/assets/components.css +178 -29
  14. package/test-theme/assets/delivery-zone.js +1 -1
  15. package/test-theme/assets/product-detail.css +1050 -0
  16. package/test-theme/assets/product-detail.js +2940 -0
  17. package/test-theme/assets/theme.css +95 -120
  18. package/test-theme/assets/theme.js +781 -186
  19. package/test-theme/layout/theme.liquid +91 -17
  20. package/test-theme/sections/content.liquid +64 -57
  21. package/test-theme/sections/footer-fallback.liquid +57 -7
  22. package/test-theme/sections/footer.liquid +63 -12
  23. package/test-theme/sections/header-fallback.liquid +41 -41
  24. package/test-theme/sections/header.liquid +41 -51
  25. package/test-theme/sections/hero-fallback.liquid +1 -1
  26. package/test-theme/sections/hero.liquid +159 -136
  27. package/test-theme/snippets/account-sidebar.liquid +121 -29
  28. package/test-theme/snippets/add-to-cart-modal.liquid +258 -206
  29. package/test-theme/snippets/breadcrumbs.liquid +98 -11
  30. package/test-theme/snippets/cart-drawer.liquid +93 -0
  31. package/test-theme/snippets/delivery-zone-city-selector.liquid +101 -15
  32. package/test-theme/snippets/delivery-zone-modal.liquid +529 -84
  33. package/test-theme/snippets/delivery-zone-search.liquid +104 -18
  34. package/test-theme/snippets/login-modal.liquid +269 -82
  35. package/test-theme/snippets/mega-menu.liquid +130 -43
  36. package/test-theme/snippets/news-thumbnail.liquid +120 -28
  37. package/test-theme/snippets/pagination.liquid +1 -1
  38. package/test-theme/snippets/price.liquid +100 -9
  39. package/test-theme/snippets/product-card-related.liquid +22 -4
  40. package/test-theme/snippets/product-card-simple.liquid +521 -25
  41. package/test-theme/snippets/product-card.liquid +145 -232
  42. package/test-theme/snippets/rating.liquid +100 -9
  43. package/test-theme/snippets/skeleton-collection-grid.liquid +94 -8
  44. package/test-theme/snippets/skeleton-product-card.liquid +102 -16
  45. package/test-theme/snippets/skeleton-product-grid.liquid +87 -1
  46. package/test-theme/snippets/social-sharing.liquid +133 -32
  47. package/test-theme/templates/account/dashboard.liquid +30 -0
  48. package/test-theme/templates/account/loyalty-redemption.liquid +29 -28
  49. package/test-theme/templates/account/loyalty.liquid +45 -43
  50. package/test-theme/templates/account/order-detail.liquid +15 -8
  51. package/test-theme/templates/account/orders.liquid +189 -35
  52. package/test-theme/templates/account/profile.liquid +509 -114
  53. package/test-theme/templates/account/register.liquid +18 -8
  54. package/test-theme/templates/account/return-orders.liquid +31 -30
  55. package/test-theme/templates/account/store-credit.liquid +27 -26
  56. package/test-theme/templates/account/subscriptions.liquid +22 -5
  57. package/test-theme/templates/account/wishlist.liquid +88 -19
  58. package/test-theme/templates/address-book.liquid +166 -69
  59. package/test-theme/templates/categories.liquid +90 -30
  60. package/test-theme/templates/checkout.liquid +137 -3834
  61. package/test-theme/templates/error.liquid +23 -21
  62. package/test-theme/templates/index.liquid +29 -0
  63. package/test-theme/templates/login.liquid +33 -6
  64. package/test-theme/templates/order-confirmation.liquid +67 -9
  65. package/test-theme/templates/page.liquid +418 -206
  66. package/test-theme/templates/product-detail.liquid +124 -3878
  67. package/test-theme/templates/products.liquid +155 -30
  68. package/test-theme/templates/search.liquid +739 -225
  69. package/test-theme/widgets/brand-carousel.liquid +102 -82
  70. package/test-theme/widgets/brand.liquid +78 -50
  71. package/test-theme/widgets/carousel.liquid +253 -121
  72. package/test-theme/widgets/category-list-carousel.liquid +32 -8
  73. package/test-theme/widgets/category-list.liquid +21 -6
  74. package/test-theme/widgets/category.liquid +104 -37
  75. package/test-theme/widgets/discount-time.liquid +326 -119
  76. package/test-theme/widgets/footer-menu.liquid +115 -23
  77. package/test-theme/widgets/footer.liquid +118 -5
  78. package/test-theme/widgets/gallery.liquid +29 -5
  79. package/test-theme/widgets/header-menu.liquid +25 -13
  80. package/test-theme/widgets/header.liquid +64 -26
  81. package/test-theme/widgets/html.liquid +29 -6
  82. package/test-theme/widgets/news.liquid +6 -0
  83. package/test-theme/widgets/product-canvas.liquid +20 -12
  84. package/test-theme/widgets/product-carousel.liquid +118 -56
  85. package/test-theme/widgets/shared/product-grid.liquid +12 -0
  86. package/test-theme/widgets/single-product.liquid +688 -250
  87. package/test-theme/widgets/spacebar-carousel.liquid +39 -10
  88. package/test-theme/widgets/spacebar.liquid +77 -6
  89. package/test-theme/widgets/splash.liquid +40 -30
  90. package/test-theme/widgets/testimonial-carousel.liquid +111 -67
@@ -40,16 +40,22 @@
40
40
  assign background_color = widget_settings.backgroundColor | default: widget_settings.BackgroundColor | default: widget_content.BackgroundColor | default: widget_content.backgroundColor
41
41
  assign text_color = widget_settings.textColor | default: widget_settings.TextColor | default: widget_content.TextColor | default: widget_content.textColor
42
42
  assign show_add_to_cart = widget_settings.showAddToCartButton | default: widget_content.ShowAddToCartButton | default: true
43
+ assign product_call_for_pricing_raw = product.showCallForPricing | default: product.ShowCallForPricing | default: product.isCallForPricing | default: product.IsCallForPricing | default: false
44
+ assign product_show_call_for_pricing = false
45
+ if product_call_for_pricing_raw == true or product_call_for_pricing_raw == 'true' or product_call_for_pricing_raw == 1 or product_call_for_pricing_raw == '1'
46
+ assign product_show_call_for_pricing = true
47
+ endif
43
48
 
44
49
  comment
45
50
  Extract short description (first 200 chars or until first paragraph)
46
51
  endcomment
47
- if product and product.description
48
- assign full_description = product.description
49
- assign short_description = full_description | truncate: 200
50
- if full_description contains '<p>'
51
- assign first_para = full_description | split: '<p>' | last | split: '</p>' | first
52
- assign short_description = first_para | truncate: 200
52
+ assign full_description = product.shortDescription | default: product.ShortDescription | default: product.description | default: product.Description | default: product.htmlContent | default: product.HtmlContent
53
+ if full_description and full_description != blank
54
+ assign short_description = full_description | strip_html | strip | truncate: 200
55
+ assign normalized_description = full_description | replace: '<P>', '<p>' | replace: '</P>', '</p>'
56
+ if normalized_description contains '<p>'
57
+ assign first_para = normalized_description | split: '<p>' | last | split: '</p>' | first
58
+ assign short_description = first_para | strip_html | strip | truncate: 200
53
59
  endif
54
60
  endif
55
61
  %}
@@ -68,34 +74,51 @@
68
74
  {% if product %}
69
75
  <div class="single-product-pdp">
70
76
  <div class="single-product-pdp__layout">
71
- <!-- Product Gallery -->
72
- <div class="single-product-pdp__gallery">
73
- <div class="gallery-main">
77
+ <!-- Product Gallery with Carousel -->
78
+ <div class="single-product-pdp__media{% if product.images and product.images.size > 1 %} has-thumbnails{% endif %}">
79
+ <div class="single-product-media-main">
74
80
  {% if product.images and product.images.size > 0 %}
75
- {% for image in product.images %}
76
- {% assign image_url = image.url | default: image %}
81
+ <!-- Carousel Images -->
82
+ <div class="carousel-track-container">
83
+ <div class="carousel-track">
84
+ {% for image in product.images %}
85
+ {% assign image_url = image.url | default: image %}
86
+ <div class="carousel-slide {% if forloop.first %}active{% endif %}" data-index="{{ forloop.index0 }}">
87
+ <img
88
+ src="{{ image_url }}"
89
+ alt="{{ product.name | default: product.title }} - {{ forloop.index }}"
90
+ class="single-product-media-main-image"
91
+ loading="{% if forloop.first %}eager{% else %}lazy{% endif %}"
92
+ width="600"
93
+ height="600"
94
+ >
95
+ </div>
96
+ {% endfor %}
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Carousel Dots Indicator -->
101
+ {% if product.images.size > 1 %}
102
+ <div class="carousel-dots">
103
+ {% for image in product.images %}
104
+ <button class="carousel-dot {% if forloop.first %}active{% endif %}" data-index="{{ forloop.index0 }}" aria-label="Go to image {{ forloop.index }}" type="button"></button>
105
+ {% endfor %}
106
+ </div>
107
+ {% endif %}
108
+ {% elsif product.thumbnailImage %}
109
+ {% assign thumbImage = product.thumbnailImage %}
110
+ <div class="carousel-slide active" data-index="0">
77
111
  <img
78
- src="{{ image_url }}"
79
- alt="{{ product.name | default: product.title }} - {{ forloop.index }}"
80
- class="gallery-main-image {% if forloop.first %}active{% endif %}"
81
- data-index="{{ forloop.index0 }}"
82
- loading="{% if forloop.first %}eager{% else %}lazy{% endif %}"
112
+ src="{{ thumbImage.url | default: thumbImage }}"
113
+ alt="{{ product.name | default: product.title }}"
114
+ class="single-product-media-main-image"
115
+ loading="eager"
83
116
  width="600"
84
117
  height="600"
85
118
  >
86
- {% endfor %}
87
- {% elsif product.thumbnailImage %}
88
- {% assign thumbImage = product.thumbnailImage %}
89
- <img
90
- src="{{ thumbImage.url | default: thumbImage }}"
91
- alt="{{ product.name | default: product.title }}"
92
- class="gallery-main-image active"
93
- loading="eager"
94
- width="600"
95
- height="600"
96
- >
119
+ </div>
97
120
  {% else %}
98
- <div class="gallery-placeholder">
121
+ <div class="single-product-media-placeholder">
99
122
  <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
100
123
  <rect x="16" y="16" width="32" height="32" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
101
124
  <path d="M16 24L24 32L32 24L40 32L48 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -106,11 +129,11 @@
106
129
 
107
130
  <!-- Thumbnails -->
108
131
  {% if product.images and product.images.size > 1 %}
109
- <div class="gallery-thumbnails">
132
+ <div class="single-product-media-thumbnails">
110
133
  {% for image in product.images limit: 8 %}
111
134
  {% assign image_url = image.url | default: image %}
112
135
  <button
113
- class="gallery-thumbnail {% if forloop.first %}active{% endif %}"
136
+ class="single-product-media-thumbnail {% if forloop.first %}active{% endif %}"
114
137
  data-image="{{ image_url }}"
115
138
  data-index="{{ forloop.index0 }}"
116
139
  aria-label="View image {{ forloop.index }}"
@@ -137,10 +160,14 @@
137
160
 
138
161
  <!-- Price -->
139
162
  <div class="product-price-wrapper">
140
- <span class="price-current" id="singleProductPrice">
141
- {{ product.prices.price | money_with_settings: shop.settings }}
163
+ <span class="price-current" id="singleProductPrice-{{ widget.id }}">
164
+ {% if product_show_call_for_pricing %}
165
+ Call for pricing
166
+ {% else %}
167
+ {{ product.prices.price | money_with_settings: shop.settings }}
168
+ {% endif %}
142
169
  </span>
143
- {% if product.prices.mrp and product.prices.mrp > product.prices.price %}
170
+ {% if product_show_call_for_pricing == false and product.prices.mrp and product.prices.mrp > product.prices.price %}
144
171
  <span class="price-compare">
145
172
  {{ product.prices.mrp | money_with_settings: shop.settings }}
146
173
  </span>
@@ -155,7 +182,7 @@
155
182
  {% endif %}
156
183
 
157
184
  <!-- Product Form -->
158
- <form class="product-form" id="singleProductForm" data-product-id="{{ product.productId }}">
185
+ <form class="product-form" id="singleProductForm-{{ widget.id }}" data-product-id="{{ product.productId }}">
159
186
  <!-- Variations -->
160
187
  {% if product.variations and product.variations.size > 0 %}
161
188
  {% assign default_variant = null %}
@@ -170,8 +197,8 @@
170
197
  {% endif %}
171
198
 
172
199
  <div class="product-variation-selector">
173
- <label for="singleProductVariant" class="variation-label">Select Variant:</label>
174
- <select id="singleProductVariant" class="variation-select" name="variantId" aria-label="Select product variant">
200
+ <label for="singleProductVariant-{{ widget.id }}" class="variation-label">Select Variant:</label>
201
+ <select id="singleProductVariant-{{ widget.id }}" class="variation-select" name="variantId" aria-label="Select product variant">
175
202
  {% for variant in product.variations %}
176
203
  {% assign isAvailable = variant.inStock | default: variant.available | default: true %}
177
204
  {% assign variant_name = variant.name | default: '' %}
@@ -192,6 +219,7 @@
192
219
  value="{{ variant.productId }}"
193
220
  data-variant-price="{{ variant_price }}"
194
221
  data-variant-mrp="{{ variant.prices.mrp | default: 0 }}"
222
+ data-show-call-for-pricing="{{ variant.showCallForPricing | default: variant.ShowCallForPricing | default: variant.isCallForPricing | default: variant.IsCallForPricing | default: false }}"
195
223
  data-in-stock="{{ isAvailable }}"
196
224
  {% if variant.productId == default_variant.productId %}selected{% endif %}>
197
225
  {{ variant_name }}{% unless isAvailable %} — Out of Stock{% endunless %}
@@ -203,8 +231,22 @@
203
231
 
204
232
  <!-- Quantity (optional) -->
205
233
  <div class="product-quantity">
206
- <label for="singleProductQuantity">Quantity:</label>
207
- <input type="number" id="singleProductQuantity" name="quantity" value="1" min="1" class="quantity-input">
234
+ <label for="singleProductQuantity-{{ widget.id }}" class="quantity-label">Quantity</label>
235
+ <div class="quantity-wrapper">
236
+ <button type="button" class="quantity-btn quantity-decrease" aria-label="Decrease quantity">−</button>
237
+ <input
238
+ type="number"
239
+ id="singleProductQuantity-{{ widget.id }}"
240
+ name="quantity"
241
+ value="1"
242
+ min="1"
243
+ max="{{ product.stockQuantity | default: 99 }}"
244
+ class="quantity-input"
245
+ aria-label="Quantity"
246
+ readonly
247
+ >
248
+ <button type="button" class="quantity-btn quantity-increase" aria-label="Increase quantity">+</button>
249
+ </div>
208
250
  </div>
209
251
 
210
252
  <!-- Add to Cart Button -->
@@ -219,7 +261,8 @@
219
261
  class="btn-add-to-cart"
220
262
  data-product-id="{{ default_variant_id }}"
221
263
  data-base-product-id="{{ product.productId }}">
222
- Add to Cart
264
+ <span class="loading-spinner" aria-hidden="true"></span>
265
+ <span class="btn-add-to-cart__text">Add to Cart</span>
223
266
  </button>
224
267
  {% endif %}
225
268
  </form>
@@ -234,377 +277,772 @@
234
277
  </div>
235
278
 
236
279
  <style>
237
- .widget-single-product {
238
- padding: 40px 0;
280
+ :root {
281
+ --single-product-primary: {{ settings.color_primary | default: 'var(--color-primary, #111827)' }};
282
+ --single-product-primary-hover: {{ settings.color_primary_dark | default: 'var(--color-primary-dark, #0f172a)' }};
283
+ --single-product-text: {{ settings.color_text | default: 'var(--color-text, #111827)' }};
284
+ --single-product-text-muted: {{ settings.color_text_muted | default: 'var(--color-text-muted, #6b7280)' }};
285
+ --single-product-background: {{ settings.color_background | default: 'var(--color-background, #ffffff)' }};
286
+ --single-product-surface: {{ settings.color_surface | default: 'var(--color-surface, #f8fafc)' }};
287
+ --single-product-border: {{ settings.color_border | default: 'var(--color-border, #e5e7eb)' }};
288
+ --single-product-shadow: 0 10px 30px color-mix(in srgb, #0f172a 8%, transparent);
239
289
  }
240
290
 
291
+ .widget-single-product {
292
+ padding: 4rem 0;
293
+ }
294
+
241
295
  .widget-single-product__inner {
242
- max-width: 1200px;
296
+ max-width: 1240px;
243
297
  margin: 0 auto;
244
- padding: 0 24px;
298
+ padding: 0 1.25rem;
245
299
  }
246
300
 
247
301
  .widget-single-product .widget-header {
248
- margin-bottom: 32px;
302
+ margin: 0 0 2.5rem;
249
303
  text-align: center;
250
304
  }
251
305
 
252
306
  .widget-single-product .widget-title {
253
- font-size: 1.4rem;
254
- font-weight: 500;
255
- line-height: 1.3;
256
- margin: 0 0 8px 0;
257
- color: #111;
307
+ font-size: 2.35rem;
308
+ font-weight: 700;
309
+ letter-spacing: -0.02em;
310
+ line-height: 1.12;
311
+ margin: 0 0 0.5rem;
312
+ color: var(--single-product-text);
258
313
  }
259
314
 
260
315
  .widget-single-product .widget-subtitle {
261
- font-size: 1.3rem;
262
- font-weight: 400;
263
- line-height: 1.5;
264
- margin: 0;
265
- color: #666;
316
+ font-size: 1.15rem;
317
+ margin: 0 auto;
318
+ max-width: 760px;
319
+ color: var(--single-product-text-muted);
320
+ line-height: 1.65;
266
321
  }
267
322
 
268
- /* PDP Layout */
269
323
  .single-product-pdp__layout {
270
324
  display: grid;
271
- grid-template-columns: 1fr 1fr;
272
- gap: 48px;
325
+ grid-template-columns: 1fr;
326
+ gap: 1.5rem;
273
327
  align-items: start;
274
328
  }
275
329
 
276
- /* Gallery */
277
- .single-product-pdp__gallery {
330
+ @media (min-width: 960px) {
331
+ .single-product-pdp__layout {
332
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
333
+ gap: 2rem;
334
+ align-items: stretch;
335
+ }
336
+ }
337
+
338
+ .single-product-pdp__media {
278
339
  display: flex;
279
340
  flex-direction: column;
280
- gap: 16px;
341
+ gap: 0.75rem;
342
+ min-width: 0;
281
343
  }
282
344
 
283
- .gallery-main {
345
+ .single-product-media-main {
284
346
  position: relative;
285
- aspect-ratio: 1;
286
347
  width: 100%;
287
- background: #f5f5f5;
288
- border-radius: var(--border-radius-medium);
348
+ aspect-ratio: 4 / 3;
349
+ max-height: 520px;
350
+ background: linear-gradient(145deg, var(--single-product-surface), color-mix(in srgb, var(--single-product-surface) 80%, #ffffff));
351
+ border-radius: 1.25rem;
289
352
  overflow: hidden;
353
+ box-shadow: var(--single-product-shadow);
354
+ border: 1px solid color-mix(in srgb, var(--single-product-border) 85%, transparent);
290
355
  }
291
356
 
292
- .gallery-main-image {
293
- display: none;
357
+ .carousel-track-container {
358
+ width: 100%;
359
+ height: 100%;
360
+ overflow: hidden;
361
+ }
362
+
363
+ .carousel-track {
364
+ display: flex;
365
+ width: 100%;
366
+ height: 100%;
367
+ transition: transform 0.45s cubic-bezier(0.2, 0.65, 0.2, 1);
368
+ }
369
+
370
+ .carousel-slide {
371
+ flex: 0 0 100%;
372
+ width: 100%;
373
+ height: 100%;
374
+ position: relative;
375
+ }
376
+
377
+ .single-product-media-main-image {
294
378
  width: 100%;
295
379
  height: 100%;
296
380
  object-fit: cover;
381
+ }
382
+
383
+ .carousel-dots {
297
384
  position: absolute;
298
- top: 0;
299
- left: 0;
385
+ bottom: 1rem;
386
+ left: 50%;
387
+ transform: translateX(-50%);
388
+ display: flex;
389
+ gap: 0.45rem;
390
+ z-index: 2;
391
+ padding: 0.5rem 0.75rem;
392
+ background: color-mix(in srgb, var(--single-product-text) 18%, transparent);
393
+ border-radius: 999px;
394
+ backdrop-filter: blur(8px);
395
+ }
396
+
397
+ .carousel-dot {
398
+ width: 0.45rem;
399
+ height: 0.45rem;
400
+ border: 0;
401
+ padding: 0;
402
+ border-radius: 999px;
403
+ cursor: pointer;
404
+ background: color-mix(in srgb, var(--single-product-background) 68%, transparent);
405
+ transition: width 0.25s ease, background 0.25s ease;
300
406
  }
301
407
 
302
- .gallery-main-image.active {
303
- display: block;
408
+ .carousel-dot.active {
409
+ width: 1.2rem;
410
+ background: var(--single-product-background);
304
411
  }
305
412
 
306
- .gallery-placeholder {
413
+ .single-product-media-placeholder {
307
414
  width: 100%;
308
415
  height: 100%;
309
416
  display: flex;
310
417
  align-items: center;
311
418
  justify-content: center;
312
- color: #999;
419
+ color: var(--single-product-text-muted);
313
420
  }
314
421
 
315
- .gallery-thumbnails {
316
- display: flex;
317
- gap: 12px;
422
+ .single-product-media-thumbnails {
423
+ display: grid;
424
+ grid-auto-flow: column;
425
+ grid-auto-columns: 3.5rem;
426
+ gap: 0.55rem;
318
427
  overflow-x: auto;
319
- padding-bottom: 8px;
428
+ padding: 0.15rem 0 0.4rem;
429
+ scrollbar-width: thin;
430
+ scrollbar-color: var(--single-product-border) transparent;
431
+ }
432
+
433
+ .single-product-media-thumbnails::-webkit-scrollbar {
434
+ height: 0.35rem;
435
+ }
436
+
437
+ .single-product-media-thumbnails::-webkit-scrollbar-thumb {
438
+ background: var(--single-product-border);
439
+ border-radius: 999px;
320
440
  }
321
441
 
322
- .gallery-thumbnail {
323
- flex-shrink: 0;
324
- width: 80px;
325
- height: 80px;
326
- border: 2px solid transparent;
327
- border-radius: 6px;
442
+ .single-product-media-thumbnail {
443
+ width: 3.5rem;
444
+ height: 3.5rem;
445
+ border: 1px solid color-mix(in srgb, var(--single-product-border) 90%, transparent);
446
+ border-radius: 0.65rem;
328
447
  overflow: hidden;
329
- background: #f5f5f5;
330
- cursor: pointer;
448
+ background: var(--single-product-surface);
331
449
  padding: 0;
332
- transition: border-color 0.2s ease;
450
+ cursor: pointer;
451
+ transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
452
+ }
453
+
454
+ .single-product-media-thumbnail:hover {
455
+ transform: translateY(-2px);
333
456
  }
334
457
 
335
- .gallery-thumbnail.active {
336
- border-color: #111;
458
+ .single-product-media-thumbnail.active {
459
+ border-color: var(--single-product-primary);
460
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--single-product-primary) 20%, transparent);
337
461
  }
338
462
 
339
- .gallery-thumbnail img {
463
+ .single-product-media-thumbnail img {
340
464
  width: 100%;
341
465
  height: 100%;
342
466
  object-fit: cover;
343
467
  }
344
468
 
345
- /* Product Info */
346
469
  .single-product-pdp__info {
347
470
  display: flex;
348
471
  flex-direction: column;
349
- gap: 20px;
472
+ gap: 2.5rem;
473
+ background: color-mix(in srgb, var(--single-product-background) 95%, #ffffff);
474
+ border: 1px solid color-mix(in srgb, var(--single-product-border) 85%, transparent);
475
+ border-radius: 1.25rem;
476
+ padding: 1.25rem;
477
+ height:100%
478
+ }
479
+
480
+ @media (min-width: 960px) {
481
+ .single-product-pdp__media {
482
+ height: 100%;
483
+ }
484
+
485
+ .single-product-pdp__media.has-thumbnails {
486
+ display: grid;
487
+ grid-template-columns: 4.1rem minmax(0, 1fr);
488
+ grid-template-rows: minmax(0, 1fr);
489
+ gap: 0.85rem;
490
+ align-items: stretch;
491
+ }
492
+
493
+ .single-product-media-main {
494
+ flex: 1;
495
+ height: 100%;
496
+ max-height: none;
497
+ aspect-ratio: auto;
498
+ }
499
+
500
+ .single-product-pdp__media.has-thumbnails .single-product-media-main {
501
+ grid-column: 2;
502
+ grid-row: 1;
503
+ }
504
+
505
+ .single-product-pdp__media.has-thumbnails .single-product-media-thumbnails {
506
+ grid-column: 1;
507
+ grid-row: 1;
508
+ display: grid;
509
+ grid-auto-flow: row;
510
+ grid-auto-rows: 3.5rem;
511
+ grid-auto-columns: auto;
512
+ align-content: start;
513
+ overflow-y: auto;
514
+ overflow-x: hidden;
515
+ height: 100%;
516
+ max-height: 100%;
517
+ padding: 0 0.2rem 0 0;
518
+ }
519
+
520
+ .single-product-pdp__media.has-thumbnails .single-product-media-thumbnails::-webkit-scrollbar {
521
+ width: 0.35rem;
522
+ height: auto;
523
+ }
524
+
525
+ .single-product-pdp__info {
526
+ height: 100%;
527
+ }
350
528
  }
351
529
 
352
530
  .product-vendor {
353
- font-size: 14px;
531
+ display: inline-flex;
532
+ align-self: flex-start;
533
+ font-size: 0.8rem;
354
534
  text-transform: uppercase;
355
- letter-spacing: 0.05em;
356
- color: #666;
357
- font-weight: 500;
535
+ letter-spacing: 0.12em;
536
+ color: color-mix(in srgb, var(--single-product-primary) 75%, #334155);
537
+ background: color-mix(in srgb, var(--single-product-primary) 9%, transparent);
538
+ border: 1px solid color-mix(in srgb, var(--single-product-primary) 20%, transparent);
539
+ padding: 0.45rem 0.75rem;
540
+ border-radius: 999px;
541
+ font-weight: 700;
358
542
  }
359
543
 
360
544
  .product-title {
361
- font-size: 32px;
362
- font-weight: 600;
363
- line-height: 1.2;
364
545
  margin: 0;
365
- color: #111;
546
+ color: var(--single-product-text);
547
+ font-size: 1.8rem;
548
+ line-height: 1.16;
549
+ letter-spacing: -0.02em;
550
+ font-weight: 700;
366
551
  }
367
552
 
368
553
  .product-price-wrapper {
369
554
  display: flex;
370
555
  align-items: baseline;
371
- gap: 12px;
556
+ gap: 0.7rem;
557
+ flex-wrap: wrap;
372
558
  }
373
559
 
374
560
  .price-current {
375
- font-size: 28px;
376
- font-weight: 600;
377
- color: #111;
561
+ font-size: 1.65rem;
562
+ font-weight: 700;
563
+ line-height: 1;
564
+ color: var(--single-product-text);
378
565
  }
379
566
 
380
567
  .price-compare {
381
- font-size: 20px;
382
- color: #999;
568
+ font-size: 1rem;
569
+ color: var(--single-product-text-muted);
383
570
  text-decoration: line-through;
571
+ text-decoration-thickness: 1px;
384
572
  }
385
573
 
386
574
  .product-short-description {
387
- font-size: 16px;
575
+ font-size: 1.5rem;
388
576
  line-height: 1.6;
389
- color: #666;
390
- margin-top: 8px;
577
+ color: var(--single-product-text-muted);
578
+ padding-top: 0.25rem;
579
+ border-top: 1px solid color-mix(in srgb, var(--single-product-border) 80%, transparent);
391
580
  }
392
581
 
393
- /* Form */
394
582
  .product-form {
395
- display: flex;
396
- flex-direction: column;
397
- gap: 20px;
398
- margin-top: 24px;
583
+ display: grid;
584
+ gap: 1.8rem;
585
+ margin-top: 0.15rem;
399
586
  }
400
587
 
401
- .product-variation-selector {
402
- display: flex;
403
- flex-direction: column;
404
- gap: 8px;
588
+ .product-variation-selector,
589
+ .product-quantity {
590
+ display: flex;
591
+ gap: 3.5rem;
592
+ align-items: center;
405
593
  }
406
594
 
407
- .variation-label {
408
- font-size: 14px;
409
- font-weight: 600;
410
- color: #111;
595
+ .variation-label,
596
+ .quantity-label {
597
+ font-size: 1.3rem;
598
+
599
+ color: var(--single-product-text);
600
+ letter-spacing: 0.08em;
601
+
411
602
  }
412
603
 
413
604
  .variation-select {
414
605
  width: 100%;
415
- padding: 12px 16px;
416
- font-size: 16px;
417
- border: 1px solid #e0e0e0;
418
- border-radius: var(--border-radius-medium);
419
- background: #fff;
420
- cursor: pointer;
421
- transition: border-color 0.2s ease;
606
+ min-height: 2.5rem;
607
+ padding: 0.5rem 0.8rem;
608
+ border-radius: 0.6rem;
609
+ border: 1px solid var(--single-product-border);
610
+ background: var(--single-product-background);
611
+ color: var(--single-product-text);
612
+ font-size: 0.92rem;
613
+ outline: none;
614
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
422
615
  }
423
616
 
424
- .variation-select:hover {
425
- border-color: #999;
617
+ .variation-select:focus {
618
+ border-color: color-mix(in srgb, var(--single-product-primary) 60%, #93c5fd);
619
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--single-product-primary) 14%, transparent);
426
620
  }
427
621
 
428
- .variation-select:focus {
429
- outline: none;
430
- border-color: #111;
431
- box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05);
622
+ .quantity-wrapper {
623
+ display: flex;
624
+ border: 1px solid var(--single-product-border);
625
+ border-radius: 0.6rem;
626
+ overflow: hidden;
627
+ background: var(--single-product-background);
628
+ align-items: center;
629
+ width: fit-content;
432
630
  }
433
631
 
434
- .product-quantity {
435
- display: flex;
436
- flex-direction: column;
437
- gap: 8px;
632
+ .quantity-btn {
633
+ width: 44px;
634
+ height: 44px;
635
+ border: none;
636
+ background: transparent;
637
+ cursor: pointer;
638
+ display: flex;
639
+ align-items: center;
640
+ justify-content: center;
641
+ transition: background 0.2s ease;
642
+ color: #111;
643
+ font-size: 20px;
644
+ font-weight: 300;
645
+ line-height: 1;
438
646
  }
439
647
 
440
- .product-quantity label {
441
- font-size: 14px;
442
- font-weight: 600;
443
- color: #111;
648
+ .quantity-btn:hover:not(:disabled) {
649
+ background: color-mix(in srgb, var(--single-product-surface) 88%, #ffffff);
444
650
  }
445
651
 
446
652
  .quantity-input {
447
- width: 120px;
448
- padding: 12px 16px;
449
- font-size: 16px;
450
- border: 1px solid #e0e0e0;
451
- border-radius: 8px;
653
+ width: 60px;
654
+ height: 44px;
655
+ border: none;
656
+ border-left: 1px solid #e5e7eb;
657
+ border-right: 1px solid #e5e7eb;
658
+ text-align: center;
659
+ font-weight: 400;
660
+ font-size: 14px;
661
+ background: #ffffff;
662
+ color: #111;
452
663
  }
453
664
 
454
- .btn-add-to-cart {
455
- width: 100%;
456
- padding: 16px 24px;
457
- font-size: 18px;
458
- font-weight: 600;
459
- color: #fff;
460
- background: #111;
461
- border: none;
462
- border-radius: 8px;
463
- cursor: pointer;
464
- transition: background 0.2s ease, transform 0.1s ease;
465
- margin-top: 8px;
665
+ .quantity-input::-webkit-inner-spin-button,
666
+ .quantity-input::-webkit-outer-spin-button {
667
+ -webkit-appearance: none;
668
+ margin: 0;
466
669
  }
467
670
 
671
+ .btn-add-to-cart {
672
+ padding: 16px 32px;
673
+ border-radius: var(--border-radius-medium);
674
+ font-weight: 500;
675
+ font-size: 14px;
676
+ cursor: pointer;
677
+ transition: all 0.2s ease;
678
+ display: inline-flex;
679
+ align-items: center;
680
+ justify-content: center;
681
+ gap: 8px;
682
+ border: 1px solid transparent;
683
+ text-transform: uppercase;
684
+ letter-spacing: 0.05em;
685
+ white-space: nowrap;
686
+ width: 100%;
687
+ background: #111;
688
+ color: #ffffff;
689
+ border-color: #111;
690
+ }
691
+
692
+
468
693
  .btn-add-to-cart:hover:not(:disabled) {
469
- background: #333;
694
+ transform: translateY(-1px);
695
+ box-shadow: 0 16px 30px color-mix(in srgb, var(--single-product-primary) 22%, transparent);
696
+ filter: brightness(1.03);
470
697
  }
471
698
 
472
699
  .btn-add-to-cart:active:not(:disabled) {
473
- transform: scale(0.98);
700
+ transform: translateY(0);
474
701
  }
475
702
 
476
703
  .btn-add-to-cart:disabled {
477
- background: #d1d5db;
478
704
  cursor: not-allowed;
705
+ background: color-mix(in srgb, var(--single-product-border) 88%, transparent);
706
+ box-shadow: none;
707
+ color: color-mix(in srgb, var(--single-product-text-muted) 85%, #9ca3af);
708
+ }
709
+
710
+ .btn-add-to-cart .loading-spinner {
711
+ display: none;
712
+ width: 0.9rem;
713
+ height: 0.9rem;
714
+ border: 2px solid color-mix(in srgb, var(--single-product-background) 35%, transparent);
715
+ border-top-color: var(--single-product-background);
716
+ border-radius: 50%;
717
+ animation: spin 0.8s linear infinite;
718
+ }
719
+
720
+ @keyframes spin {
721
+ to {
722
+ transform: rotate(360deg);
723
+ }
724
+ }
725
+
726
+ .btn-add-to-cart.loading .loading-spinner {
727
+ display: inline-block;
479
728
  }
480
729
 
481
730
  .widget-empty {
482
- padding: 60px 24px;
731
+ padding: 3rem 1.25rem;
483
732
  text-align: center;
484
- color: #6b7280;
733
+ color: var(--single-product-text-muted);
485
734
  }
486
735
 
487
736
  .widget-empty p {
488
737
  margin: 0;
489
- font-size: 14px;
738
+ font-size: 1rem;
490
739
  }
491
740
 
492
- /* Mobile */
493
- @media (max-width: 768px) {
741
+ @media (max-width: 1024px) {
494
742
  .widget-single-product {
495
- padding: 32px 0;
743
+ padding: 3rem 0;
744
+ }
745
+
746
+ .widget-single-product .widget-title {
747
+ font-size: 2rem;
748
+ }
749
+
750
+ .product-title {
751
+ font-size: 1.8rem;
496
752
  }
753
+ }
497
754
 
755
+ @media (max-width: 640px) {
498
756
  .widget-single-product__inner {
499
- padding: 0 16px;
757
+ padding: 0 0.9rem;
500
758
  }
501
759
 
502
- .single-product-pdp__layout {
503
- grid-template-columns: 1fr;
504
- gap: 32px;
760
+ .single-product-pdp__info {
761
+ padding: 1.1rem;
762
+ border-radius: 1rem;
505
763
  }
506
764
 
507
- .product-title {
508
- font-size: 24px;
765
+ .widget-single-product .widget-title {
766
+ font-size: 1.7rem;
509
767
  }
510
768
 
511
- .price-current {
512
- font-size: 24px;
769
+ .widget-single-product .widget-subtitle {
770
+ font-size: 1rem;
513
771
  }
514
772
 
515
- .price-compare {
516
- font-size: 18px;
773
+ .price-current {
774
+ font-size: 1.65rem;
517
775
  }
518
- }
519
776
 
520
- @media (max-width: 480px) {
521
- .widget-single-product {
522
- padding: 24px 0;
777
+ .single-product-media-thumbnails {
778
+ grid-auto-columns: 4rem;
523
779
  }
524
780
 
525
- .widget-single-product__inner {
526
- padding: 0 12px;
781
+ .single-product-media-thumbnail {
782
+ width: 4rem;
783
+ height: 4rem;
527
784
  }
528
785
 
529
- .gallery-thumbnails {
530
- gap: 8px;
786
+ .single-product-media-main {
787
+ aspect-ratio: 1 / 1;
788
+ max-height: none;
531
789
  }
790
+ }
791
+ </style>
532
792
 
533
- .gallery-thumbnail {
534
- width: 60px;
535
- height: 60px;
793
+ {% if product %}<script>
794
+ (() => {
795
+ const widgetRoot = (document.currentScript && document.currentScript.closest('.widget-single-product'))
796
+ || document.querySelector('.widget-single-product[data-widget-id="{{ widget.id }}"]');
797
+ if (!widgetRoot) return;
798
+ if (widgetRoot.dataset.singleProductBound === 'true') return;
799
+ widgetRoot.dataset.singleProductBound = 'true';
800
+
801
+ const currencySymbol = '{{ shop.settings.currencySymbol | default: shop.currency | default: "$" }}';
802
+ let touchStartX = 0;
803
+ let touchEndX = 0;
804
+
805
+ function formatMoney(value) {
806
+ const amount = Number(value || 0);
807
+ return currencySymbol + amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
808
+ }
809
+
810
+ function getCarouselState() {
811
+ return {
812
+ track: widgetRoot.querySelector('.carousel-track'),
813
+ slides: Array.from(widgetRoot.querySelectorAll('.carousel-slide')),
814
+ dots: Array.from(widgetRoot.querySelectorAll('.carousel-dot')),
815
+ thumbnails: Array.from(widgetRoot.querySelectorAll('.single-product-media-thumbnail'))
816
+ };
817
+ }
818
+
819
+ function setCarouselIndex(index) {
820
+ const state = getCarouselState();
821
+ if (!state.track || state.slides.length === 0) return;
822
+
823
+ const totalSlides = state.slides.length;
824
+ let safeIndex = index;
825
+ if (safeIndex < 0) safeIndex = totalSlides - 1;
826
+ if (safeIndex >= totalSlides) safeIndex = 0;
827
+ widgetRoot.dataset.currentSlideIndex = String(safeIndex);
828
+
829
+ state.track.style.transform = `translateX(-${safeIndex * 100}%)`;
830
+ state.dots.forEach((dot, i) => dot.classList.toggle('active', i === safeIndex));
831
+ state.thumbnails.forEach((thumb, i) => thumb.classList.toggle('active', i === safeIndex));
832
+ }
833
+
834
+ function getCurrentSlideIndex() {
835
+ return parseInt(widgetRoot.dataset.currentSlideIndex || '0', 10) || 0;
836
+ }
837
+
838
+ function openLoginModal() {
839
+ if (window.Theme && typeof window.Theme.openLoginModal === 'function') {
840
+ window.Theme.openLoginModal();
841
+ return;
842
+ }
843
+ if (window.CartManager && typeof window.CartManager.openLoginModal === 'function') {
844
+ window.CartManager.openLoginModal();
845
+ return;
846
+ }
847
+ const loginTrigger = document.querySelector('[data-login-modal-trigger]');
848
+ if (loginTrigger) loginTrigger.click();
849
+ }
850
+
851
+ function isUserLoggedIn() {
852
+ return document.cookie.includes('O2VENDIsUserLoggedin=true') || document.cookie.includes('O2VENDUserToken=');
853
+ }
854
+
855
+ async function addToCartAPI(productId, quantity) {
856
+ if (window.Theme && typeof window.Theme.addToCart === 'function') {
857
+ const wasLoggedIn = isUserLoggedIn();
858
+ const themeResult = await window.Theme.addToCart(productId, quantity, true);
859
+ const isLoggedInNow = isUserLoggedIn();
860
+
861
+ if (!wasLoggedIn || !isLoggedInNow) {
862
+ throw new Error('Authentication required');
536
863
  }
537
864
 
538
- .product-title {
539
- font-size: 20px;
865
+ if (themeResult === false || (themeResult && themeResult.success === false)) {
866
+ if (themeResult && themeResult.requiresAuth) {
867
+ openLoginModal();
868
+ throw new Error('Authentication required');
869
+ }
870
+ throw new Error((themeResult && (themeResult.error || themeResult.message)) || 'Failed to add to cart');
540
871
  }
872
+ return { success: true, viaTheme: true, data: themeResult };
873
+ }
541
874
 
542
- .btn-add-to-cart {
543
- padding: 14px 20px;
544
- font-size: 16px;
875
+ const response = await fetch('/webstoreapi/carts/add', {
876
+ method: 'POST',
877
+ headers: {
878
+ 'Content-Type': 'application/json',
879
+ 'X-Requested-With': 'XMLHttpRequest',
880
+ 'Accept': 'application/json'
881
+ },
882
+ body: JSON.stringify({ productId: productId, quantity: quantity })
883
+ });
884
+
885
+ let data = {};
886
+ try {
887
+ data = await response.json();
888
+ } catch (error) {
889
+ data = {};
890
+ }
891
+
892
+ if (!response.ok || !data.success) {
893
+ if (data.requiresAuth || response.status === 401 || response.status === 403 || response.status === 404) {
894
+ openLoginModal();
895
+ throw new Error('Authentication required');
545
896
  }
897
+ throw new Error(data.error || data.message || 'Failed to add to cart');
546
898
  }
547
- </style>
548
899
 
549
- {% if product %}
550
- <script>
551
- (function() {
552
- const form = document.getElementById('singleProductForm');
553
- if (!form) return;
554
-
555
- const variantSelect = document.getElementById('singleProductVariant');
556
- const priceElement = document.getElementById('singleProductPrice');
557
- const addToCartBtn = form.querySelector('.btn-add-to-cart');
558
-
559
- // Handle variant selection
560
- if (variantSelect) {
561
- variantSelect.addEventListener('change', function() {
562
- const selectedOption = this.options[this.selectedIndex];
563
- const variantPrice = parseFloat(selectedOption.dataset.variantPrice);
564
- const variantMrp = parseFloat(selectedOption.dataset.variantMrp) || 0;
565
- const isInStock = selectedOption.dataset.inStock === 'true';
566
-
567
- // Update price
568
- if (priceElement) {
569
- const currencySymbol = '{{ shop.settings.currencySymbol | default: shop.currency | default: "$" }}';
570
- priceElement.textContent = currencySymbol + variantPrice.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
571
- }
572
-
573
- // Update add to cart button
574
- if (addToCartBtn) {
575
- addToCartBtn.setAttribute('data-product-id', selectedOption.value);
576
- addToCartBtn.disabled = !isInStock;
577
- addToCartBtn.textContent = isInStock ? 'Add to Cart' : 'Out of Stock';
578
- }
579
- });
900
+ return { success: true, viaTheme: false, data: data };
901
+ }
902
+
903
+ widgetRoot.addEventListener('click', (event) => {
904
+ const dot = event.target.closest('.carousel-dot');
905
+ if (dot && widgetRoot.contains(dot)) {
906
+ const index = parseInt(dot.dataset.index || '0', 10);
907
+ if (!Number.isNaN(index)) setCarouselIndex(index);
908
+ return;
909
+ }
910
+
911
+ const thumbnail = event.target.closest('.single-product-media-thumbnail');
912
+ if (thumbnail && widgetRoot.contains(thumbnail)) {
913
+ const index = parseInt(thumbnail.dataset.index || '0', 10);
914
+ if (!Number.isNaN(index)) setCarouselIndex(index);
915
+ return;
916
+ }
917
+
918
+ const decreaseBtn = event.target.closest('.quantity-decrease');
919
+ if (decreaseBtn && widgetRoot.contains(decreaseBtn)) {
920
+ const quantityInput = widgetRoot.querySelector('.quantity-input');
921
+ if (!quantityInput) return;
922
+ const value = parseInt(quantityInput.value || '1', 10);
923
+ if (value > 1) quantityInput.value = String(value - 1);
924
+ return;
925
+ }
926
+
927
+ const increaseBtn = event.target.closest('.quantity-increase');
928
+ if (increaseBtn && widgetRoot.contains(increaseBtn)) {
929
+ const quantityInput = widgetRoot.querySelector('.quantity-input');
930
+ if (!quantityInput) return;
931
+ const value = parseInt(quantityInput.value || '1', 10);
932
+ const max = parseInt(quantityInput.max || '99', 10);
933
+ if (value < max) quantityInput.value = String(value + 1);
934
+ }
935
+ });
936
+
937
+ widgetRoot.addEventListener('change', (event) => {
938
+ const variantSelect = event.target.closest('.variation-select');
939
+ if (!variantSelect || !widgetRoot.contains(variantSelect)) return;
940
+
941
+ const option = variantSelect.options[variantSelect.selectedIndex];
942
+ if (!option) return;
943
+
944
+ const addToCartBtn = widgetRoot.querySelector('.btn-add-to-cart');
945
+ const priceCurrent = widgetRoot.querySelector('.price-current');
946
+ const priceCompare = widgetRoot.querySelector('.price-compare');
947
+ const price = Number(option.dataset.variantPrice || 0);
948
+ const mrp = Number(option.dataset.variantMrp || 0);
949
+ const inStock = option.dataset.inStock === 'true';
950
+ const callForPricing = option.dataset.showCallForPricing === 'true' || option.dataset.showCallForPricing === '1';
951
+
952
+ if (priceCurrent) {
953
+ priceCurrent.textContent = callForPricing ? 'Call for pricing' : formatMoney(price);
954
+ }
955
+
956
+ if (priceCompare) {
957
+ if (!callForPricing && mrp > price) {
958
+ priceCompare.textContent = formatMoney(mrp);
959
+ priceCompare.style.display = '';
960
+ } else {
961
+ priceCompare.style.display = 'none';
580
962
  }
963
+ }
964
+
965
+ if (addToCartBtn) {
966
+ addToCartBtn.setAttribute('data-product-id', option.value);
967
+ addToCartBtn.disabled = !inStock;
968
+ const textEl = addToCartBtn.querySelector('.btn-add-to-cart__text');
969
+ if (textEl) textEl.textContent = inStock ? 'Add to Cart' : 'Out of Stock';
970
+ }
971
+ });
972
+
973
+ widgetRoot.addEventListener('submit', async (event) => {
974
+ const form = event.target.closest('.product-form');
975
+ if (!form || !widgetRoot.contains(form)) return;
976
+
977
+ event.preventDefault();
978
+
979
+ const addToCartBtn = form.querySelector('.btn-add-to-cart');
980
+ if (!addToCartBtn || addToCartBtn.disabled || addToCartBtn.classList.contains('loading')) return;
981
+
982
+ const quantityInput = form.querySelector('.quantity-input');
983
+ const productId = addToCartBtn.getAttribute('data-product-id') || form.getAttribute('data-product-id');
984
+ const quantity = parseInt((quantityInput && quantityInput.value) ? quantityInput.value : '1', 10);
985
+ if (!productId) return;
986
+
987
+ const textEl = addToCartBtn.querySelector('.btn-add-to-cart__text');
988
+ const originalText = textEl ? textEl.textContent : 'Add to Cart';
989
+ addToCartBtn.classList.add('loading');
990
+ addToCartBtn.disabled = true;
991
+ if (textEl) textEl.textContent = 'Adding...';
581
992
 
582
- // Handle form submission
583
- form.addEventListener('submit', function(e) {
584
- e.preventDefault();
585
-
586
- const productId = addToCartBtn ? addToCartBtn.getAttribute('data-product-id') : form.getAttribute('data-product-id');
587
- const quantity = parseInt(document.getElementById('singleProductQuantity')?.value || '1');
588
-
589
- // Prefer Theme.addToCart: unified add-to-cart flow + notifications + CartManager integration
590
- if (window.Theme && typeof window.Theme.addToCart === 'function') {
591
- window.Theme.addToCart(productId, quantity);
592
- } else if (window.CartManager && typeof window.CartManager.addToCart === 'function') {
593
- // Fallback to CartManager if Theme.addToCart is not available
594
- window.CartManager.addToCart(productId, quantity)
595
- .then(function() {
596
- console.log('Product added to cart');
597
- })
598
- .catch(function(error) {
599
- console.error('Failed to add to cart:', error);
600
- alert('Failed to add product to cart. Please try again.');
601
- });
993
+ try {
994
+ const addResult = await addToCartAPI(productId, quantity);
995
+ if (textEl) textEl.textContent = 'Added';
996
+ if (addResult && addResult.viaTheme !== true && window.Theme && typeof window.Theme.showNotification === 'function') {
997
+ window.Theme.showNotification('Product added to cart!', 'success', 3000);
998
+ }
999
+ } catch (error) {
1000
+ if (!String((error && error.message) || '').toLowerCase().includes('auth')) {
1001
+ if (window.Theme && typeof window.Theme.showNotification === 'function') {
1002
+ window.Theme.showNotification('Failed to add product to cart. Please try again.', 'error', 3000);
1003
+ }
1004
+ }
1005
+ } finally {
1006
+ setTimeout(() => {
1007
+ addToCartBtn.classList.remove('loading');
1008
+ const variantSelect = form.querySelector('.variation-select');
1009
+ if (variantSelect) {
1010
+ const selectedOption = variantSelect.options[variantSelect.selectedIndex];
1011
+ addToCartBtn.disabled = selectedOption ? selectedOption.dataset.inStock !== 'true' : false;
602
1012
  } else {
603
- console.warn('No add-to-cart handler available (Theme.addToCart / CartManager.addToCart)');
604
- alert('Cart functionality not available. Please try again later.');
1013
+ addToCartBtn.disabled = false;
605
1014
  }
606
- });
607
- })();
608
- </script>
1015
+ if (textEl) textEl.textContent = originalText;
1016
+ }, 1200);
1017
+ }
1018
+ });
1019
+
1020
+ widgetRoot.addEventListener('keydown', (event) => {
1021
+ if (event.key === 'ArrowLeft') {
1022
+ setCarouselIndex(getCurrentSlideIndex() - 1);
1023
+ }
1024
+ if (event.key === 'ArrowRight') {
1025
+ setCarouselIndex(getCurrentSlideIndex() + 1);
1026
+ }
1027
+ });
1028
+
1029
+ const carouselContainer = widgetRoot.querySelector('.single-product-media-main');
1030
+ if (carouselContainer) {
1031
+ carouselContainer.addEventListener('touchstart', (event) => {
1032
+ touchStartX = event.changedTouches[0].screenX;
1033
+ }, { passive: true });
1034
+ carouselContainer.addEventListener('touchend', (event) => {
1035
+ touchEndX = event.changedTouches[0].screenX;
1036
+ const diff = touchStartX - touchEndX;
1037
+ if (Math.abs(diff) > 50) {
1038
+ setCarouselIndex(diff > 0 ? getCurrentSlideIndex() + 1 : getCurrentSlideIndex() - 1);
1039
+ }
1040
+ }, { passive: true });
1041
+ }
1042
+
1043
+ setCarouselIndex(0);
1044
+ })();
1045
+ </script>
1046
+
609
1047
  {% endif %}
610
1048
  </section>