@o2vend/theme-cli 1.0.32

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 (116) hide show
  1. package/README.md +425 -0
  2. package/assets/Logo_o2vend.png +0 -0
  3. package/assets/favicon.png +0 -0
  4. package/assets/logo-white.png +0 -0
  5. package/bin/o2vend +42 -0
  6. package/config/widget-map.json +50 -0
  7. package/lib/commands/check.js +201 -0
  8. package/lib/commands/generate.js +33 -0
  9. package/lib/commands/init.js +214 -0
  10. package/lib/commands/optimize.js +216 -0
  11. package/lib/commands/package.js +208 -0
  12. package/lib/commands/serve.js +105 -0
  13. package/lib/commands/validate.js +191 -0
  14. package/lib/lib/api-client.js +357 -0
  15. package/lib/lib/dev-server.js +2618 -0
  16. package/lib/lib/file-watcher.js +80 -0
  17. package/lib/lib/hot-reload.js +106 -0
  18. package/lib/lib/liquid-engine.js +822 -0
  19. package/lib/lib/liquid-filters.js +671 -0
  20. package/lib/lib/mock-api-server.js +989 -0
  21. package/lib/lib/mock-data.js +1468 -0
  22. package/lib/lib/widget-service.js +321 -0
  23. package/package.json +70 -0
  24. package/test-theme/README.md +27 -0
  25. package/test-theme/assets/async-sections.js +446 -0
  26. package/test-theme/assets/cart-drawer.js +463 -0
  27. package/test-theme/assets/cart-manager.js +223 -0
  28. package/test-theme/assets/checkout-price-handler.js +368 -0
  29. package/test-theme/assets/components.css +4629 -0
  30. package/test-theme/assets/delivery-zone.css +299 -0
  31. package/test-theme/assets/delivery-zone.js +396 -0
  32. package/test-theme/assets/logo.png +0 -0
  33. package/test-theme/assets/sections.css +48 -0
  34. package/test-theme/assets/theme.css +3500 -0
  35. package/test-theme/assets/theme.js +3745 -0
  36. package/test-theme/config/settings_data.json +292 -0
  37. package/test-theme/config/settings_schema.json +1050 -0
  38. package/test-theme/layout/theme.liquid +195 -0
  39. package/test-theme/locales/en.default.json +260 -0
  40. package/test-theme/sections/content-fallback.liquid +53 -0
  41. package/test-theme/sections/content.liquid +57 -0
  42. package/test-theme/sections/footer-fallback.liquid +328 -0
  43. package/test-theme/sections/footer.liquid +278 -0
  44. package/test-theme/sections/header-fallback.liquid +1805 -0
  45. package/test-theme/sections/header.liquid +1145 -0
  46. package/test-theme/sections/hero-fallback.liquid +212 -0
  47. package/test-theme/sections/hero.liquid +136 -0
  48. package/test-theme/snippets/account-sidebar.liquid +200 -0
  49. package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
  50. package/test-theme/snippets/breadcrumbs.liquid +134 -0
  51. package/test-theme/snippets/cart-drawer.liquid +467 -0
  52. package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
  53. package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
  54. package/test-theme/snippets/delivery-zone-search.liquid +78 -0
  55. package/test-theme/snippets/icon.liquid +105 -0
  56. package/test-theme/snippets/login-modal.liquid +346 -0
  57. package/test-theme/snippets/mega-menu.liquid +812 -0
  58. package/test-theme/snippets/news-thumbnail.liquid +187 -0
  59. package/test-theme/snippets/pagination.liquid +120 -0
  60. package/test-theme/snippets/price.liquid +92 -0
  61. package/test-theme/snippets/product-card-related.liquid +78 -0
  62. package/test-theme/snippets/product-card-simple.liquid +41 -0
  63. package/test-theme/snippets/product-card.liquid +697 -0
  64. package/test-theme/snippets/rating.liquid +85 -0
  65. package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
  66. package/test-theme/snippets/skeleton-product-card.liquid +124 -0
  67. package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
  68. package/test-theme/snippets/social-sharing.liquid +185 -0
  69. package/test-theme/templates/account/dashboard.liquid +401 -0
  70. package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
  71. package/test-theme/templates/account/loyalty.liquid +588 -0
  72. package/test-theme/templates/account/order-detail.liquid +230 -0
  73. package/test-theme/templates/account/orders.liquid +349 -0
  74. package/test-theme/templates/account/profile.liquid +758 -0
  75. package/test-theme/templates/account/register.liquid +232 -0
  76. package/test-theme/templates/account/return-orders.liquid +348 -0
  77. package/test-theme/templates/account/store-credit.liquid +464 -0
  78. package/test-theme/templates/account/subscriptions.liquid +601 -0
  79. package/test-theme/templates/account/wishlist.liquid +419 -0
  80. package/test-theme/templates/address-book.liquid +1092 -0
  81. package/test-theme/templates/categories.liquid +452 -0
  82. package/test-theme/templates/checkout.liquid +4511 -0
  83. package/test-theme/templates/error.liquid +384 -0
  84. package/test-theme/templates/index.liquid +11 -0
  85. package/test-theme/templates/login.liquid +185 -0
  86. package/test-theme/templates/order-confirmation.liquid +720 -0
  87. package/test-theme/templates/page.liquid +297 -0
  88. package/test-theme/templates/product-detail.liquid +4363 -0
  89. package/test-theme/templates/products.liquid +518 -0
  90. package/test-theme/templates/search.liquid +922 -0
  91. package/test-theme/theme.json.example +19 -0
  92. package/test-theme/widgets/brand-carousel.liquid +676 -0
  93. package/test-theme/widgets/brand.liquid +245 -0
  94. package/test-theme/widgets/carousel.liquid +843 -0
  95. package/test-theme/widgets/category-list-carousel.liquid +656 -0
  96. package/test-theme/widgets/category-list.liquid +340 -0
  97. package/test-theme/widgets/category.liquid +475 -0
  98. package/test-theme/widgets/discount-time.liquid +176 -0
  99. package/test-theme/widgets/footer-menu.liquid +695 -0
  100. package/test-theme/widgets/footer.liquid +179 -0
  101. package/test-theme/widgets/gallery.liquid +271 -0
  102. package/test-theme/widgets/header-menu.liquid +932 -0
  103. package/test-theme/widgets/header.liquid +159 -0
  104. package/test-theme/widgets/html.liquid +214 -0
  105. package/test-theme/widgets/news.liquid +217 -0
  106. package/test-theme/widgets/product-canvas.liquid +235 -0
  107. package/test-theme/widgets/product-carousel.liquid +502 -0
  108. package/test-theme/widgets/product.liquid +45 -0
  109. package/test-theme/widgets/recently-viewed.liquid +26 -0
  110. package/test-theme/widgets/shared/product-grid.liquid +339 -0
  111. package/test-theme/widgets/simple-product.liquid +42 -0
  112. package/test-theme/widgets/single-product.liquid +610 -0
  113. package/test-theme/widgets/spacebar-carousel.liquid +663 -0
  114. package/test-theme/widgets/spacebar.liquid +279 -0
  115. package/test-theme/widgets/splash.liquid +378 -0
  116. package/test-theme/widgets/testimonial-carousel.liquid +709 -0
