@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,502 @@
1
+ {% liquid
2
+ assign widget_settings = widget.settings
3
+ assign widget_data = widget.data
4
+
5
+ comment
6
+ Dynamic Widget: Products come from widget_data.products (enriched by WidgetService)
7
+ Settings come from widget_settings
8
+ endcomment
9
+ assign products = widget_data.products
10
+ assign heading = widget_settings.title
11
+ assign description = widget_settings.subtitle
12
+ assign show_widget_title_raw = widget_settings.showWidgetTitle | default: 'Yes'
13
+ if show_widget_title_raw == null or show_widget_title_raw == blank or show_widget_title_raw == 'null'
14
+ assign show_widget_title = true
15
+ else
16
+ if show_widget_title_raw == 'Yes'
17
+ assign show_widget_title = true
18
+ elsif show_widget_title_raw == true
19
+ assign show_widget_title = true
20
+ else
21
+ assign show_widget_title = false
22
+ endif
23
+ endif
24
+ assign widget_title_alignment_raw = widget_settings.widgetTitleAlignment | default: 'center'
25
+ assign widget_title_alignment = widget_title_alignment_raw | downcase
26
+ if widget_title_alignment != 'left' and widget_title_alignment != 'right' and widget_title_alignment != 'center'
27
+ assign widget_title_alignment = 'center'
28
+ endif
29
+ assign background_color = widget_settings.backgroundColor
30
+ %}
31
+
32
+ <section class="widget widget-product-carousel" data-widget-id="{{ widget.id }}" data-carousel-widget{% if background_color and background_color != blank and background_color != 'null' %} style="background-color: {{ background_color }};"{% endif %}>
33
+ <div class="carousel-container">
34
+ {% if show_widget_title %}
35
+ {% if heading or description %}
36
+ <header class="carousel-header">
37
+ <div class="carousel-header__text" style="text-align: {{ widget_title_alignment }};">
38
+ {% if heading %}
39
+ <h2 class="carousel-title">{{ heading }}</h2>
40
+ {% endif %}
41
+ {% if description %}
42
+ <p class="carousel-subtitle">{{ description }}</p>
43
+ {% endif %}
44
+ </div>
45
+ {% if products and products.size > 0 %}
46
+ <div class="carousel-nav">
47
+ <button type="button" class="carousel-arrow carousel-arrow--prev" data-carousel-prev aria-label="Previous">
48
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
49
+ <polyline points="15 18 9 12 15 6"></polyline>
50
+ </svg>
51
+ </button>
52
+ <button type="button" class="carousel-arrow carousel-arrow--next" data-carousel-next aria-label="Next">
53
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
54
+ <polyline points="9 18 15 12 9 6"></polyline>
55
+ </svg>
56
+ </button>
57
+ </div>
58
+ {% endif %}
59
+ </header>
60
+ {% endif %}
61
+ {% endif %}
62
+
63
+ {% if products and products.size > 0 %}
64
+ <div class="carousel-wrapper">
65
+ <!-- Side Navigation Arrows (Desktop) -->
66
+ <button type="button" class="carousel-side-arrow carousel-side-arrow--prev" data-carousel-prev aria-label="Previous">
67
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
68
+ <polyline points="15 18 9 12 15 6"></polyline>
69
+ </svg>
70
+ </button>
71
+
72
+ <div class="carousel-track-wrapper" data-carousel-viewport>
73
+ <div class="carousel-track" data-carousel-track>
74
+ {% for product in products %}
75
+ <div class="carousel-slide" data-slide-index="{{ forloop.index0 }}">
76
+ {% render 'snippets/product-card', product: product, widget: widget %}
77
+ </div>
78
+ {% endfor %}
79
+ </div>
80
+ </div>
81
+
82
+ <button type="button" class="carousel-side-arrow carousel-side-arrow--next" data-carousel-next aria-label="Next">
83
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
84
+ <polyline points="9 18 15 12 9 6"></polyline>
85
+ </svg>
86
+ </button>
87
+ </div>
88
+
89
+ <!-- Dot Indicators -->
90
+ <div class="carousel-dots" data-carousel-dots></div>
91
+ {% else %}
92
+ <div class="widget-empty">
93
+ <p>{{ widget_settings.empty_state | default: 'No products found.' }}</p>
94
+ </div>
95
+ {% endif %}
96
+ </div>
97
+
98
+ <style>
99
+ /* Product Carousel - True Carousel Behavior */
100
+ .widget-product-carousel {
101
+ padding: 48px 0;
102
+ overflow: hidden;
103
+ }
104
+
105
+ .widget-product-carousel .carousel-container {
106
+ max-width: 1400px;
107
+ margin: 0 auto;
108
+ padding: 0 24px;
109
+ }
110
+
111
+ /* Header */
112
+ .widget-product-carousel .carousel-header {
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: space-between;
116
+ margin-bottom: 28px;
117
+ gap: 16px;
118
+ position: static;
119
+ z-index: auto;
120
+ }
121
+
122
+ .widget-product-carousel .carousel-header__text {
123
+ flex: 1;
124
+ }
125
+
126
+ .widget-product-carousel .carousel-title {
127
+ font-size: 1.4rem;
128
+ font-weight: 500;
129
+ line-height: 1.3;
130
+ color: #111;
131
+ margin: 0;
132
+ letter-spacing: -0.01em;
133
+ }
134
+
135
+ .widget-product-carousel .carousel-subtitle {
136
+ font-size: 14px;
137
+ color: #6b7280;
138
+ margin: 6px 0 0 0;
139
+ }
140
+
141
+ /* Header Nav Arrows */
142
+ .widget-product-carousel .carousel-nav {
143
+ display: flex;
144
+ gap: 8px;
145
+ }
146
+
147
+ .widget-product-carousel .carousel-arrow {
148
+ width: 40px;
149
+ height: 40px;
150
+ border-radius: 50%;
151
+ border: 1px solid #e0e0e0;
152
+ background: #fff;
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ cursor: pointer;
157
+ color: #333;
158
+ transition: all 0.2s ease;
159
+ flex-shrink: 0;
160
+ }
161
+
162
+ .widget-product-carousel .carousel-arrow:hover:not(:disabled) {
163
+ background: #111;
164
+ color: #fff;
165
+ border-color: #111;
166
+ }
167
+
168
+ .widget-product-carousel .carousel-arrow:disabled {
169
+ opacity: 0.3;
170
+ cursor: not-allowed;
171
+ }
172
+
173
+ /* Carousel Wrapper with Side Arrows */
174
+ .widget-product-carousel .carousel-wrapper {
175
+ position: relative;
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 12px;
179
+ }
180
+
181
+ /* Side Navigation Arrows */
182
+ .widget-product-carousel .carousel-side-arrow {
183
+ display: none; /* Hidden by default, shown on desktop */
184
+ width: 30px;
185
+ height: 30px;
186
+ border-radius: 50%;
187
+ border: none;
188
+ background: #fff;
189
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
190
+ align-items: center;
191
+ justify-content: center;
192
+ cursor: pointer;
193
+ color: #333;
194
+ transition: all 0.2s ease;
195
+ flex-shrink: 0;
196
+ z-index: 10;
197
+ }
198
+
199
+ .widget-product-carousel .carousel-side-arrow:hover:not(:disabled) {
200
+ background: #111;
201
+ color: #fff;
202
+ transform: scale(1.05);
203
+ }
204
+
205
+ .widget-product-carousel .carousel-side-arrow:disabled {
206
+ opacity: 0;
207
+ pointer-events: none;
208
+ }
209
+
210
+ /* Track Wrapper */
211
+ .widget-product-carousel .carousel-track-wrapper {
212
+ flex: 1;
213
+ overflow: hidden;
214
+ position: relative;
215
+ }
216
+
217
+ /* The Scrolling Track */
218
+ .widget-product-carousel .carousel-track {
219
+ display: flex !important;
220
+ gap: 20px;
221
+ overflow-x: auto !important;
222
+ overflow-y: hidden !important;
223
+ scroll-snap-type: x mandatory;
224
+ scroll-behavior: smooth;
225
+ -webkit-overflow-scrolling: touch;
226
+ scrollbar-width: none;
227
+ -ms-overflow-style: none;
228
+ padding: 4px 0;
229
+ }
230
+
231
+ .widget-product-carousel .carousel-track::-webkit-scrollbar {
232
+ display: none;
233
+ }
234
+
235
+ /* Individual Slides - Desktop: 4 items */
236
+ .widget-product-carousel .carousel-slide {
237
+ flex: 0 0 calc(25% - 15px) !important;
238
+ min-width: 0 !important;
239
+ max-width: calc(25% - 15px) !important;
240
+ scroll-snap-align: start;
241
+ scroll-snap-stop: normal;
242
+ }
243
+
244
+ /* Override any product-card grid styles */
245
+ .widget-product-carousel .carousel-slide .product-card {
246
+ width: 100% !important;
247
+ max-width: 100% !important;
248
+ grid-column: unset !important;
249
+ grid-row: unset !important;
250
+ }
251
+
252
+ /* Dot Indicators */
253
+ .widget-product-carousel .carousel-dots {
254
+ display: flex;
255
+ justify-content: center;
256
+ gap: 8px;
257
+ margin-top: 20px;
258
+ }
259
+
260
+ .widget-product-carousel .carousel-dot {
261
+ width: 8px;
262
+ height: 8px;
263
+ border-radius: 50%;
264
+ background: #d1d5db;
265
+ border: none;
266
+ padding: 0;
267
+ cursor: pointer;
268
+ transition: all 0.2s ease;
269
+ }
270
+
271
+ .widget-product-carousel .carousel-dot:hover {
272
+ background: #9ca3af;
273
+ }
274
+
275
+ .widget-product-carousel .carousel-dot.active {
276
+ background: #111;
277
+ width: 24px;
278
+ border-radius: 4px;
279
+ }
280
+
281
+ /* Desktop: Show side arrows, hide header arrows */
282
+ @media (min-width: 1025px) {
283
+ .widget-product-carousel .carousel-side-arrow {
284
+ display: flex;
285
+ }
286
+ .widget-product-carousel .carousel-nav {
287
+ display: none;
288
+ }
289
+ }
290
+
291
+ /* Tablet: 3 items */
292
+ @media (max-width: 1024px) {
293
+ .widget-product-carousel .carousel-slide {
294
+ flex: 0 0 calc(33.333% - 14px) !important;
295
+ max-width: calc(33.333% - 14px) !important;
296
+ }
297
+ .widget-product-carousel .carousel-track {
298
+ gap: 20px;
299
+ }
300
+ }
301
+
302
+ /* Mobile: 2 items */
303
+ @media (max-width: 768px) {
304
+ .widget-product-carousel {
305
+ padding: 36px 0;
306
+ }
307
+ .widget-product-carousel .carousel-container {
308
+ padding: 0 16px;
309
+ }
310
+ .widget-product-carousel .carousel-title {
311
+ font-size: 1.3rem;
312
+ }
313
+ .widget-product-carousel .carousel-track {
314
+ gap: 12px;
315
+ padding-left: 0;
316
+ padding-right: 0;
317
+ }
318
+ .widget-product-carousel .carousel-slide {
319
+ flex: 0 0 calc(50% - 6px) !important;
320
+ max-width: calc(50% - 6px) !important;
321
+ }
322
+ .widget-product-carousel .carousel-arrow {
323
+ width: 36px;
324
+ height: 36px;
325
+ }
326
+ .widget-product-carousel .carousel-dots {
327
+ margin-top: 16px;
328
+ }
329
+ }
330
+
331
+ /* Small Mobile */
332
+ @media (max-width: 480px) {
333
+ .widget-product-carousel .carousel-container {
334
+ padding: 0 12px;
335
+ }
336
+ .widget-product-carousel .carousel-track {
337
+ gap: 10px;
338
+ }
339
+ .widget-product-carousel .carousel-slide {
340
+ flex: 0 0 calc(50% - 5px) !important;
341
+ max-width: calc(50% - 5px) !important;
342
+ }
343
+ .widget-product-carousel .carousel-title {
344
+ font-size: 1.2rem;
345
+ }
346
+ }
347
+ </style>
348
+
349
+ <script>
350
+ (function() {
351
+ // Wait for DOM
352
+ if (document.readyState === 'loading') {
353
+ document.addEventListener('DOMContentLoaded', initCarousel);
354
+ } else {
355
+ initCarousel();
356
+ }
357
+
358
+ function initCarousel() {
359
+ const widget = document.querySelector('[data-widget-id="{{ widget.id }}"]');
360
+ if (!widget) return;
361
+
362
+ const track = widget.querySelector('[data-carousel-track]');
363
+ const viewport = widget.querySelector('[data-carousel-viewport]');
364
+ const prevBtns = widget.querySelectorAll('[data-carousel-prev]');
365
+ const nextBtns = widget.querySelectorAll('[data-carousel-next]');
366
+ const dotsContainer = widget.querySelector('[data-carousel-dots]');
367
+ const slides = widget.querySelectorAll('.carousel-slide');
368
+
369
+ if (!track || slides.length === 0) return;
370
+
371
+ // Calculate items per view based on screen width
372
+ function getItemsPerView() {
373
+ const width = window.innerWidth;
374
+ if (width <= 480) return 2;
375
+ if (width <= 768) return 2;
376
+ if (width <= 1024) return 3;
377
+ return 4;
378
+ }
379
+
380
+ // Calculate number of pages
381
+ function getPageCount() {
382
+ const itemsPerView = getItemsPerView();
383
+ return Math.ceil(slides.length / itemsPerView);
384
+ }
385
+
386
+ // Get current page based on scroll position
387
+ function getCurrentPage() {
388
+ if (!track.scrollWidth || track.scrollWidth <= viewport.clientWidth) return 0;
389
+ const scrollRatio = track.scrollLeft / (track.scrollWidth - viewport.clientWidth);
390
+ return Math.round(scrollRatio * (getPageCount() - 1));
391
+ }
392
+
393
+ // Create dot indicators
394
+ function createDots() {
395
+ if (!dotsContainer) return;
396
+ dotsContainer.innerHTML = '';
397
+ const pageCount = getPageCount();
398
+ if (pageCount <= 1) return;
399
+
400
+ for (let i = 0; i < pageCount; i++) {
401
+ const dot = document.createElement('button');
402
+ dot.className = 'carousel-dot' + (i === 0 ? ' active' : '');
403
+ dot.setAttribute('aria-label', 'Go to page ' + (i + 1));
404
+ dot.addEventListener('click', () => goToPage(i));
405
+ dotsContainer.appendChild(dot);
406
+ }
407
+ }
408
+
409
+ // Update active dot
410
+ function updateDots() {
411
+ if (!dotsContainer) return;
412
+ const dots = dotsContainer.querySelectorAll('.carousel-dot');
413
+ const currentPage = getCurrentPage();
414
+ dots.forEach((dot, index) => {
415
+ dot.classList.toggle('active', index === currentPage);
416
+ });
417
+ }
418
+
419
+ // Update arrow states
420
+ function updateArrows() {
421
+ const atStart = track.scrollLeft <= 5;
422
+ const atEnd = track.scrollLeft >= track.scrollWidth - viewport.clientWidth - 5;
423
+
424
+ prevBtns.forEach(btn => {
425
+ btn.disabled = atStart;
426
+ });
427
+ nextBtns.forEach(btn => {
428
+ btn.disabled = atEnd;
429
+ });
430
+ }
431
+
432
+ // Navigate to specific page
433
+ function goToPage(pageIndex) {
434
+ const itemsPerView = getItemsPerView();
435
+ const slide = slides[0];
436
+ if (!slide) return;
437
+
438
+ const gap = parseFloat(getComputedStyle(track).gap) || 20;
439
+ const slideWidth = slide.offsetWidth + gap;
440
+ const scrollTo = pageIndex * itemsPerView * slideWidth;
441
+
442
+ track.scrollTo({ left: scrollTo, behavior: 'smooth' });
443
+ }
444
+
445
+ // Scroll by one page
446
+ function scrollByPage(direction) {
447
+ const itemsPerView = getItemsPerView();
448
+ const slide = slides[0];
449
+ if (!slide) return;
450
+
451
+ const gap = parseFloat(getComputedStyle(track).gap) || 20;
452
+ const scrollAmount = (slide.offsetWidth + gap) * Math.max(1, itemsPerView - 1);
453
+
454
+ track.scrollBy({ left: direction * scrollAmount, behavior: 'smooth' });
455
+ }
456
+
457
+ // Event listeners
458
+ prevBtns.forEach(btn => {
459
+ btn.addEventListener('click', (e) => {
460
+ e.preventDefault();
461
+ scrollByPage(-1);
462
+ });
463
+ });
464
+
465
+ nextBtns.forEach(btn => {
466
+ btn.addEventListener('click', (e) => {
467
+ e.preventDefault();
468
+ scrollByPage(1);
469
+ });
470
+ });
471
+
472
+ // Track scroll events
473
+ let scrollTimeout;
474
+ track.addEventListener('scroll', () => {
475
+ clearTimeout(scrollTimeout);
476
+ scrollTimeout = setTimeout(() => {
477
+ updateDots();
478
+ updateArrows();
479
+ }, 50);
480
+ }, { passive: true });
481
+
482
+ // Touch/swipe support is built-in via CSS scroll-snap
483
+
484
+ // Handle resize
485
+ let resizeTimeout;
486
+ window.addEventListener('resize', () => {
487
+ clearTimeout(resizeTimeout);
488
+ resizeTimeout = setTimeout(() => {
489
+ createDots();
490
+ updateArrows();
491
+ }, 200);
492
+ });
493
+
494
+ // Initialize
495
+ createDots();
496
+ updateArrows();
497
+ }
498
+ })();
499
+ </script>
500
+ </section>
501
+
502
+
@@ -0,0 +1,45 @@
1
+ {% liquid
2
+ assign widget_settings = widget.settings
3
+ assign widget_content = widget.content
4
+ assign widget_data = widget.data
5
+
6
+ comment
7
+ Dynamic Widget: Products come from widget_data.products (enriched by WidgetService)
8
+ Settings come from widget_settings
9
+ This widget supports filtering by CategoryId, BrandId, OrderBy, IsFeatured, NumberOfProducts
10
+ endcomment
11
+
12
+ assign products = widget_data.products
13
+ assign heading = widget_settings.title | default: widget.Title | default: widget.title
14
+ assign description = widget_settings.subtitle
15
+ assign columns = widget_settings.columns | default: 4
16
+ assign show_add_to_cart = widget_settings.show_add_to_cart | default: true
17
+ assign show_widget_title_raw = widget_settings.showWidgetTitle | default: 'Yes'
18
+ if show_widget_title_raw == null or show_widget_title_raw == blank or show_widget_title_raw == 'null'
19
+ assign show_widget_title = true
20
+ else
21
+ if show_widget_title_raw == 'Yes'
22
+ assign show_widget_title = true
23
+ elsif show_widget_title_raw == true
24
+ assign show_widget_title = true
25
+ else
26
+ assign show_widget_title = false
27
+ endif
28
+ endif
29
+ assign widget_title_alignment_raw = widget_settings.widgetTitleAlignment | default: 'center'
30
+ assign widget_title_alignment = widget_title_alignment_raw | downcase
31
+ if widget_title_alignment != 'left' and widget_title_alignment != 'right' and widget_title_alignment != 'center'
32
+ assign widget_title_alignment = 'center'
33
+ endif
34
+ %}
35
+
36
+ {% render 'widgets/shared/product-grid',
37
+ widget: widget,
38
+ heading: heading,
39
+ description: description,
40
+ products: products,
41
+ columns: columns,
42
+ show_add_to_cart: show_add_to_cart,
43
+ show_widget_title: show_widget_title,
44
+ widget_title_alignment: widget_title_alignment %}
45
+
@@ -0,0 +1,26 @@
1
+ {% liquid
2
+ assign widget_settings = widget.settings
3
+ assign widget_content = widget.content
4
+ assign widget_data = widget.data
5
+
6
+ assign products = widget_data.products | default: widget_content.products | default: widget_settings.products | default: cart.recentlyViewed
7
+ assign heading = widget_settings.title | default: widget_content.title | default: 'Recently viewed'
8
+ assign description = widget_settings.subtitle | default: widget_content.subtitle
9
+ assign columns = widget_settings.columns | default: widget_content.columns | default: 4
10
+ assign show_add_to_cart = widget_settings.show_add_to_cart
11
+ if show_add_to_cart == nil and widget_content and widget_content.show_add_to_cart != nil
12
+ assign show_add_to_cart = widget_content.show_add_to_cart
13
+ endif
14
+ if show_add_to_cart == nil
15
+ assign show_add_to_cart = false
16
+ endif
17
+ %}
18
+
19
+ {% render 'widgets/shared/product-grid',
20
+ widget: widget,
21
+ heading: heading,
22
+ description: description,
23
+ products: products,
24
+ columns: columns,
25
+ show_add_to_cart: show_add_to_cart %}
26
+