@@ -0,0 +1,610 @@
1
+ {% liquid
2
+ assign widget_settings = widget.settings
3
+ assign widget_data = widget.data
4
+ assign widget_content = widget.content
5
+
6
+ comment
7
+ Dynamic Widget: Product comes from widget_data.products (enriched by WidgetService)
8
+ Single Product widget displays one product with PDP-like layout
9
+ Settings come from widget_settings
10
+ endcomment
11
+ assign products = widget_data.products
12
+ assign product = products.first
13
+
14
+ comment
15
+ Title can come from widget_settings.title, widget.Title, or widget_content.ProductName
16
+ endcomment
17
+ assign heading = widget_settings.title | default: widget.Title | default: widget_content.ProductName | default: widget_content.productName
18
+
19
+ assign description = widget_settings.subtitle | default: widget.Subtitle
20
+ assign show_widget_title_raw = widget_settings.showWidgetTitle | default: widget_content.ShowWidgetTitle | default: 'Yes'
21
+
22
+ if show_widget_title_raw == null or show_widget_title_raw == blank or show_widget_title_raw == 'null'
23
+ assign show_widget_title = true
24
+ else
25
+ if show_widget_title_raw == 'Yes'
26
+ assign show_widget_title = true
27
+ elsif show_widget_title_raw == true
28
+ assign show_widget_title = true
29
+ else
30
+ assign show_widget_title = false
31
+ endif
32
+ endif
33
+
34
+ assign widget_title_alignment_raw = widget_settings.widgetTitleAlignment | default: widget_content.WidgetTitleAlignment | default: 'center'
35
+ assign widget_title_alignment = widget_title_alignment_raw | downcase
36
+ if widget_title_alignment != 'left' and widget_title_alignment != 'right' and widget_title_alignment != 'center'
37
+ assign widget_title_alignment = 'center'
38
+ endif
39
+
40
+ assign background_color = widget_settings.backgroundColor | default: widget_settings.BackgroundColor | default: widget_content.BackgroundColor | default: widget_content.backgroundColor
41
+ assign text_color = widget_settings.textColor | default: widget_settings.TextColor | default: widget_content.TextColor | default: widget_content.textColor
42
+ assign show_add_to_cart = widget_settings.showAddToCartButton | default: widget_content.ShowAddToCartButton | default: true
43
+
44
+ comment
45
+ Extract short description (first 200 chars or until first paragraph)
46
+ 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
53
+ endif
54
+ endif
55
+ %}
56
+
57
+ <section class="widget widget-single-product" data-widget-id="{{ widget.id }}"{% if background_color and background_color != blank and background_color != 'null' %} style="background-color: {{ background_color }};"{% endif %}>
58
+ <div class="widget-single-product__inner">
59
+ {% if show_widget_title and heading and heading != blank %}
60
+ <div class="widget-header" style="text-align: {{ widget_title_alignment }};{% if text_color and text_color != blank and text_color != 'null' %} color: {{ text_color }};{% endif %}">
61
+ <h2 class="widget-title">{{ heading }}</h2>
62
+ {% if description and description != blank %}
63
+ <p class="widget-subtitle">{{ description }}</p>
64
+ {% endif %}
65
+ </div>
66
+ {% endif %}
67
+
68
+ {% if product %}
69
+ <div class="single-product-pdp">
70
+ <div class="single-product-pdp__layout">
71
+ <!-- Product Gallery -->
72
+ <div class="single-product-pdp__gallery">
73
+ <div class="gallery-main">
74
+ {% if product.images and product.images.size > 0 %}
75
+ {% for image in product.images %}
76
+ {% assign image_url = image.url | default: image %}
77
+ <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 %}"
83
+ width="600"
84
+ height="600"
85
+ >
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
+ >
97
+ {% else %}
98
+ <div class="gallery-placeholder">
99
+ <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
100
+ <rect x="16" y="16" width="32" height="32" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
101
+ <path d="M16 24L24 32L32 24L40 32L48 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
102
+ </svg>
103
+ </div>
104
+ {% endif %}
105
+ </div>
106
+
107
+ <!-- Thumbnails -->
108
+ {% if product.images and product.images.size > 1 %}
109
+ <div class="gallery-thumbnails">
110
+ {% for image in product.images limit: 8 %}
111
+ {% assign image_url = image.url | default: image %}
112
+ <button
113
+ class="gallery-thumbnail {% if forloop.first %}active{% endif %}"
114
+ data-image="{{ image_url }}"
115
+ data-index="{{ forloop.index0 }}"
116
+ aria-label="View image {{ forloop.index }}"
117
+ type="button"
118
+ >
119
+ <img src="{{ image_url }}" alt="{{ product.name }} - {{ forloop.index }}" loading="lazy" width="80" height="80">
120
+ </button>
121
+ {% endfor %}
122
+ </div>
123
+ {% endif %}
124
+ </div>
125
+
126
+ <!-- Product Info -->
127
+ <div class="single-product-pdp__info"{% if text_color and text_color != blank and text_color != 'null' %} style="color: {{ text_color }};"{% endif %}>
128
+ <!-- Vendor/Brand -->
129
+ {% if product.vendor or product.brand %}
130
+ <div class="product-vendor">
131
+ {{ product.vendor | default: product.brand.name }}
132
+ </div>
133
+ {% endif %}
134
+
135
+ <!-- Product Title -->
136
+ <h1 class="product-title">{{ product.name | default: product.title }}</h1>
137
+
138
+ <!-- Price -->
139
+ <div class="product-price-wrapper">
140
+ <span class="price-current" id="singleProductPrice">
141
+ {{ product.prices.price | money_with_settings: shop.settings }}
142
+ </span>
143
+ {% if product.prices.mrp and product.prices.mrp > product.prices.price %}
144
+ <span class="price-compare">
145
+ {{ product.prices.mrp | money_with_settings: shop.settings }}
146
+ </span>
147
+ {% endif %}
148
+ </div>
149
+
150
+ <!-- Short Description -->
151
+ {% if short_description and short_description != blank %}
152
+ <div class="product-short-description">
153
+ {{ short_description }}
154
+ </div>
155
+ {% endif %}
156
+
157
+ <!-- Product Form -->
158
+ <form class="product-form" id="singleProductForm" data-product-id="{{ product.productId }}">
159
+ <!-- Variations -->
160
+ {% if product.variations and product.variations.size > 0 %}
161
+ {% assign default_variant = null %}
162
+ {% for variant in product.variations %}
163
+ {% assign isAvailable = variant.inStock | default: variant.available | default: true %}
164
+ {% if default_variant == null and isAvailable %}
165
+ {% assign default_variant = variant %}
166
+ {% endif %}
167
+ {% endfor %}
168
+ {% if default_variant == null %}
169
+ {% assign default_variant = product.variations.first %}
170
+ {% endif %}
171
+
172
+ <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">
175
+ {% for variant in product.variations %}
176
+ {% assign isAvailable = variant.inStock | default: variant.available | default: true %}
177
+ {% assign variant_name = variant.name | default: '' %}
178
+ {% if variant_name == '' and variant.options and variant.options.size > 0 %}
179
+ {% assign variant_name = '' %}
180
+ {% for option in variant.options %}
181
+ {% if variant_name != '' %}
182
+ {% assign variant_name = variant_name | append: ' / ' %}
183
+ {% endif %}
184
+ {% assign variant_name = variant_name | append: option.value %}
185
+ {% endfor %}
186
+ {% endif %}
187
+ {% if variant_name == '' %}
188
+ {% assign variant_name = 'Option ' | append: forloop.index %}
189
+ {% endif %}
190
+ {% assign variant_price = variant.prices.price | default: product.prices.price %}
191
+ <option
192
+ value="{{ variant.productId }}"
193
+ data-variant-price="{{ variant_price }}"
194
+ data-variant-mrp="{{ variant.prices.mrp | default: 0 }}"
195
+ data-in-stock="{{ isAvailable }}"
196
+ {% if variant.productId == default_variant.productId %}selected{% endif %}>
197
+ {{ variant_name }}{% unless isAvailable %} — Out of Stock{% endunless %}
198
+ </option>
199
+ {% endfor %}
200
+ </select>
201
+ </div>
202
+ {% endif %}
203
+
204
+ <!-- Quantity (optional) -->
205
+ <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">
208
+ </div>
209
+
210
+ <!-- Add to Cart Button -->
211
+ {% if show_add_to_cart != false %}
212
+ {% assign default_variant_id = product.productId %}
213
+ {% if product.variations and product.variations.size > 0 %}
214
+ {% assign default_variant_id = default_variant.productId %}
215
+ {% endif %}
216
+
217
+ <button
218
+ type="submit"
219
+ class="btn-add-to-cart"
220
+ data-product-id="{{ default_variant_id }}"
221
+ data-base-product-id="{{ product.productId }}">
222
+ Add to Cart
223
+ </button>
224
+ {% endif %}
225
+ </form>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ {% else %}
230
+ <div class="widget-empty">
231
+ <p>No product configured for this widget.</p>
232
+ </div>
233
+ {% endif %}
234
+ </div>
235
+
236
+ <style>
237
+ .widget-single-product {
238
+ padding: 40px 0;
239
+ }
240
+
241
+ .widget-single-product__inner {
242
+ max-width: 1200px;
243
+ margin: 0 auto;
244
+ padding: 0 24px;
245
+ }
246
+
247
+ .widget-single-product .widget-header {
248
+ margin-bottom: 32px;
249
+ text-align: center;
250
+ }
251
+
252
+ .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;
258
+ }
259
+
260
+ .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;
266
+ }
267
+
268
+ /* PDP Layout */
269
+ .single-product-pdp__layout {
270
+ display: grid;
271
+ grid-template-columns: 1fr 1fr;
272
+ gap: 48px;
273
+ align-items: start;
274
+ }
275
+
276
+ /* Gallery */
277
+ .single-product-pdp__gallery {
278
+ display: flex;
279
+ flex-direction: column;
280
+ gap: 16px;
281
+ }
282
+
283
+ .gallery-main {
284
+ position: relative;
285
+ aspect-ratio: 1;
286
+ width: 100%;
287
+ background: #f5f5f5;
288
+ border-radius: var(--border-radius-medium);
289
+ overflow: hidden;
290
+ }
291
+
292
+ .gallery-main-image {
293
+ display: none;
294
+ width: 100%;
295
+ height: 100%;
296
+ object-fit: cover;
297
+ position: absolute;
298
+ top: 0;
299
+ left: 0;
300
+ }
301
+
302
+ .gallery-main-image.active {
303
+ display: block;
304
+ }
305
+
306
+ .gallery-placeholder {
307
+ width: 100%;
308
+ height: 100%;
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: center;
312
+ color: #999;
313
+ }
314
+
315
+ .gallery-thumbnails {
316
+ display: flex;
317
+ gap: 12px;
318
+ overflow-x: auto;
319
+ padding-bottom: 8px;
320
+ }
321
+
322
+ .gallery-thumbnail {
323
+ flex-shrink: 0;
324
+ width: 80px;
325
+ height: 80px;
326
+ border: 2px solid transparent;
327
+ border-radius: 6px;
328
+ overflow: hidden;
329
+ background: #f5f5f5;
330
+ cursor: pointer;
331
+ padding: 0;
332
+ transition: border-color 0.2s ease;
333
+ }
334
+
335
+ .gallery-thumbnail.active {
336
+ border-color: #111;
337
+ }
338
+
339
+ .gallery-thumbnail img {
340
+ width: 100%;
341
+ height: 100%;
342
+ object-fit: cover;
343
+ }
344
+
345
+ /* Product Info */
346
+ .single-product-pdp__info {
347
+ display: flex;
348
+ flex-direction: column;
349
+ gap: 20px;
350
+ }
351
+
352
+ .product-vendor {
353
+ font-size: 14px;
354
+ text-transform: uppercase;
355
+ letter-spacing: 0.05em;
356
+ color: #666;
357
+ font-weight: 500;
358
+ }
359
+
360
+ .product-title {
361
+ font-size: 32px;
362
+ font-weight: 600;
363
+ line-height: 1.2;
364
+ margin: 0;
365
+ color: #111;
366
+ }
367
+
368
+ .product-price-wrapper {
369
+ display: flex;
370
+ align-items: baseline;
371
+ gap: 12px;
372
+ }
373
+
374
+ .price-current {
375
+ font-size: 28px;
376
+ font-weight: 600;
377
+ color: #111;
378
+ }
379
+
380
+ .price-compare {
381
+ font-size: 20px;
382
+ color: #999;
383
+ text-decoration: line-through;
384
+ }
385
+
386
+ .product-short-description {
387
+ font-size: 16px;
388
+ line-height: 1.6;
389
+ color: #666;
390
+ margin-top: 8px;
391
+ }
392
+
393
+ /* Form */
394
+ .product-form {
395
+ display: flex;
396
+ flex-direction: column;
397
+ gap: 20px;
398
+ margin-top: 24px;
399
+ }
400
+
401
+ .product-variation-selector {
402
+ display: flex;
403
+ flex-direction: column;
404
+ gap: 8px;
405
+ }
406
+
407
+ .variation-label {
408
+ font-size: 14px;
409
+ font-weight: 600;
410
+ color: #111;
411
+ }
412
+
413
+ .variation-select {
414
+ 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;
422
+ }
423
+
424
+ .variation-select:hover {
425
+ border-color: #999;
426
+ }
427
+
428
+ .variation-select:focus {
429
+ outline: none;
430
+ border-color: #111;
431
+ box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05);
432
+ }
433
+
434
+ .product-quantity {
435
+ display: flex;
436
+ flex-direction: column;
437
+ gap: 8px;
438
+ }
439
+
440
+ .product-quantity label {
441
+ font-size: 14px;
442
+ font-weight: 600;
443
+ color: #111;
444
+ }
445
+
446
+ .quantity-input {
447
+ width: 120px;
448
+ padding: 12px 16px;
449
+ font-size: 16px;
450
+ border: 1px solid #e0e0e0;
451
+ border-radius: 8px;
452
+ }
453
+
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;
466
+ }
467
+
468
+ .btn-add-to-cart:hover:not(:disabled) {
469
+ background: #333;
470
+ }
471
+
472
+ .btn-add-to-cart:active:not(:disabled) {
473
+ transform: scale(0.98);
474
+ }
475
+
476
+ .btn-add-to-cart:disabled {
477
+ background: #d1d5db;
478
+ cursor: not-allowed;
479
+ }
480
+
481
+ .widget-empty {
482
+ padding: 60px 24px;
483
+ text-align: center;
484
+ color: #6b7280;
485
+ }
486
+
487
+ .widget-empty p {
488
+ margin: 0;
489
+ font-size: 14px;
490
+ }
491
+
492
+ /* Mobile */
493
+ @media (max-width: 768px) {
494
+ .widget-single-product {
495
+ padding: 32px 0;
496
+ }
497
+
498
+ .widget-single-product__inner {
499
+ padding: 0 16px;
500
+ }
501
+
502
+ .single-product-pdp__layout {
503
+ grid-template-columns: 1fr;
504
+ gap: 32px;
505
+ }
506
+
507
+ .product-title {
508
+ font-size: 24px;
509
+ }
510
+
511
+ .price-current {
512
+ font-size: 24px;
513
+ }
514
+
515
+ .price-compare {
516
+ font-size: 18px;
517
+ }
518
+ }
519
+
520
+ @media (max-width: 480px) {
521
+ .widget-single-product {
522
+ padding: 24px 0;
523
+ }
524
+
525
+ .widget-single-product__inner {
526
+ padding: 0 12px;
527
+ }
528
+
529
+ .gallery-thumbnails {
530
+ gap: 8px;
531
+ }
532
+
533
+ .gallery-thumbnail {
534
+ width: 60px;
535
+ height: 60px;
536
+ }
537
+
538
+ .product-title {
539
+ font-size: 20px;
540
+ }
541
+
542
+ .btn-add-to-cart {
543
+ padding: 14px 20px;
544
+ font-size: 16px;
545
+ }
546
+ }
547
+ </style>
548
+
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
+ });
580
+ }
581
+
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
+ });
602
+ } else {
603
+ console.warn('No add-to-cart handler available (Theme.addToCart / CartManager.addToCart)');
604
+ alert('Cart functionality not available. Please try again later.');
605
+ }
606
+ });
607
+ })();
608
+ </script>
609
+ {% endif %}
610
+ </section>